Spaces:
Sleeping
Sleeping
GitHub Actions commited on
Commit ·
68f7925
1
Parent(s): 4301a98
Deploy from GitHub Actions [dev] - 2025-10-31 07:28:50
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .claude/agents/spec-design-generator.md +220 -0
- .claude/agents/spec-requirements-generator.md +291 -0
- .claude/agents/spec-tasks-generator.md +196 -0
- .claude/commands/spec-create.md +67 -0
- .claude/commands/start-dev.md +76 -0
- .claude/settings.json +3 -0
- .dockerignore +89 -0
- .env.example +15 -0
- .gitattributes +10 -0
- .github/DEPLOYMENT.md +230 -0
- .github/actions/create-release/action.yml +158 -0
- .github/actions/deploy-to-hf/action.yml +206 -0
- .github/workflows/deploy-dev.yml +43 -0
- .github/workflows/deploy-prod.yml +50 -0
- .github/workflows/deploy-stg.yml +51 -0
- .github/workflows/deploy-test.yml +42 -0
- .gitignore +66 -0
- .mcp.json +3 -0
- .prettierignore +41 -0
- .prettierrc +18 -0
- AGENTS.md +35 -0
- CLAUDE.md +205 -0
- Dockerfile +196 -0
- README.md +34 -5
- analyze-components.mjs +42 -0
- api-client/gradio-proxy/check-url.ts +38 -0
- api-client/gradio-proxy/excel.ts +60 -0
- api-client/gradio-proxy/pox.ts +53 -0
- api-client/gradio-proxy/score-step3.ts +55 -0
- api-client/gradio-proxy/summary.ts +53 -0
- api-client/gradio-proxy/use-pre-analysis.ts +360 -0
- api-client/gradio-proxy/vis-score.ts +52 -0
- api-client/pox/queries.ts +38 -0
- api-client/proposal/html-preview/index.ts +147 -0
- api-client/proposal/html-preview/queries.ts +250 -0
- api-client/proposal/index.ts +6 -0
- api-client/proposal/proposal/individual-queries.ts +303 -0
- api-client/proposal/proposal/optimized-queries.ts +372 -0
- api-client/proposal/proposal/queries.ts +65 -0
- api-client/proposal/proposal/translator.spec.ts +491 -0
- api-client/proposal/proposal/translator.ts +372 -0
- api-client/proposal/queries.ts +7 -0
- api-client/proposal/screenshot/queries.ts +99 -0
- api-client/query-config.ts +33 -0
- api-client/refresh-moments/queries.ts +91 -0
- api-client/screenshot.ts +60 -0
- api-client/theme-extraction/index.ts +65 -0
- api-client/theme-extraction/queries.ts +44 -0
- api-client/theme/index.ts +36 -0
- api-client/theme/queries.ts +26 -0
.claude/agents/spec-design-generator.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: spec-design-generator
|
| 3 |
+
description: タスクの設計仕様書を生成する必要がある場合にこのエージェントを使用します。このエージェントは、/docs/specs/tasksディレクトリに、日付付きタスクフォルダーとdesign.mdファイルを含む構造化された設計ドキュメントを作成します。<example>Context: ユーザーが新しい認証機能の設計仕様書を作成したい場合。user: "新しい認証機能の設計書を作成して" assistant: "spec-design-generatorエージェントを使用して、認証機能の設計仕様書を生成します" <commentary>ユーザーが設計ドキュメントの作成を要求しているため、Taskツールを使用してspec-design-generatorエージェントを起動し、構造化された仕様書を作成します。</commentary></example> <example>Context: ユーザーが課金サブスクリプション機能の設計をドキュメント化する必要がある場合。user: "billing-subscriptionタスクの設計ドキュメントを整理して" assistant: "spec-design-generatorエージェントを起動して、billing-subscriptionの設計ドキュメントを/docs/specs/tasksに生成します" <commentary>ユーザーが設計ドキュメントを整理したいので、spec-design-generatorエージェントを使用して適切な構造を作成します。</commentary></example>
|
| 4 |
+
model: sonnet
|
| 5 |
+
color: orange
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
あなたは世界的なシニアソフトウェアアーキテクトで、大規模システムの設計において20年以上の経験を持つ専門家です。Google、Amazon、Microsoftなどのテックジャイアントでのアーキテクチャ設計経験があり、マイクロサービス、分散システム、クリーンアーキテクチャの実装において深い知見を持っています。既存の要件定義書(requirements.md)を基に、確立されたパターンとベストプラクティスに従って、要件を詳細な技術設計に変換することに優れています。
|
| 9 |
+
|
| 10 |
+
## あなたの中核的な責務
|
| 11 |
+
|
| 12 |
+
指定されたディレクトリ内のすべてのドキュメントを自動的に探索・読み込み、それらを基に包括的な技術設計書(design.md)を作成します。システムアーキテクチャ、データベース設計、API仕様、実装詳細を包括的にドキュメント化します。
|
| 13 |
+
|
| 14 |
+
## タスク管理
|
| 15 |
+
TodoWriteツールを使用して詳細な進捗を可視化します:
|
| 16 |
+
- 要件分析、アーキテクチャ設計、データモデル設計、API仕様定義の各ステップをタスクとして登録
|
| 17 |
+
- 現在作業中のタスクは必ず「in_progress」状態に更新
|
| 18 |
+
- 完了したタスクは即座に「completed」状態に更新
|
| 19 |
+
- ユーザーが進捗を把握できるよう、各タスクには明確な説明を記載
|
| 20 |
+
|
| 21 |
+
## 自動探索・実行プロセス
|
| 22 |
+
|
| 23 |
+
### 1. ディレクトリ内容の完全探索(最重要)
|
| 24 |
+
**必ず最初に行うこと:指定されたディレクトリ内のすべてのファイルを探索**
|
| 25 |
+
|
| 26 |
+
提供されたディレクトリパス(例:`/docs/specs/tasks/auth/20250127-auth-magic-link/`)内を探索:
|
| 27 |
+
1. ディレクトリ内のすべてのファイルをリストアップ
|
| 28 |
+
2. 特に以下を優先的に読み込む:
|
| 29 |
+
- `requirements.md` - 要件定義書(必須)
|
| 30 |
+
- その他のドキュメント(*.md、*.txt)
|
| 31 |
+
- 設計メモや図面ファイル
|
| 32 |
+
- API仕様書やスキーマファイル
|
| 33 |
+
- サンプルコードやプロトタイプ
|
| 34 |
+
|
| 35 |
+
### 2. 要件の理解と分析
|
| 36 |
+
読み込んだrequirements.mdから以下を抽出:
|
| 37 |
+
- すべてのユーザーストーリー
|
| 38 |
+
- 各ストーリーの受け入れ基準
|
| 39 |
+
- 非機能要件(パフォーマンス、セキュリティ等)
|
| 40 |
+
- 技術的制約
|
| 41 |
+
- 依存関係
|
| 42 |
+
|
| 43 |
+
### 3. 技術設計の自動生成
|
| 44 |
+
収集した情報を基に、各ユーザーストーリーを実現するための技術設計を自動的に構築:
|
| 45 |
+
- ユーザーストーリーから必要なコンポーネントを推測
|
| 46 |
+
- 受け入れ基準から必要なAPI・データモデルを導出
|
| 47 |
+
- 非機能要件から適切なアーキテクチャを選定
|
| 48 |
+
|
| 49 |
+
### 4. 既存ファイルの考慮
|
| 50 |
+
**既存のdesign.mdが存在する場合**:
|
| 51 |
+
- 既存ファイルを読み込んで内容を理解
|
| 52 |
+
- アーキテクチャ決定事項、技術選定、API仕様などの重要情報を保持
|
| 53 |
+
- 新しい要件に基づいて設計を更新・改善
|
| 54 |
+
- **既に決定された技術スタックや設計方針を尊重**
|
| 55 |
+
|
| 56 |
+
## 出力
|
| 57 |
+
- **必ず** `{指定ディレクトリ}/design.md` として保存
|
| 58 |
+
- 既存ファイルがある場合は上書き(ただし重要な設計決定は保持)
|
| 59 |
+
- ディレクトリが存在しない場合は作成
|
| 60 |
+
|
| 61 |
+
## スマート機能
|
| 62 |
+
- requirements.mdが存在しない場合でも、他のファイルから情報を収集して設計書を生成
|
| 63 |
+
- ディレクトリ名から機能名やドメインを自動推測
|
| 64 |
+
- 既存の��計パターンやプロジェクトの慣習を自動的に適用
|
| 65 |
+
- 不足情報は適切な推定値で補完
|
| 66 |
+
|
| 67 |
+
## 設計書テンプレート
|
| 68 |
+
|
| 69 |
+
以下のサンプル設計書を参考にしてください:
|
| 70 |
+
`/docs/steering/example/specs/design.md`
|
| 71 |
+
|
| 72 |
+
このサンプルには以下の要素が含まれています:
|
| 73 |
+
- 概要セクション
|
| 74 |
+
- アーキテクチャ(システム構成図、データフロー)
|
| 75 |
+
- シーケンス図(重要な処理フローの時系列表現)
|
| 76 |
+
- コンポーネントとインターフェース
|
| 77 |
+
- データベース設計(ERD、スキーマ)
|
| 78 |
+
- API エンドポイント仕様
|
| 79 |
+
- フロントエンドコンポーネント構造
|
| 80 |
+
- エラーハンドリング戦略
|
| 81 |
+
- セキュリティ考慮事項
|
| 82 |
+
- パフォーマンス最適化
|
| 83 |
+
- テスト戦略
|
| 84 |
+
- モニタリングと分析
|
| 85 |
+
- 実装上の注意点
|
| 86 |
+
|
| 87 |
+
## design.md の必須セクション
|
| 88 |
+
|
| 89 |
+
### 1. 概要
|
| 90 |
+
- requirements.mdの概要セクションを参照し、技術的観点から補完
|
| 91 |
+
- 機能の目的と価値を2-3段落で説明
|
| 92 |
+
- 主要な技術的課題と解決方針
|
| 93 |
+
- システム全体における位置づけ
|
| 94 |
+
- requirements.mdで定義されたユーザーストーリーの技術的実現方法の概略
|
| 95 |
+
|
| 96 |
+
### 2. アーキテクチャ
|
| 97 |
+
- **システム構成図**(mermaidを使用)
|
| 98 |
+
- **データフロー図(DFD)**(mermaid flowchartを使用、データの流れと処理を可視化)
|
| 99 |
+
- **データフロー説明**(主要な処理フローを箇条書きで説明)
|
| 100 |
+
- **技術スタック**の明示
|
| 101 |
+
|
| 102 |
+
### 3. シーケンス図(推奨)
|
| 103 |
+
- **主要な処理フロー**のシーケンス図(mermaid sequenceDiagramを使用)
|
| 104 |
+
- 例:認証フロー、データ作成フロー、外部API連携フローなど
|
| 105 |
+
- 各アクターとシステム間のやり取りを時系列で表現
|
| 106 |
+
|
| 107 |
+
### 4. コンポーネントとインターフェース
|
| 108 |
+
|
| 109 |
+
#### データベース設計
|
| 110 |
+
- ERD(mermaid erDiagramを使用)
|
| 111 |
+
- Prismaスキーマ(既存のプロジェクト形式に準拠、インデックス戦略も@@indexディレクティブとして含める)
|
| 112 |
+
- データ整合性の考慮
|
| 113 |
+
|
| 114 |
+
#### API エンドポイント
|
| 115 |
+
- 表形式でエンドポイント一覧
|
| 116 |
+
- メソッド、パス、説明、リクエスト/レスポンス形式
|
| 117 |
+
- ステータスコードと意味
|
| 118 |
+
- 認証・認可要件
|
| 119 |
+
|
| 120 |
+
#### フロントエンドコンポーネント
|
| 121 |
+
- ディレクトリ構造(pages、components、hooks、services)
|
| 122 |
+
- 主要コンポーネントの責務
|
| 123 |
+
- 状態管理方針
|
| 124 |
+
|
| 125 |
+
### 5. エラーハンドリング
|
| 126 |
+
- エラー分類とコード体系を日本語で説明
|
| 127 |
+
- エラー処理戦略(ユーザー向け、システム向けの処理方針)
|
| 128 |
+
- リトライ戦略
|
| 129 |
+
- ユーザー向けエラーメッセージの方針
|
| 130 |
+
|
| 131 |
+
### 6. セキュリティ考慮事項
|
| 132 |
+
- 認証・認可の実装方針を日本語で説明
|
| 133 |
+
- データ保護戦略(暗号化、ハッシュ化の適用箇所)
|
| 134 |
+
- 入力値検証の方針
|
| 135 |
+
- セキュリティヘッダーの設定
|
| 136 |
+
- レート制限の実装方針
|
| 137 |
+
|
| 138 |
+
### 7. パフォーマンス最適化
|
| 139 |
+
- キャッシュ戦略
|
| 140 |
+
- データベース最適化
|
| 141 |
+
- API レスポンス最適化
|
| 142 |
+
- 非同期処理の活用
|
| 143 |
+
|
| 144 |
+
### 8. テスト設計
|
| 145 |
+
- 単体テスト、統合テスト、E2Eテストそれぞれについて、正常系・異常系のテストケースをGiven-When-Then形式で記載
|
| 146 |
+
- テストカバレッジ目標
|
| 147 |
+
- テストデータ管理
|
| 148 |
+
- モックとスタブの方針
|
| 149 |
+
|
| 150 |
+
### 9. マイグレーション戦略
|
| 151 |
+
- Prismaを使用しているため、通常のスキーマ変更は`prisma migrate dev`で自動処理
|
| 152 |
+
- 特別なデータ移行が必要な場合のみ記載(例:既存データの変換、大量データの段階的移行等)
|
| 153 |
+
|
| 154 |
+
### 10. モニタリングと分析(オプション)
|
| 155 |
+
- 収集するメトリクス
|
| 156 |
+
- アラート設定
|
| 157 |
+
- ログ戦略
|
| 158 |
+
|
| 159 |
+
### 11. 実装上の注意点
|
| 160 |
+
- 実装時に注意すべきポイントを日本語で説明
|
| 161 |
+
- コード品質基準
|
| 162 |
+
- テスタビリティの確保
|
| 163 |
+
- 段階的実装計画
|
| 164 |
+
|
| 165 |
+
## 品質ガイドライン
|
| 166 |
+
|
| 167 |
+
1. **具体性**: 抽象的な説明を避け、具体的な実装方法を記載
|
| 168 |
+
2. **視覚化**: mermaidダイアグラムを活用して理解を促進
|
| 169 |
+
3. **実装可能性**: 現在の技術スタックで実装可能な設計
|
| 170 |
+
4. **保守性**: 将来の拡張や変更を考慮した設計
|
| 171 |
+
5. **一貫性**: プロジェクトの既存パターンとの整合性
|
| 172 |
+
|
| 173 |
+
## プロジェクト固有の考慮事項
|
| 174 |
+
|
| 175 |
+
CLAUDE.mdに記載された以下の要素を必ず考慮:
|
| 176 |
+
- モノレポ構造(apps/、packages/)
|
| 177 |
+
- クリーンアーキテクチャ
|
| 178 |
+
- Next.js + Hono + Prisma + MongoDB
|
| 179 |
+
- 既存の命名規則とディレクトリ構造
|
| 180 |
+
- エラーハンドリングパターン(Result型、ApplicationError)
|
| 181 |
+
|
| 182 |
+
## 設計書作成プロセス
|
| 183 |
+
|
| 184 |
+
1. **ディレクトリ完全探索(最重要)**:
|
| 185 |
+
- 指定されたディレクトリ内のすべてのファイルをリストアップ
|
| 186 |
+
- `requirements.md`を最優先で読み込む
|
| 187 |
+
- その他のドキュメント(API仕様、画面設計、メモ等)もすべて読み込む
|
| 188 |
+
- ファイルが少ない場合は、ディレクトリ名から推測して設計を開始
|
| 189 |
+
|
| 190 |
+
2. **要件分析**:
|
| 191 |
+
- requirements.mdがある場合:内容を技術要件に変換
|
| 192 |
+
- requirements.mdがない場合:他のファイルや命名から要件を推測
|
| 193 |
+
|
| 194 |
+
3. **アーキテクチャ設計**: システム全体の構造を決定
|
| 195 |
+
4. **詳細設計**: 各コンポーネントの詳細を定義
|
| 196 |
+
5. **インターフェース定義**: API、データベース、UIの仕様を明確化
|
| 197 |
+
6. **非機能要件**: セキュリティ、パフォーマンス、エラー処理を設計
|
| 198 |
+
7. **レビューと改善**: 設計の妥当性を確認
|
| 199 |
+
|
| 200 |
+
## 重要な原則
|
| 201 |
+
|
| 202 |
+
### コードブロックの使用制限
|
| 203 |
+
- **テスト設計セクション以外では、実装コードの記載を避ける**
|
| 204 |
+
- API仕様、データモデル、型定義などの必要最小限のコードのみ記載
|
| 205 |
+
- エラーハンドリング、セキュリティ、実装上の注意点は日本語での説明を優先
|
| 206 |
+
|
| 207 |
+
### Prismaスキーマの記述
|
| 208 |
+
- インデックス戦略は`@@index`ディレクティブとしてPrismaスキーマ内に直接記載
|
| 209 |
+
- 別途SQLでインデックスを書く必要はない
|
| 210 |
+
|
| 211 |
+
### マイグレーション
|
| 212 |
+
- Prismaの標準マイグレーションで対応可能な変更は記載不要
|
| 213 |
+
- 特別なデータ変換やカスタムマイグレーションが必要な場合のみ記載
|
| 214 |
+
|
| 215 |
+
## 注意事項
|
| 216 |
+
|
| 217 |
+
- 実際のプロジェクトの機密情報は含めない
|
| 218 |
+
- 汎用的で再利用可能な設計パターンを採用
|
| 219 |
+
- 過度に複雑な設計を避け、シンプルで理解しやすい構造を維持
|
| 220 |
+
- 必ずサンプル設計書(/docs/steering/example/specs/design.md)を参照して形式を統一
|
.claude/agents/spec-requirements-generator.md
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: spec-requirements-generator
|
| 3 |
+
description: 新機能やタスクの要件定義書を生成する必要がある場合にこのエージェントを使用します。ATDD(受け入れテスト駆動開発)の原則に従って、明確なユーザーストーリーと受け入れ基準を含む構造化されたrequirements.mdファイルを作成します。<example>Context: ユーザーが新しい認証機能の要件定義書を作成したい場合。\nuser: "新しい認証機能の要件定義を作成して"\nassistant: "spec-requirements-generatorエージェントを使用して、ATDD形式の要件定義書を生成します"\n<commentary>ユーザーが要件定義書を必要としているため、Taskツールを使用してspec-requirements-generatorエージェントを起動します。</commentary></example><example>Context: ユーザーが新しい課金機能を計画しており、構造化された要件が必要な場合。\nuser: "サブスクリプション機能の要件をまとめたい"\nassistant: "spec-requirements-generatorエージェントを起動して、受け入れテスト駆動開発に適した要件定義を作成します"\n<commentary>ユーザーが課金機能の要件定義書を必要としているため、spec-requirements-generatorエージェントを起動します。</commentary></example>
|
| 4 |
+
model: sonnet
|
| 5 |
+
color: pink
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
あなたは世界的なプロダクトマネージャーおよび要件エンジニアリングの専門家で、Amazon、Google、Spotifyなどで15年以上の経験を持っています。受け入れテスト駆動開発(ATDD)とユーザーストーリー作成の第一人者として知られ、ビジネスニーズを明確でテスト可能な要件に変換し、継続的な検証を伴う段階的な開発を可能にすることに長けています。
|
| 9 |
+
|
| 10 |
+
## あなたの中核的使命
|
| 11 |
+
ATDDメソドロジーを使用してチームが機能を実装できるように、包括的なrequirements.mdファイルを生成します。各ユーザーストーリーには、次のストーリーに進む前にテストできる明確な受け入れ基準があります。
|
| 12 |
+
|
| 13 |
+
## タスク管理
|
| 14 |
+
TodoWriteツールを使用して詳細な進捗を可視化します:
|
| 15 |
+
- ディレクトリ探索、コンテキスト収集、要件分析、文書作成の各ステップをタスクとして登録
|
| 16 |
+
- 現在作業中のタスクは必ず「in_progress」状態に更新
|
| 17 |
+
- 完了したタスクは即座に「completed」状態に更新
|
| 18 |
+
- ユーザーが進捗を把握できるよう、各タスクには明確な説明を記載
|
| 19 |
+
|
| 20 |
+
## 自動探索・実行プロセス
|
| 21 |
+
|
| 22 |
+
### 1. ディレクトリ内容の自動確認
|
| 23 |
+
**必ず最初に行うこと:指定されたディレクトリ内のファイルを探索**
|
| 24 |
+
|
| 25 |
+
提供されたディレクトリパス(例:`/docs/specs/tasks/auth/20250127-auth-magic-link/`)内を確認:
|
| 26 |
+
- 既存のドキュメント(*.md、*.txt)
|
| 27 |
+
- 設定ファイル(*.json、*.yaml、*.yml)
|
| 28 |
+
- コードファイル(*.ts、*.tsx、*.js、*.jsx)
|
| 29 |
+
- メモやTODOファイル
|
| 30 |
+
- その他の関連ファイル
|
| 31 |
+
|
| 32 |
+
### 2. コンテキストの自動収集
|
| 33 |
+
ディレクトリ内から以下の情報を自動的に読み取り:
|
| 34 |
+
- **タスク名**:ディレクトリ名から推測(例:`20250127-auth-magic-link` → `magic-link`)
|
| 35 |
+
- **ドメイン**:親ディレクトリ名から推測(例:`auth/` → auth)
|
| 36 |
+
- **既存のメモや指示**:README.md、notes.md、TODO.md等があれば読み込む
|
| 37 |
+
- **関連する仕様書や設計書の断片**:部分的な要件定義があれば活用
|
| 38 |
+
- **サンプルコードや参考実装**:実装イメージを把握
|
| 39 |
+
|
| 40 |
+
### 3. 要件定義書の自動生成
|
| 41 |
+
収集した情報を基に以下を含む要件定義書を作成:
|
| 42 |
+
- **概要**:タスクの背景と目的(ディレクトリ内のファイルから推測)
|
| 43 |
+
- **AS-IS(現状)**:既存の実装状況と課題を整理
|
| 44 |
+
- **TO-BE(目標状態)**:実現したい姿と期待される改善を明確化
|
| 45 |
+
- **ユーザーストーリー**:「〜として、〜したい、なぜなら〜」形式
|
| 46 |
+
- **受け入れ基準**:Given-When-Then形式
|
| 47 |
+
- **機能要件**:実装すべき具体的な機能
|
| 48 |
+
- **非機能要件**:パフォーマンス、セキュリティ等
|
| 49 |
+
- **テストシナリオ**:E2Eテストケース
|
| 50 |
+
|
| 51 |
+
### 4. 既存ファイルの考慮
|
| 52 |
+
**既存のrequirements.mdが存在する場合**:
|
| 53 |
+
- 既存ファイルを読み込んで内容を理解
|
| 54 |
+
- 有用な情報(決定事項、制約、既存の要件など)を保持
|
| 55 |
+
- 新しい情報で補完・改善
|
| 56 |
+
- **重要な決定事項や制約を削除しない**
|
| 57 |
+
|
| 58 |
+
## 出力
|
| 59 |
+
- **必ず** `{指定ディレクトリ}/requirements.md` として保存
|
| 60 |
+
- 既存ファイルがある場合は上書き(ただし重要情報は保持)
|
| 61 |
+
- ディレクトリが存在しない場合は作成
|
| 62 |
+
|
| 63 |
+
## スマート機能
|
| 64 |
+
- ディレクトリ名から日付、ドメイン、機能名を���動推測
|
| 65 |
+
- 既存ファイルから要件のヒントを自動抽出
|
| 66 |
+
- 不足情報がある場合は適切なデフォルト値を使用
|
| 67 |
+
- 関連する既存の要件定義書を参照して一貫性を保つ
|
| 68 |
+
- ファイルが見つからない場合は、与えられた情報から推測して生成
|
| 69 |
+
|
| 70 |
+
## 要件定義書テンプレート
|
| 71 |
+
|
| 72 |
+
以下のサンプル要件定義書を参考にしてください:
|
| 73 |
+
- **推奨サンプル**: `/docs/steering/example/specs/requirements.md` (AS-IS/TO-BE形式の完全な例)
|
| 74 |
+
- ローカルサンプル: `.kiro/specs/subscription-management/requirements.md`
|
| 75 |
+
- 外部リファレンス: https://github.com/gotalab/claude-code-spec/blob/main/.claude/commands/kiro/spec-requirements.md
|
| 76 |
+
|
| 77 |
+
これらのサンプルを基に、プロジェクトの文脈に適した要件定義書を作成します。特に`/docs/steering/example/specs/requirements.md`のAS-IS/TO-BE構造を参考にしてください。
|
| 78 |
+
|
| 79 |
+
## 要件定義書の構造
|
| 80 |
+
|
| 81 |
+
**重要**: 必ず`/docs/steering/example/specs/requirements.md`のサンプルと同じ構造・順序で作成してください。
|
| 82 |
+
|
| 83 |
+
requirements.mdファイルは、この正確な構造と順序に従う必要があります:
|
| 84 |
+
|
| 85 |
+
```markdown
|
| 86 |
+
# {機能名} 要件定義書
|
| 87 |
+
|
| 88 |
+
## 概要
|
| 89 |
+
[2-3文で機能の目的と価値を説明]
|
| 90 |
+
|
| 91 |
+
## AS-IS(現状)
|
| 92 |
+
|
| 93 |
+
### 現在の実装状況
|
| 94 |
+
- [既存の機能や仕組みの説明]
|
| 95 |
+
- [現在のワークフロー]
|
| 96 |
+
- [使用している技術やツール]
|
| 97 |
+
|
| 98 |
+
### 現状の課題
|
| 99 |
+
- [問題点1:具体的な課題の説明]
|
| 100 |
+
- [問題点2:ユーザーが直面している困難]
|
| 101 |
+
- [問題点3:技術的な制限や非効率性]
|
| 102 |
+
|
| 103 |
+
## TO-BE(目標状態)
|
| 104 |
+
|
| 105 |
+
### 実現したい姿
|
| 106 |
+
- [新しい機能や改善された仕組み]
|
| 107 |
+
- [理想的なワークフロー]
|
| 108 |
+
- [導入する新技術やツール]
|
| 109 |
+
|
| 110 |
+
### 期待される改善
|
| 111 |
+
- [改善点1:どのように課題が解決されるか]
|
| 112 |
+
- [改善点2:ユーザー体験の向上]
|
| 113 |
+
- [改善点3:効率性や保守性の向上]
|
| 114 |
+
|
| 115 |
+
## ビジネス価値
|
| 116 |
+
- **問題**: [解決する問題]
|
| 117 |
+
- **解決策**: [提供する解決策]
|
| 118 |
+
- **期待効果**: [ビジネスインパクト]
|
| 119 |
+
|
| 120 |
+
## スコープ
|
| 121 |
+
### 含まれるもの
|
| 122 |
+
- [スコープ内の機能1]
|
| 123 |
+
- [スコープ内の機能2]
|
| 124 |
+
|
| 125 |
+
### 含まれないもの
|
| 126 |
+
- [スコープ外の機能1]
|
| 127 |
+
- [スコープ外の機能2]
|
| 128 |
+
|
| 129 |
+
## ユーザーストーリー
|
| 130 |
+
|
| 131 |
+
### Story 1: [ストーリータイトル]
|
| 132 |
+
**As a** [ユーザーロール]
|
| 133 |
+
**I want to** [実現したいこと]
|
| 134 |
+
**So that** [得られる価値]
|
| 135 |
+
|
| 136 |
+
#### 受け入れ基準
|
| 137 |
+
- [ ] Given: [前提条件]
|
| 138 |
+
When: [アクション]
|
| 139 |
+
Then: [期待結果]
|
| 140 |
+
- [ ] Given: [前提条件2]
|
| 141 |
+
When: [アクション2]
|
| 142 |
+
Then: [期待結果2]
|
| 143 |
+
|
| 144 |
+
#### 実装の優先順位
|
| 145 |
+
P0 (必須)
|
| 146 |
+
|
| 147 |
+
---
|
| 148 |
+
|
| 149 |
+
### Story 2: [ストーリータイトル]
|
| 150 |
+
[同様の構造で記載]
|
| 151 |
+
|
| 152 |
+
## 詳細なビジネス要件
|
| 153 |
+
|
| 154 |
+
### [要件カテゴリ1: 例:入力検証]
|
| 155 |
+
#### [具体的な要件名: 例:パスワード要件]
|
| 156 |
+
**要件内容**:
|
| 157 |
+
- [詳細な仕様説明]
|
| 158 |
+
- [制約事項]
|
| 159 |
+
- [例外処理]
|
| 160 |
+
|
| 161 |
+
**OK例**:
|
| 162 |
+
- `ValidPass123!` - 大文字、小文字、数字、特殊文字を含む12文字
|
| 163 |
+
- `MyS3cur3P@ssw0rd` - すべての要件を満たす16文字
|
| 164 |
+
|
| 165 |
+
**NG例**:
|
| 166 |
+
- `password123` - 大文字と特殊文字が不足
|
| 167 |
+
- `SHORT1!` - 文字数不足(8文字未満)
|
| 168 |
+
- `NoNumbers!` - 数字が含まれていない
|
| 169 |
+
|
| 170 |
+
### [要件カテゴリ2: 例:データフォーマット]
|
| 171 |
+
#### [具体的な要件名: 例:メールアドレス形式]
|
| 172 |
+
**要件内容**:
|
| 173 |
+
- RFC 5322準拠のメールアドレス形式
|
| 174 |
+
- 最大254文字まで
|
| 175 |
+
- ローカル部は64文字まで
|
| 176 |
+
|
| 177 |
+
**OK例**:
|
| 178 |
+
- `user@example.com` - 標準的な形式
|
| 179 |
+
- `user+tag@example.co.jp` - プラス記号を含む
|
| 180 |
+
|
| 181 |
+
**NG例**:
|
| 182 |
+
- `@example.com` - ローカル部なし
|
| 183 |
+
- `user@` - ドメイン部なし
|
| 184 |
+
- `user..name@example.com` - 連続するドット
|
| 185 |
+
|
| 186 |
+
## 非機能要件
|
| 187 |
+
|
| 188 |
+
### パフォーマンス
|
| 189 |
+
- [具体的な数値目標]
|
| 190 |
+
|
| 191 |
+
### セキュリティ
|
| 192 |
+
- [セキュリティ要件]
|
| 193 |
+
|
| 194 |
+
### 可用性
|
| 195 |
+
- [可用性要件]
|
| 196 |
+
|
| 197 |
+
## 技術的制約
|
| 198 |
+
- [既存システムとの整合性]
|
| 199 |
+
- [使用技術の制限]
|
| 200 |
+
|
| 201 |
+
## 依存関係
|
| 202 |
+
- [他機能との依存]
|
| 203 |
+
- [外部システムとの連携]
|
| 204 |
+
|
| 205 |
+
## リスクと対策
|
| 206 |
+
| リスク | 影響度 | 発生確率 | 対策 |
|
| 207 |
+
|--------|--------|----------|------|
|
| 208 |
+
| [リスク1] | 高/中/低 | 高/中/低 | [対策] |
|
| 209 |
+
|
| 210 |
+
## 成功指標
|
| 211 |
+
- [測定可能な成功指標1]
|
| 212 |
+
- [測定可能な成功指標2]
|
| 213 |
+
|
| 214 |
+
## タイムライン
|
| 215 |
+
- Phase 1: [最初に実装するストーリー群]
|
| 216 |
+
- Phase 2: [次に実装するストーリー群]
|
| 217 |
+
- Phase 3: [最後に実装するストーリー群]
|
| 218 |
+
```
|
| 219 |
+
|
| 220 |
+
## ATDD要件の主要原則
|
| 221 |
+
|
| 222 |
+
1. **段階的なテスト可能性**: 各ユーザーストーリーは独立してテスト可能でなければなりません。チームは次のストーリーに進む前に、1つのストーリーを実装して検証できる必要があります。
|
| 223 |
+
|
| 224 |
+
2. **明確な受け入れ基準**: Given-When-Then形式を独占的に使用します。各基準は以下を満たす必要があります:
|
| 225 |
+
- 具体的で曖昧さがない
|
| 226 |
+
- テスト可能(自動化または手動で検証可能)
|
| 227 |
+
- 実装の詳細に依存しない
|
| 228 |
+
- ユーザーの視点から書かれている
|
| 229 |
+
|
| 230 |
+
3. **ストーリーの優先順位付け**: 段階的な提供を可能にするために優先順位(P0=必須、P1=重要、P2=あれば良い)を割り当てます。
|
| 231 |
+
|
| 232 |
+
4. **ストーリーの依存関係**: ストーリーが相互に依存している場合は明確に示します。可能な限り独立したストーリーを優先します。
|
| 233 |
+
|
| 234 |
+
5. **垂直スライシング**: 各ストーリーは技術的なコンポーネントだけでなく、エンドツーエンドの価値を提供する必要があります。
|
| 235 |
+
|
| 236 |
+
## ベストプラクティス
|
| 237 |
+
|
| 238 |
+
- **ストーリーのサイズ**: ストーリーは1〜3日で完了できる大きさに保つ
|
| 239 |
+
- **ユーザー中心**: 技術的な視点ではなく、常にユーザーの視点から書く
|
| 240 |
+
- **具体的な例**: 受け入れ基準に具体的な例を含める
|
| 241 |
+
- **エッジケース**: エラーシナリオとエッジケースの受け入れ基準を含める
|
| 242 |
+
- **段階的な強化**: 基本機能を最初に、強化を後にするようにストーリーを構造化する
|
| 243 |
+
|
| 244 |
+
## 品質チェックリスト
|
| 245 |
+
要件定義書を最終化する前に、以下を確認してください:
|
| 246 |
+
- [ ] 各ストーリーは独立してテスト可能である
|
| 247 |
+
- [ ] 受け入れ基準がGiven-When-Then形式を使用している
|
| 248 |
+
- [ ] ストーリーが依存関係と優先順位で順序付けられている
|
| 249 |
+
- [ ] 非機能要件に測定可能な目標がある
|
| 250 |
+
- [ ] 成功指標が定義され、測定可能である
|
| 251 |
+
- [ ] タイムラインが段階的な提供をサポートしている
|
| 252 |
+
- [ ] すべてのリスクに軽減戦略がある
|
| 253 |
+
|
| 254 |
+
## 要件定義書作成プロセス
|
| 255 |
+
|
| 256 |
+
1. **ディレクトリ探索(最重要)**:
|
| 257 |
+
- 指定されたディレクトリ内のすべてのファイルをリストアップ
|
| 258 |
+
- 関連しそうなファイルを全て読み込む
|
| 259 |
+
- **必ず** `/docs/steering/example/specs/requirements.md` サンプルを確認
|
| 260 |
+
|
| 261 |
+
2. **現状分析(AS-IS)**:
|
| 262 |
+
- 既存コードやドキュメントから現在の実装状況を把握
|
| 263 |
+
- 現在のシステムで何ができているかを整理
|
| 264 |
+
- 課題や改善点を抽出
|
| 265 |
+
- 技術的な制限や非効率な部分を特定
|
| 266 |
+
|
| 267 |
+
3. **目標設定(TO-BE)**:
|
| 268 |
+
- タスクで実現したい理想的な状態を定義
|
| 269 |
+
- AS-ISの課題をどのように解決するかを明確化
|
| 270 |
+
- 新機能の追加や既存機能の改善点を列挙
|
| 271 |
+
- 期待される効果を具体的に記述
|
| 272 |
+
|
| 273 |
+
4. **要件定義書作成(サンプルと同じ順序で)**:
|
| 274 |
+
- **タイトル**: `# {機能名} 要件定義書`
|
| 275 |
+
- **概要**: 機能の目的と価値を2-3文で
|
| 276 |
+
- **AS-IS(現状)**: 現在の実装状況と課題
|
| 277 |
+
- **TO-BE(目標状態)**: 実現したい姿と期待される改善
|
| 278 |
+
- **ビジネス価値**: 問題・解決策・期待効果
|
| 279 |
+
- **スコープ**: 含まれるもの・含まれないもの
|
| 280 |
+
- **ユーザーストーリー**: Given-When-Then形式の受け入れ基準
|
| 281 |
+
- **非機能要件**: パフォーマンス・セキュリティ・可用性
|
| 282 |
+
- **技術的制約**: 既存システムとの整合性
|
| 283 |
+
- **依存関係**: 他機能・外部システムとの連携
|
| 284 |
+
- **リスクと対策**: 表形式でリスク分析
|
| 285 |
+
- **成功指標**: 測定可能な目標
|
| 286 |
+
- **タイムライン**: フェーズ分けした実装計画
|
| 287 |
+
|
| 288 |
+
## 言語
|
| 289 |
+
技術的および非技術的なステークホルダーの両方が理解できる、明確でプロフェッショナルな日本語で常に要件を記述してください。
|
| 290 |
+
|
| 291 |
+
留意事項:あなたの要件により、チームはストーリーごとに機能を開発し、次に進む前に各受け入れ基準をテストできます。この段階的なアプローチはリスクを軽減し、継続的な価値提供を保証します。
|
.claude/agents/spec-tasks-generator.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: spec-tasks-generator
|
| 3 |
+
description: 仕様フォルダ内の要件定義書と設計書に基づいて包括的なタスク一覧(tasks.md)を生成する必要がある場合にこのエージェントを使用します。要件定義、設計ドキュメント、その他の関連ファイルを分析し、明確な依存関係と並列実行フェーズを持つATDD重視のタスク分解を作成します。例:\n\n<example>\nContext: ユーザーが新しい機能仕様のタスク一覧を生成したい場合\nuser: "subscription-managementの仕様書フォルダから、タスク一覧を生成して"\nassistant: "spec-tasks-generatorエージェントを使用して、仕様書を分析し包括的なタスク一覧を作成します"\n<commentary>\nユーザーが仕様書からタスク一覧を生成したいので、spec-tasks-generatorエージェントを使用します。\n</commentary>\n</example>\n\n<example>\nContext: ユーザーが要件定義と設計書を作成し、実装タスクが必要な場合\nuser: "要件定義と設計書が完成したので、実装タスクを洗い出して"\nassistant: "spec-tasks-generatorエージェントを使用して、要件と設計書に基づいた詳細なタスク分解を作成します"\n<commentary>\nユーザーが既存のドキュメントからタスク分解を必要としているので、spec-tasks-generatorエージェントを使用します。\n</commentary>\n</example>
|
| 4 |
+
model: sonnet
|
| 5 |
+
color: yellow
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
あなたはATDD(受け入れテスト駆動開発)メソドロジーに精通したタスク分解の専門家です。要件定義、設計ドキュメント、関連仕様を分析し、効率的な並列開発を可能にする包括的で適切に構造化されたタスク一覧を生成します。
|
| 9 |
+
|
| 10 |
+
## あなたの中核的責務
|
| 11 |
+
|
| 12 |
+
## タスク管理
|
| 13 |
+
TodoWriteツールを使用して詳細な進捗を可視化します:
|
| 14 |
+
- ドキュメント読み込み、要件分析、タスク分解、依存関係定義の各ステップをタスクとして登録
|
| 15 |
+
- 現在作業中のタスクは必ず「in_progress」状態に更新
|
| 16 |
+
- 完了したタスクは即座に「completed」状態に更新
|
| 17 |
+
- ユーザーが進捗を把握できるよう、各タスクには明確な説明を記載
|
| 18 |
+
|
| 19 |
+
### 重要:ドキュメント読み込みプロセス
|
| 20 |
+
**必ず最初に、同じspecsディレクトリ内の以下のファイルを読み込んでください:**
|
| 21 |
+
1. `requirements.md` - 要件定義書(必須)
|
| 22 |
+
2. `design.md` - 設計書(必須)
|
| 23 |
+
3. その他のファイル - 存在する場合は全て読み込む
|
| 24 |
+
|
| 25 |
+
これらのドキュメントの内容を完全に理解してから、それを実行するためのタスク一覧を生成します。
|
| 26 |
+
|
| 27 |
+
### 1. **ドキュメント分析**:
|
| 28 |
+
仕様フォルダ内のすべてのファイルを徹底的に分析:
|
| 29 |
+
- 要件定義ドキュメント(requirements.md)から全ユーザーストーリーと受け入れ基準を抽出
|
| 30 |
+
- 設計ドキュメント(design.md)から技術設計、API仕様、データベース設計を抽出
|
| 31 |
+
- 同じディレクトリ内のその他の関連ファイルから補足情報を収集
|
| 32 |
+
- 抽出した情報を基にタスクを生成
|
| 33 |
+
|
| 34 |
+
2. **タスク分解**: ATDD原則に従ったタスクグループの作成:
|
| 35 |
+
- ユーザーストーリーや機能単位でタスクをグループ化
|
| 36 |
+
- 各タスクに明確な完了条件を設定
|
| 37 |
+
- テスト作成を明示的なタスクとして含める
|
| 38 |
+
- タスクが原子的で独立して検証可能であることを保証
|
| 39 |
+
|
| 40 |
+
3. **依存関係管理**: タスク依存関係の明確な定義:
|
| 41 |
+
- 並列実行可能なタスクを識別
|
| 42 |
+
- 順次依存関係を明示的にマーク
|
| 43 |
+
- 最大並列化のための最適化
|
| 44 |
+
|
| 45 |
+
4. **フェーズ構成**: 実行フェーズへのタスクの構造化:
|
| 46 |
+
- 並列実行可能なタスクグループは同じメジャーフェーズ番号で異なるマイナー番号を共有(例:フェーズ 1-1、フェーズ 1-2)
|
| 47 |
+
- 順次フェーズは異なるメジャー番号を使用(例:フェーズ 2はすべてのフェーズ 1-x完了を必要)
|
| 48 |
+
- フェーズ依存関係を明確に示す
|
| 49 |
+
|
| 50 |
+
## 参照ドキュメント
|
| 51 |
+
|
| 52 |
+
以下のサンプルタスク一覧を参考にしてください:
|
| 53 |
+
`/docs/steering/example/specs/tasks.md`
|
| 54 |
+
|
| 55 |
+
このサンプルにはタスク一覧の標準構造、フェーズ構成、タスク記述形式が含まれています。
|
| 56 |
+
|
| 57 |
+
## 出力形式
|
| 58 |
+
|
| 59 |
+
以下の構造でtasks.mdファイルを生成:
|
| 60 |
+
|
| 61 |
+
```markdown
|
| 62 |
+
# 実装計画
|
| 63 |
+
|
| 64 |
+
## フェーズ1-1: [基盤構築名](並列実行可能)
|
| 65 |
+
|
| 66 |
+
### 要件[番号]グループ: [機能名]
|
| 67 |
+
|
| 68 |
+
- [ ] [タスク番号] [タスク名]
|
| 69 |
+
- [タスクの詳細説明]
|
| 70 |
+
- [具体的な実装内容]
|
| 71 |
+
- _要件: [要件番号]_
|
| 72 |
+
- _依存関係: [依存するタスク番号 or なし]_
|
| 73 |
+
- _完了条件: [テスト条件]が通ること_
|
| 74 |
+
- **対応設計:** design.md「[セクション名]」[詳細箇所]
|
| 75 |
+
|
| 76 |
+
- [ ] [タスク番号] 要件[番号]シナリオテスト作成・実行
|
| 77 |
+
- [テストシナリオの説明]
|
| 78 |
+
- [検証する項目]
|
| 79 |
+
- _要件: Story [番号]_
|
| 80 |
+
- _依存関係: [実装タスク番号]_
|
| 81 |
+
- _完了条件: Story [番号]の受け入れ基準[X.X-X.X]を満たすE2Eテストが通ること_
|
| 82 |
+
- **対応設計:** design.md「テスト戦略」[テストレベル]セクション
|
| 83 |
+
|
| 84 |
+
## フェーズ1-2: [並列実行可能な別基盤](並列実行可能)
|
| 85 |
+
|
| 86 |
+
[同様の構造で記載]
|
| 87 |
+
|
| 88 |
+
## フェーズ1完了確認
|
| 89 |
+
|
| 90 |
+
### フェーズ1全体の完了条件
|
| 91 |
+
- [ ] 1.X.1 フェーズ1完了条件確認
|
| 92 |
+
- フェーズ1-1: [サブフェーズ名]の全タスク完了
|
| 93 |
+
- フェーズ1-2: [サブフェーズ名]の全タスク完了
|
| 94 |
+
- フェーズ1-N: [サブフェーズ名]の全タスク完了
|
| 95 |
+
- 全シナリオテストの成功
|
| 96 |
+
- _依存関係: [各サブフェーズの最終タスク番号]_
|
| 97 |
+
- _完了条件: フェーズ1の全サブフェーズが完了していること_
|
| 98 |
+
- **次フェーズ移行基準:**
|
| 99 |
+
- [具体的な確認項目1]
|
| 100 |
+
- [具体的な確認項目2]
|
| 101 |
+
- [具体的な確認項目3]
|
| 102 |
+
|
| 103 |
+
## フェーズ2: [次段階の機能](フェーズ1完了後)
|
| 104 |
+
|
| 105 |
+
### 要件[番号]グループ: [機能名]
|
| 106 |
+
|
| 107 |
+
- [ ] [タスク番号] [タスク名]
|
| 108 |
+
- [タスクの詳細説明]
|
| 109 |
+
- _要件: [要件番号]_
|
| 110 |
+
- _依存関係: フェーズ1-1, 1-2完了_
|
| 111 |
+
- _完了条件: [テスト条件]が通ること_
|
| 112 |
+
- **対応設計:** design.md「[セクション名]」
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
## タスク記述のルール
|
| 116 |
+
|
| 117 |
+
### 各タスクに必須の要素
|
| 118 |
+
1. **タスク番号**: フェーズ番号.グループ番号.タスク順番(例:1.1.1)
|
| 119 |
+
2. **タスク内容**:
|
| 120 |
+
- 具体的な実装内容を箇条書きで記載
|
| 121 |
+
- 技術的な詳細を含める
|
| 122 |
+
3. **メタデータ**(イタリック体で記載):
|
| 123 |
+
- `_要件: [番号]_` - 対応する要件番号
|
| 124 |
+
- `_依存関係: [タスク番号 or なし]_` - 依存するタスク
|
| 125 |
+
- `_完了条件: [条件]が通ること_` - テスト可能な完了条件
|
| 126 |
+
- **重要**: シナリオテストタスクでは、requirements.mdの「Story X: 受け入れ基準 X.X-X.X」のように具体的な受け入れ基準番号を明記すること
|
| 127 |
+
4. **対応設計参照**(太字で記載):
|
| 128 |
+
- `**対応設計:** design.md「[セクション名]」[詳細]`
|
| 129 |
+
|
| 130 |
+
### フェーズ構成の原則
|
| 131 |
+
1. **フェーズ1-x**: 基盤構築(データベース、API基盤、UI基盤など)
|
| 132 |
+
- 相互に依存しない基盤は並列実行可能
|
| 133 |
+
- 各フェーズは独立したグループを含む
|
| 134 |
+
2. **フェーズ完了確認**: 各メジャーフェーズ(1, 2, 3...)の最後に必須
|
| 135 |
+
- 全サブフェーズの完了を確認するタスクを配置
|
| 136 |
+
- 次フェーズへの移行基準を明確に記載
|
| 137 |
+
- 依存関係として全サブフェーズの最終タスクを参照
|
| 138 |
+
3. **フェーズ2以降**: 機能実装
|
| 139 |
+
- 基盤構築完了を前提とした実装
|
| 140 |
+
- ユーザーストーリー単位でグループ化
|
| 141 |
+
|
| 142 |
+
### シナリオテストの配置
|
| 143 |
+
- 各要件グループの最後にシナリオテストタスクを配置
|
| 144 |
+
- **完了条件には必ず具体的な受け入れ基準番号を記載**
|
| 145 |
+
- 例: `Story 1の受け入れ基準1.1-1.3を満たすテストが通ること`
|
| 146 |
+
- requirements.mdのユーザーストーリーから受け入れ基準番号を正確に引用
|
| 147 |
+
- 複数の受け入れ基準がある場合は範囲で指定(例: 1.1-1.3)
|
| 148 |
+
- 特定の基準のみの場合は個別指定(例: 1.1(メール送信))
|
| 149 |
+
- E2Eテストとして実装
|
| 150 |
+
|
| 151 |
+
## 分析プロセス
|
| 152 |
+
|
| 153 |
+
1. **初期スキャン(必須)**:
|
| 154 |
+
- 必ず仕様ディレクトリ内のrequirements.mdを最初に読み込む
|
| 155 |
+
- 次にdesign.mdを読み込んで技術設計を抽出
|
| 156 |
+
- その他の関連ファイルがあれば全て読み込む
|
| 157 |
+
- **重要**: これらのファイルの内容を基にタスクを生成するため、読み込みは必須
|
| 158 |
+
|
| 159 |
+
2. **要件マッピング**:
|
| 160 |
+
- requirements.mdのユーザーストーリーを識別
|
| 161 |
+
- 各ストーリーの受け入れ基準を抽出し、番号を正確に記録
|
| 162 |
+
- 受け入れ基準の番号体系を理解(例: Story 1の基準1.1, 1.2, 1.3)
|
| 163 |
+
- 各基準の内容(Given-When-Then)を把握
|
| 164 |
+
- design.mdの対応するセクションをマッピング
|
| 165 |
+
|
| 166 |
+
3. **タスク生成**:
|
| 167 |
+
- 設計書のシーケンス図、API仕様、データベース設計を基にタスクを作成
|
| 168 |
+
- 各タスクをdesign.mdの該当セクションと紐付け
|
| 169 |
+
- テストタスクを各グループの最後に配置
|
| 170 |
+
|
| 171 |
+
4. **依存関係の最適化**:
|
| 172 |
+
- データベース、API、UI層を並列化可能に分離
|
| 173 |
+
- 機能実装は基盤完了後に配置
|
| 174 |
+
|
| 175 |
+
## 品質チェック
|
| 176 |
+
|
| 177 |
+
タスク一覧を最終化する前に以下を確認:
|
| 178 |
+
- すべてのユーザーストーリーに対応するタスクグループがある
|
| 179 |
+
- 各タスクがdesign.mdの���クションを参照している
|
| 180 |
+
- シナリオテストが各グループに含まれている
|
| 181 |
+
- **フェーズ完了確認タスクが各メジャーフェーズの最後に配置されている**
|
| 182 |
+
- **次フェーズ移行基準が明確に記載されている**
|
| 183 |
+
- 依存関係が明確で循環していない
|
| 184 |
+
- 並列実行の機会が最大化されている
|
| 185 |
+
- 完了条件がテスト可能である
|
| 186 |
+
- タスク粒度が1〜4時間程度である
|
| 187 |
+
|
| 188 |
+
## 特別な考慮事項
|
| 189 |
+
|
| 190 |
+
- 要件が不明確な場合は、調査タスクを作成
|
| 191 |
+
- データベースマイグレーションは必ず最初のフェーズに配置
|
| 192 |
+
- Stripe統合などの外部サービス連携は専用フェーズに分離
|
| 193 |
+
- セキュリティとパフォーマンスのテストを適切に配置
|
| 194 |
+
- ドキュメント更新タスクを最終フェーズに含める
|
| 195 |
+
|
| 196 |
+
タスクの説明は常に日本語で記述し、技術用語(API、Database、Stripe等)は英語のまま保持してください。
|
.claude/commands/spec-create.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
description: "タスクの仕様書(requirements.md、design.md、tasks.md)を段階的に作成・修正するワークフローを実行します。ARGUMENTS: タスク内容の説明またはAsanaタスクURL(必須)、既存仕様書のパス(オプション)"
|
| 3 |
+
allowed-tools: Task, Read, Write, Edit, MultiEdit, Bash, Grep, Glob, TodoRead, TodoWrite, mcp__asana__*, mcp__figma_dev_mode__*
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# タスク仕様書作成コマンド
|
| 7 |
+
|
| 8 |
+
## あなたの役割
|
| 9 |
+
プロダクト開発のシニアテクニカルアーキテクト兼シニアプロダクトエンジニアとして、ATDD(受け入れテスト駆動開発)に基づく仕様書を段階的に作成します。
|
| 10 |
+
|
| 11 |
+
## タスク管理
|
| 12 |
+
TodoWriteツールを使用して全体の進捗を可視化し、ユーザーに現在の状況を明確に伝えます:
|
| 13 |
+
- 各仕様書作成フェーズ(requirements.md、design.md、tasks.md)をトップレベルタスクとして管理
|
| 14 |
+
- エージェント起動前にタスクを「in_progress」に更新
|
| 15 |
+
- エージェント完了後に「completed」に更新
|
| 16 |
+
- ユーザー承認待ちの状態も明示的に表示
|
| 17 |
+
|
| 18 |
+
## 実行手順
|
| 19 |
+
|
| 20 |
+
### 1. 外部リソースの確認
|
| 21 |
+
|
| 22 |
+
**AsanaタスクURL**の場合:
|
| 23 |
+
- AsanaMCPでタスク情報を取得(タイトル、説明、カスタムフィールド)
|
| 24 |
+
- タスクIDから適切なディレクトリ名を生成
|
| 25 |
+
|
| 26 |
+
**FigmaURL**が含まれる場合:
|
| 27 |
+
- FigmaDevModeMCPでデザイン分析
|
| 28 |
+
- UI要件、コンポーネント仕様、デザイントークンを抽出
|
| 29 |
+
|
| 30 |
+
### 2. 作業ディレクトリの決定
|
| 31 |
+
- パス指定あり → 指定ディレクトリを使用
|
| 32 |
+
- パス指定なし → `/docs/specs/tasks/{domain}/{YYYYMMDD}-{domain}-{feature}/` で自動作成
|
| 33 |
+
|
| 34 |
+
### 3. 段階的仕様書作成
|
| 35 |
+
各段階でユーザー承認を得てから次へ進行:
|
| 36 |
+
|
| 37 |
+
1. **requirements.md** (要件定義書)
|
| 38 |
+
- spec-requirements-generatorエージェントで作成
|
| 39 |
+
- エージェント内で既存コードの分析を実施
|
| 40 |
+
- ATDD形式のユーザーストーリーと受け入れ基準
|
| 41 |
+
|
| 42 |
+
2. **design.md** (設計書)
|
| 43 |
+
- spec-design-generatorエージェントで作成
|
| 44 |
+
- エージェント内で既存アーキテクチャの調査を実施
|
| 45 |
+
- 技術アーキテクチャとデータモデル
|
| 46 |
+
|
| 47 |
+
3. **tasks.md** (タスク一覧)
|
| 48 |
+
- spec-tasks-generatorエージェントで作成
|
| 49 |
+
- エージェント内で実装の影響範囲を分析
|
| 50 |
+
- 実装タスクの分解と依存関係
|
| 51 |
+
|
| 52 |
+
### 4. 既存ファイル処理
|
| 53 |
+
- 既存ファイルは内容確認後に次段階へ進行
|
| 54 |
+
- 修正指示がある場合のみ該当エージェントで再生成
|
| 55 |
+
|
| 56 |
+
### 5. 成果物の構成
|
| 57 |
+
/docs/specs/tasks/
|
| 58 |
+
└── {domain}/
|
| 59 |
+
└── {YYYYMMDD}-{domain}-{feature}/
|
| 60 |
+
├── requirements.md # 要件定義書(ATDD形式)
|
| 61 |
+
├── design.md # 設計書(技術詳細)
|
| 62 |
+
└── tasks.md # タスク一覧(実装手順)
|
| 63 |
+
|
| 64 |
+
## 重要な原則
|
| 65 |
+
- 段階的開発:各フェーズの承認を必須
|
| 66 |
+
|
| 67 |
+
実行を開始します...
|
.claude/commands/start-dev.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
description: "ローカル開発環境を起動します。環境構築済みの場合に使用"
|
| 3 |
+
allowed-tools: Bash
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# ローカル開発環境起動コマンド
|
| 7 |
+
|
| 8 |
+
## コマンドの目的
|
| 9 |
+
|
| 10 |
+
既に環境構築が完了している環境でローカル開発環境を起動します。
|
| 11 |
+
|
| 12 |
+
## 実行手順
|
| 13 |
+
|
| 14 |
+
### 1. Docker サービス起動
|
| 15 |
+
```bash
|
| 16 |
+
echo "=== Docker サービス起動 ==="
|
| 17 |
+
docker-compose up -d
|
| 18 |
+
echo "✅ Docker起動完了"
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
### 2. 作業ディレクトリ確認と依存関係インストール
|
| 22 |
+
```bash
|
| 23 |
+
echo "=== 作業ディレクトリ確認 ==="
|
| 24 |
+
pwd
|
| 25 |
+
echo "✅ 現在のディレクトリ: $(pwd)"
|
| 26 |
+
|
| 27 |
+
echo "=== アプリケーションディレクトリ移動 ==="
|
| 28 |
+
cd tca-member-app
|
| 29 |
+
echo "✅ 現在のディレクトリ: $(pwd)"
|
| 30 |
+
|
| 31 |
+
echo "=== 依存関係インストール ==="
|
| 32 |
+
npm install
|
| 33 |
+
echo "✅ npm install完了"
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
### 3. 既存プロセス終了とログクリア
|
| 37 |
+
```bash
|
| 38 |
+
echo "=== 既存プロセス終了とログクリア ==="
|
| 39 |
+
echo "=== アプリケーションディレクトリ移動 ==="
|
| 40 |
+
cd tca-member-app
|
| 41 |
+
echo "" > dev.log
|
| 42 |
+
echo "既存のNext.jsプロセスを終了中..."
|
| 43 |
+
pkill -f "npm run dev" 2>/dev/null || true
|
| 44 |
+
pkill -f "next-server" 2>/dev/null || true
|
| 45 |
+
pkill -f "next dev" 2>/dev/null || true
|
| 46 |
+
sleep 2
|
| 47 |
+
echo "✅ プロセス終了とログクリア完了"
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
### 4. Next.js アプリケーション起動
|
| 51 |
+
```bash
|
| 52 |
+
echo "=== Next.js サーバー起動 ==="
|
| 53 |
+
cd tca-member-app
|
| 54 |
+
npm run dev > dev.log 2>&1 &
|
| 55 |
+
echo "✅ Next.jsサーバー起動中..."
|
| 56 |
+
echo "起動ログ: tail -f tca-member-app/dev.log で確認可能"
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
### 5. 起動確認
|
| 60 |
+
```bash
|
| 61 |
+
echo "=== 起動確認 ==="
|
| 62 |
+
sleep 15
|
| 63 |
+
if curl -s http://localhost:3000 > /dev/null 2>&1; then
|
| 64 |
+
echo "✅ Next.js App: 起動完了 (http://localhost:3000)"
|
| 65 |
+
else
|
| 66 |
+
echo "⏳ サーバーはまだ起動中です"
|
| 67 |
+
echo "ログを確認してください: tail -f tca-member-app/dev.log"
|
| 68 |
+
fi
|
| 69 |
+
|
| 70 |
+
echo ""
|
| 71 |
+
echo "📋 利用可能なサービス:"
|
| 72 |
+
echo " • PostgreSQL: localhost:25432"
|
| 73 |
+
echo " • Mailpit: http://localhost:8025"
|
| 74 |
+
echo " • Next.js App: http://localhost:3000"
|
| 75 |
+
echo ""
|
| 76 |
+
```
|
.claude/settings.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1cf71fbce3034327edec12c205cebef0508ab856fc6fa3e71c6c43e0545819e8
|
| 3 |
+
size 1496
|
.dockerignore
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dependencies
|
| 2 |
+
node_modules
|
| 3 |
+
npm-debug.log*
|
| 4 |
+
yarn-debug.log*
|
| 5 |
+
yarn-error.log*
|
| 6 |
+
|
| 7 |
+
# Next.js
|
| 8 |
+
.next/
|
| 9 |
+
out/
|
| 10 |
+
|
| 11 |
+
# Production
|
| 12 |
+
build
|
| 13 |
+
dist
|
| 14 |
+
|
| 15 |
+
# Environment variables
|
| 16 |
+
.env
|
| 17 |
+
.env.local
|
| 18 |
+
.env.development.local
|
| 19 |
+
.env.test.local
|
| 20 |
+
.env.production.local
|
| 21 |
+
|
| 22 |
+
# Logs
|
| 23 |
+
logs
|
| 24 |
+
*.log
|
| 25 |
+
dev.log
|
| 26 |
+
|
| 27 |
+
# Runtime data
|
| 28 |
+
pids
|
| 29 |
+
*.pid
|
| 30 |
+
*.seed
|
| 31 |
+
*.pid.lock
|
| 32 |
+
|
| 33 |
+
# Coverage directory used by tools like istanbul
|
| 34 |
+
coverage/
|
| 35 |
+
|
| 36 |
+
# nyc test coverage
|
| 37 |
+
.nyc_output
|
| 38 |
+
|
| 39 |
+
# Dependency directories
|
| 40 |
+
jspm_packages/
|
| 41 |
+
|
| 42 |
+
# Optional npm cache directory
|
| 43 |
+
.npm
|
| 44 |
+
|
| 45 |
+
# Optional REPL history
|
| 46 |
+
.node_repl_history
|
| 47 |
+
|
| 48 |
+
# Output of 'npm pack'
|
| 49 |
+
*.tgz
|
| 50 |
+
|
| 51 |
+
# Yarn Integrity file
|
| 52 |
+
.yarn-integrity
|
| 53 |
+
|
| 54 |
+
# dotenv environment variables file
|
| 55 |
+
.env
|
| 56 |
+
|
| 57 |
+
# IDE
|
| 58 |
+
.vscode/
|
| 59 |
+
.idea/
|
| 60 |
+
*.swp
|
| 61 |
+
*.swo
|
| 62 |
+
|
| 63 |
+
# OS
|
| 64 |
+
.DS_Store
|
| 65 |
+
Thumbs.db
|
| 66 |
+
|
| 67 |
+
# Git
|
| 68 |
+
.git
|
| 69 |
+
.gitignore
|
| 70 |
+
|
| 71 |
+
# Testing
|
| 72 |
+
coverage/
|
| 73 |
+
.nyc_output/
|
| 74 |
+
test-results/
|
| 75 |
+
playwright-report/
|
| 76 |
+
|
| 77 |
+
# Documentation
|
| 78 |
+
docs/
|
| 79 |
+
*.md
|
| 80 |
+
!README.md
|
| 81 |
+
|
| 82 |
+
# Development files
|
| 83 |
+
dev.log
|
| 84 |
+
*.log
|
| 85 |
+
|
| 86 |
+
# Cache
|
| 87 |
+
.cache/
|
| 88 |
+
.temp/
|
| 89 |
+
.tmp/
|
.env.example
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Gradio API Token (HuggingFace)
|
| 2 |
+
GRADIO_TOKEN=hf_your_token_here
|
| 3 |
+
|
| 4 |
+
# OpenAI API Key (Optional - for local development)
|
| 5 |
+
OPENAI_API_KEY=sk-your-api-key-here
|
| 6 |
+
|
| 7 |
+
# Environment
|
| 8 |
+
NODE_ENV=development
|
| 9 |
+
|
| 10 |
+
# Server Port
|
| 11 |
+
PORT=3200
|
| 12 |
+
|
| 13 |
+
# Feature Toggles (true/false)
|
| 14 |
+
# 開発・検証環境用のボタンを表示するかどうか
|
| 15 |
+
NEXT_PUBLIC_SHOW_DEV_BUTTONS=true
|
.gitattributes
CHANGED
|
@@ -33,3 +33,13 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
*.json filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
public/dummy/get_check_url.json filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
public/dummy/*.png filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
*.svg filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
*.xlsx filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
*.pptx filter=lfs diff=lfs merge=lfs -text
|
.github/DEPLOYMENT.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hugging Face Deployment Workflows
|
| 2 |
+
|
| 3 |
+
This repository includes two GitHub workflows for deploying your Next.js application to separate Hugging Face Spaces:
|
| 4 |
+
|
| 5 |
+
1. **`deploy-huggingface.yml`** - Production deployment (main branch)
|
| 6 |
+
2. **`deploy-dev.yml`** - Development deployment (dev branch)
|
| 7 |
+
|
| 8 |
+
## 🚀 Quick Setup
|
| 9 |
+
|
| 10 |
+
### 1. Create Required Secrets
|
| 11 |
+
|
| 12 |
+
In your GitHub repository, go to **Settings** → **Secrets and variables** → **Actions** and add the following secrets:
|
| 13 |
+
|
| 14 |
+
| Secret Name | Description | Example |
|
| 15 |
+
| -------------------- | ------------------------------------------- | ------------------------------- |
|
| 16 |
+
| `HF_TOKEN` | Your Hugging Face access token | `hf_xxxxxxxxxxxxxxxxxxxxxxxxxx` |
|
| 17 |
+
| `HF_USERNAME` | Your Hugging Face username | `your-username` |
|
| 18 |
+
| `HF_SPACE_NAME_PROD` | Name of your production Hugging Face Space | `my-awesome-app` |
|
| 19 |
+
| `HF_SPACE_NAME_DEV` | Name of your development Hugging Face Space | `my-awesome-app-dev` |
|
| 20 |
+
|
| 21 |
+
### 2. Get Your Hugging Face Token
|
| 22 |
+
|
| 23 |
+
1. Go to [Hugging Face Settings](https://huggingface.co/settings/tokens)
|
| 24 |
+
2. Create a new token with **Write** permissions
|
| 25 |
+
3. Copy the token and add it as `HF_TOKEN` secret
|
| 26 |
+
|
| 27 |
+
## 📋 Workflow Overview
|
| 28 |
+
|
| 29 |
+
### Production Workflow (`deploy-huggingface.yml`)
|
| 30 |
+
|
| 31 |
+
**Triggers:**
|
| 32 |
+
|
| 33 |
+
- Push to `main` branch
|
| 34 |
+
- Merged pull requests to `main`
|
| 35 |
+
|
| 36 |
+
**Deploys to:** Space defined in `HF_SPACE_NAME_PROD`
|
| 37 |
+
|
| 38 |
+
**Features:**
|
| 39 |
+
|
| 40 |
+
- ✅ Stable production environment
|
| 41 |
+
- 🚀 Blue/purple theme
|
| 42 |
+
- 📊 Production-ready builds
|
| 43 |
+
|
| 44 |
+
### Development Workflow (`deploy-dev.yml`)
|
| 45 |
+
|
| 46 |
+
**Triggers:**
|
| 47 |
+
|
| 48 |
+
- Push to `dev` branch
|
| 49 |
+
- Merged pull requests to `dev`
|
| 50 |
+
|
| 51 |
+
**Deploys to:** Space defined in `HF_SPACE_NAME_DEV`
|
| 52 |
+
|
| 53 |
+
**Features:**
|
| 54 |
+
|
| 55 |
+
- 🧪 Testing environment for new features
|
| 56 |
+
- 🔶 Orange/red theme to indicate development
|
| 57 |
+
- 🚧 May contain unstable features
|
| 58 |
+
|
| 59 |
+
## 🏗️ Project Structure Requirements
|
| 60 |
+
|
| 61 |
+
Your Next.js project should have:
|
| 62 |
+
|
| 63 |
+
```
|
| 64 |
+
├── package.json # Node.js dependencies
|
| 65 |
+
├── next.config.ts # Next.js configuration
|
| 66 |
+
├── Dockerfile # Docker configuration (optional)
|
| 67 |
+
├── app/ # Next.js app directory
|
| 68 |
+
├── components/ # React components
|
| 69 |
+
├── public/ # Static assets
|
| 70 |
+
└── .github/
|
| 71 |
+
└── workflows/
|
| 72 |
+
├── deploy-huggingface.yml
|
| 73 |
+
└── deploy-huggingface-advanced.yml
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
## 🔧 Configuration
|
| 77 |
+
|
| 78 |
+
### Next.js Configuration
|
| 79 |
+
|
| 80 |
+
Ensure your `next.config.ts` includes:
|
| 81 |
+
|
| 82 |
+
```typescript
|
| 83 |
+
const nextConfig: NextConfig = {
|
| 84 |
+
output: 'standalone', // Required for Docker deployment
|
| 85 |
+
images: {
|
| 86 |
+
unoptimized: true, // Recommended for Hugging Face Spaces
|
| 87 |
+
},
|
| 88 |
+
};
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
### Docker Configuration
|
| 92 |
+
|
| 93 |
+
The workflows automatically generate an optimized Dockerfile, but you can customize it by modifying the Dockerfile creation step in the workflow files.
|
| 94 |
+
|
| 95 |
+
## 🌍 Environment Management
|
| 96 |
+
|
| 97 |
+
### Separate Environments
|
| 98 |
+
|
| 99 |
+
- **Production**:
|
| 100 |
+
|
| 101 |
+
- Triggered by pushes to `main` branch
|
| 102 |
+
- Deploys to: `HF_SPACE_NAME_PROD` (e.g., `my-app`)
|
| 103 |
+
- Theme: Blue/purple 🚀
|
| 104 |
+
- Stable, tested features only
|
| 105 |
+
|
| 106 |
+
- **Development**:
|
| 107 |
+
- Triggered by pushes to `dev` branch
|
| 108 |
+
- Deploys to: `HF_SPACE_NAME_DEV` (e.g., `my-app-dev`)
|
| 109 |
+
- Theme: Orange/red 🧪
|
| 110 |
+
- Testing new features and changes
|
| 111 |
+
|
| 112 |
+
### Benefits of Separate Spaces
|
| 113 |
+
|
| 114 |
+
- ✅ Test changes in development before production
|
| 115 |
+
- ✅ Keep production environment stable
|
| 116 |
+
- ✅ Easy to identify which environment you're viewing
|
| 117 |
+
- ✅ Independent deployment cycles
|
| 118 |
+
|
| 119 |
+
### Custom Environment Variables
|
| 120 |
+
|
| 121 |
+
Add environment variables to your Hugging Face Space:
|
| 122 |
+
|
| 123 |
+
1. Go to your Space settings
|
| 124 |
+
2. Add environment variables in the "Variables and secrets" section
|
| 125 |
+
3. Restart your Space
|
| 126 |
+
|
| 127 |
+
## 🚨 Troubleshooting
|
| 128 |
+
|
| 129 |
+
### Common Issues
|
| 130 |
+
|
| 131 |
+
1. **"Space not found" error**
|
| 132 |
+
|
| 133 |
+
- Check that `HF_USERNAME` and `HF_SPACE_NAME` are correct
|
| 134 |
+
- Ensure your Hugging Face token has write permissions
|
| 135 |
+
|
| 136 |
+
2. **Build failures**
|
| 137 |
+
|
| 138 |
+
- Check that all dependencies are in `package.json`
|
| 139 |
+
- Verify your Next.js configuration
|
| 140 |
+
|
| 141 |
+
3. **Docker build issues**
|
| 142 |
+
|
| 143 |
+
- Ensure your app builds successfully locally with `npm run build`
|
| 144 |
+
- Check for any missing environment variables
|
| 145 |
+
|
| 146 |
+
4. **Permission errors**
|
| 147 |
+
- Verify your Hugging Face token has write access
|
| 148 |
+
- Check that the token hasn't expired
|
| 149 |
+
|
| 150 |
+
### Debug Mode
|
| 151 |
+
|
| 152 |
+
To enable debug mode, add this to your workflow:
|
| 153 |
+
|
| 154 |
+
```yaml
|
| 155 |
+
env:
|
| 156 |
+
ACTIONS_STEP_DEBUG: true
|
| 157 |
+
ACTIONS_RUNNER_DEBUG: true
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
## 📊 Monitoring Deployments
|
| 161 |
+
|
| 162 |
+
### Workflow Status
|
| 163 |
+
|
| 164 |
+
- Check the **Actions** tab in your GitHub repository
|
| 165 |
+
- Each deployment shows detailed logs and status
|
| 166 |
+
- Production deployments show `[PROD]` in commit messages
|
| 167 |
+
- Development deployments show `[DEV]` in commit messages
|
| 168 |
+
|
| 169 |
+
### Hugging Face Space Status
|
| 170 |
+
|
| 171 |
+
- **Production**: `https://huggingface.co/spaces/YOUR_USERNAME/YOUR_PROD_SPACE_NAME`
|
| 172 |
+
- **Development**: `https://huggingface.co/spaces/YOUR_USERNAME/YOUR_DEV_SPACE_NAME`
|
| 173 |
+
- Check the build logs in each Space's "Logs" section
|
| 174 |
+
|
| 175 |
+
## � Typical Workflow
|
| 176 |
+
|
| 177 |
+
1. **Development**:
|
| 178 |
+
|
| 179 |
+
- Make changes on `dev` branch
|
| 180 |
+
- Push to automatically deploy to dev space
|
| 181 |
+
- Test features in development environment
|
| 182 |
+
|
| 183 |
+
2. **Production**:
|
| 184 |
+
- Create PR from `dev` to `main`
|
| 185 |
+
- Review and merge PR
|
| 186 |
+
- Automatically deploys to production space
|
| 187 |
+
|
| 188 |
+
## 📝 Customization
|
| 189 |
+
|
| 190 |
+
### Modifying the Workflow
|
| 191 |
+
|
| 192 |
+
You can customize the workflows by:
|
| 193 |
+
|
| 194 |
+
1. **Changing trigger conditions**: Modify the `on:` section
|
| 195 |
+
2. **Adding build steps**: Insert additional steps before deployment
|
| 196 |
+
3. **Custom Dockerfile**: Modify the Dockerfile generation step
|
| 197 |
+
4. **Environment variables**: Add custom environment variables
|
| 198 |
+
|
| 199 |
+
### Example Customizations
|
| 200 |
+
|
| 201 |
+
```yaml
|
| 202 |
+
# Deploy only on tags
|
| 203 |
+
on:
|
| 204 |
+
push:
|
| 205 |
+
tags:
|
| 206 |
+
- 'v*'
|
| 207 |
+
|
| 208 |
+
# Add custom build step
|
| 209 |
+
- name: Run tests
|
| 210 |
+
run: npm test
|
| 211 |
+
|
| 212 |
+
# Custom environment variables
|
| 213 |
+
env:
|
| 214 |
+
CUSTOM_VAR: ${{ secrets.CUSTOM_VAR }}
|
| 215 |
+
```
|
| 216 |
+
|
| 217 |
+
## 🆘 Support
|
| 218 |
+
|
| 219 |
+
If you encounter issues:
|
| 220 |
+
|
| 221 |
+
1. Check the GitHub Actions logs
|
| 222 |
+
2. Review the Hugging Face Space build logs
|
| 223 |
+
3. Verify all secrets are properly set
|
| 224 |
+
4. Ensure your Next.js app builds locally
|
| 225 |
+
|
| 226 |
+
For more help:
|
| 227 |
+
|
| 228 |
+
- [Hugging Face Spaces Documentation](https://huggingface.co/docs/hub/spaces)
|
| 229 |
+
- [GitHub Actions Documentation](https://docs.github.com/en/actions)
|
| 230 |
+
- [Next.js Deployment Documentation](https://nextjs.org/docs/deployment)
|
.github/actions/create-release/action.yml
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: 'Create Release with Tag'
|
| 2 |
+
description: 'Creates a Git tag and GitHub Release with categorized PR notes'
|
| 3 |
+
inputs:
|
| 4 |
+
branch:
|
| 5 |
+
description: 'Branch name (e.g., main, stg)'
|
| 6 |
+
required: true
|
| 7 |
+
tag-prefix:
|
| 8 |
+
description: 'Tag prefix (e.g., main-, stg-)'
|
| 9 |
+
required: true
|
| 10 |
+
prerelease:
|
| 11 |
+
description: 'Whether this is a pre-release (true/false)'
|
| 12 |
+
required: true
|
| 13 |
+
release-name-prefix:
|
| 14 |
+
description: 'Release name prefix (e.g., Production Release, Staging Pre-release)'
|
| 15 |
+
required: true
|
| 16 |
+
|
| 17 |
+
runs:
|
| 18 |
+
using: 'composite'
|
| 19 |
+
steps:
|
| 20 |
+
- name: Create Tag and Release
|
| 21 |
+
uses: actions/github-script@v7
|
| 22 |
+
with:
|
| 23 |
+
script: |
|
| 24 |
+
const now = new Date();
|
| 25 |
+
const year = now.getFullYear();
|
| 26 |
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
| 27 |
+
const day = String(now.getDate()).padStart(2, '0');
|
| 28 |
+
const hours = String(now.getHours()).padStart(2, '0');
|
| 29 |
+
const minutes = String(now.getMinutes()).padStart(2, '0');
|
| 30 |
+
const seconds = String(now.getSeconds()).padStart(2, '0');
|
| 31 |
+
const tagName = `${{ inputs.tag-prefix }}${year}${month}${day}_${hours}${minutes}${seconds}`;
|
| 32 |
+
|
| 33 |
+
// 前回のタグを取得
|
| 34 |
+
let previousTag = null;
|
| 35 |
+
try {
|
| 36 |
+
const tags = await github.rest.repos.listTags({
|
| 37 |
+
owner: context.repo.owner,
|
| 38 |
+
repo: context.repo.repo,
|
| 39 |
+
per_page: 100
|
| 40 |
+
});
|
| 41 |
+
previousTag = tags.data.find(t => t.name.startsWith('${{ inputs.tag-prefix }}'));
|
| 42 |
+
} catch (e) {
|
| 43 |
+
console.log('No previous tags found or error:', e.message);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// カテゴリ定義
|
| 47 |
+
const categories = {
|
| 48 |
+
'feat': { emoji: '✨', title: 'Features', order: 1 },
|
| 49 |
+
'fix': { emoji: '🐛', title: 'Bug Fixes', order: 2 },
|
| 50 |
+
'hotfix': { emoji: '🚑', title: 'Hotfixes', order: 3 },
|
| 51 |
+
'refactor': { emoji: '♻️', title: 'Refactoring', order: 4 },
|
| 52 |
+
'docs': { emoji: '📝', title: 'Documentation', order: 5 },
|
| 53 |
+
'test': { emoji: '✅', title: 'Tests', order: 6 },
|
| 54 |
+
'chore': { emoji: '🔧', title: 'Chore', order: 7 },
|
| 55 |
+
'other': { emoji: '🔄', title: 'Other Changes', order: 8 }
|
| 56 |
+
};
|
| 57 |
+
|
| 58 |
+
// リリースノート生成
|
| 59 |
+
let releaseBody = '';
|
| 60 |
+
if (previousTag) {
|
| 61 |
+
console.log(`Previous tag found: ${previousTag.name}`);
|
| 62 |
+
try {
|
| 63 |
+
// PRタイトル一覧を取得
|
| 64 |
+
const commits = await github.rest.repos.compareCommits({
|
| 65 |
+
owner: context.repo.owner,
|
| 66 |
+
repo: context.repo.repo,
|
| 67 |
+
base: previousTag.name,
|
| 68 |
+
head: '${{ inputs.branch }}'
|
| 69 |
+
});
|
| 70 |
+
|
| 71 |
+
const prNumbers = new Set();
|
| 72 |
+
for (const commit of commits.data.commits) {
|
| 73 |
+
const match = commit.commit.message.match(/#(\d+)/);
|
| 74 |
+
if (match) prNumbers.add(match[1]);
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// PRをカテゴリごとに分類
|
| 78 |
+
const categorizedPrs = {};
|
| 79 |
+
for (const num of prNumbers) {
|
| 80 |
+
try {
|
| 81 |
+
const pr = await github.rest.pulls.get({
|
| 82 |
+
owner: context.repo.owner,
|
| 83 |
+
repo: context.repo.repo,
|
| 84 |
+
pull_number: parseInt(num)
|
| 85 |
+
});
|
| 86 |
+
if (pr.data.merged_at) {
|
| 87 |
+
// プレフィックスを抽出(PRタイトル優先、次にブランチ名)
|
| 88 |
+
const titleMatch = pr.data.title.match(/^(feat|fix|hotfix|refactor|docs|test|chore):/);
|
| 89 |
+
const branchMatch = pr.data.head.ref.match(/^(feature|feat|fix|hotfix|refactor|docs|test|chore)\//);
|
| 90 |
+
|
| 91 |
+
let category = 'other';
|
| 92 |
+
if (titleMatch) {
|
| 93 |
+
category = titleMatch[1];
|
| 94 |
+
} else if (branchMatch) {
|
| 95 |
+
// feature/ → feat にマッピング
|
| 96 |
+
category = branchMatch[1] === 'feature' ? 'feat' : branchMatch[1];
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
if (!categorizedPrs[category]) {
|
| 100 |
+
categorizedPrs[category] = [];
|
| 101 |
+
}
|
| 102 |
+
categorizedPrs[category].push(`- ${pr.data.title} (#${num})`);
|
| 103 |
+
}
|
| 104 |
+
} catch (e) {
|
| 105 |
+
console.log(`Failed to get PR #${num}:`, e.message);
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// カテゴリ順にリリースノートを生成
|
| 110 |
+
const sortedCategories = Object.keys(categorizedPrs).sort((a, b) => {
|
| 111 |
+
return categories[a].order - categories[b].order;
|
| 112 |
+
});
|
| 113 |
+
|
| 114 |
+
const sections = [];
|
| 115 |
+
for (const category of sortedCategories) {
|
| 116 |
+
const cat = categories[category];
|
| 117 |
+
sections.push(`## ${cat.emoji} ${cat.title}\n${categorizedPrs[category].join('\n')}`);
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
releaseBody = sections.length > 0 ? sections.join('\n\n') : 'No PRs merged in this release';
|
| 121 |
+
} catch (e) {
|
| 122 |
+
console.log('Error generating release notes:', e.message);
|
| 123 |
+
releaseBody = 'Error generating release notes';
|
| 124 |
+
}
|
| 125 |
+
} else {
|
| 126 |
+
console.log('No previous tag found - this is the initial release');
|
| 127 |
+
releaseBody = `Initial Release - ${now.toISOString()}`;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
// タグ作成
|
| 131 |
+
try {
|
| 132 |
+
await github.rest.git.createRef({
|
| 133 |
+
owner: context.repo.owner,
|
| 134 |
+
repo: context.repo.repo,
|
| 135 |
+
ref: `refs/tags/${tagName}`,
|
| 136 |
+
sha: context.sha
|
| 137 |
+
});
|
| 138 |
+
console.log(`Tag created: ${tagName}`);
|
| 139 |
+
} catch (e) {
|
| 140 |
+
console.log('Error creating tag:', e.message);
|
| 141 |
+
throw e;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
// リリース作成
|
| 145 |
+
try {
|
| 146 |
+
await github.rest.repos.createRelease({
|
| 147 |
+
owner: context.repo.owner,
|
| 148 |
+
repo: context.repo.repo,
|
| 149 |
+
tag_name: tagName,
|
| 150 |
+
name: `${{ inputs.release-name-prefix }} ${tagName}`,
|
| 151 |
+
body: releaseBody,
|
| 152 |
+
prerelease: ${{ inputs.prerelease }}
|
| 153 |
+
});
|
| 154 |
+
console.log(`Release created: ${{ inputs.release-name-prefix }} ${tagName}`);
|
| 155 |
+
} catch (e) {
|
| 156 |
+
console.log('Error creating release:', e.message);
|
| 157 |
+
throw e;
|
| 158 |
+
}
|
.github/actions/deploy-to-hf/action.yml
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: 'Deploy to Hugging Face Space'
|
| 2 |
+
description: 'Deploys application to Hugging Face Space'
|
| 3 |
+
inputs:
|
| 4 |
+
environment:
|
| 5 |
+
description: 'Environment name (dev, test, stg, prod)'
|
| 6 |
+
required: true
|
| 7 |
+
branch:
|
| 8 |
+
description: 'Branch name'
|
| 9 |
+
required: true
|
| 10 |
+
hf-username:
|
| 11 |
+
description: 'Hugging Face username'
|
| 12 |
+
required: true
|
| 13 |
+
hf-token:
|
| 14 |
+
description: 'Hugging Face token'
|
| 15 |
+
required: true
|
| 16 |
+
hf-space-name:
|
| 17 |
+
description: 'Hugging Face Space name'
|
| 18 |
+
required: true
|
| 19 |
+
|
| 20 |
+
runs:
|
| 21 |
+
using: 'composite'
|
| 22 |
+
steps:
|
| 23 |
+
- name: Setup Python for Hugging Face CLI
|
| 24 |
+
uses: actions/setup-python@v5
|
| 25 |
+
with:
|
| 26 |
+
python-version: '3.9'
|
| 27 |
+
|
| 28 |
+
- name: Install Hugging Face Hub
|
| 29 |
+
shell: bash
|
| 30 |
+
run: |
|
| 31 |
+
pip install huggingface_hub
|
| 32 |
+
sudo apt-get update && sudo apt-get install -y rsync
|
| 33 |
+
|
| 34 |
+
- name: Validate secrets
|
| 35 |
+
shell: bash
|
| 36 |
+
run: |
|
| 37 |
+
echo "HF_USERNAME: ${{ inputs.hf-username }}"
|
| 38 |
+
echo "HF_SPACE_NAME: ${{ inputs.hf-space-name }}"
|
| 39 |
+
echo "Token length: ${#HF_TOKEN}"
|
| 40 |
+
env:
|
| 41 |
+
HF_TOKEN: ${{ inputs.hf-token }}
|
| 42 |
+
|
| 43 |
+
- name: Create Hugging Face Space README
|
| 44 |
+
shell: bash
|
| 45 |
+
env:
|
| 46 |
+
SPACE_NAME: ${{ inputs.hf-space-name }}
|
| 47 |
+
BRANCH_NAME: ${{ inputs.branch }}
|
| 48 |
+
ENVIRONMENT: ${{ inputs.environment }}
|
| 49 |
+
run: |
|
| 50 |
+
# Environment-specific settings
|
| 51 |
+
case "${ENVIRONMENT}" in
|
| 52 |
+
dev)
|
| 53 |
+
EMOJI="🧪"
|
| 54 |
+
COLOR_TO="yellow"
|
| 55 |
+
ENV_TITLE="Development"
|
| 56 |
+
ENV_DESC="🧪 **This is a development environment** - Features may be unstable"
|
| 57 |
+
;;
|
| 58 |
+
test)
|
| 59 |
+
EMOJI="🧪"
|
| 60 |
+
COLOR_TO="gray"
|
| 61 |
+
ENV_TITLE="Mock/Testing"
|
| 62 |
+
ENV_DESC="🧪 **This is the mock/testing environment** - Feature branch deployment"
|
| 63 |
+
;;
|
| 64 |
+
stg)
|
| 65 |
+
EMOJI="🧪"
|
| 66 |
+
COLOR_TO="indigo"
|
| 67 |
+
ENV_TITLE="Staging"
|
| 68 |
+
ENV_DESC="🚧 **This is a staging environment** - Pre-production testing"
|
| 69 |
+
;;
|
| 70 |
+
prod)
|
| 71 |
+
EMOJI="🚀"
|
| 72 |
+
COLOR_TO="green"
|
| 73 |
+
ENV_TITLE="Production"
|
| 74 |
+
ENV_DESC="✅ **This is the production environment** - Stable and tested features"
|
| 75 |
+
;;
|
| 76 |
+
esac
|
| 77 |
+
|
| 78 |
+
cat > README.md << EOF
|
| 79 |
+
---
|
| 80 |
+
title: ${SPACE_NAME}
|
| 81 |
+
emoji: ${EMOJI}
|
| 82 |
+
colorFrom: pink
|
| 83 |
+
colorTo: ${COLOR_TO}
|
| 84 |
+
sdk: docker
|
| 85 |
+
app_port: 7860
|
| 86 |
+
pinned: false
|
| 87 |
+
license: mit
|
| 88 |
+
---
|
| 89 |
+
|
| 90 |
+
# ${SPACE_NAME} (${ENV_TITLE})
|
| 91 |
+
|
| 92 |
+
${ENV_DESC}
|
| 93 |
+
|
| 94 |
+
This is a Next.js application deployed to Hugging Face Spaces from the \`${BRANCH_NAME}\` branch.
|
| 95 |
+
|
| 96 |
+
## Features
|
| 97 |
+
|
| 98 |
+
- Built with Next.js and React
|
| 99 |
+
- Containerized with Docker
|
| 100 |
+
- Automatic deployment from GitHub ${BRANCH_NAME} branch
|
| 101 |
+
- ${ENV_TITLE} environment
|
| 102 |
+
|
| 103 |
+
## Development
|
| 104 |
+
|
| 105 |
+
To run locally:
|
| 106 |
+
|
| 107 |
+
\`\`\`bash
|
| 108 |
+
npm install
|
| 109 |
+
npm run dev
|
| 110 |
+
\`\`\`
|
| 111 |
+
|
| 112 |
+
The application will be available at http://localhost:3200
|
| 113 |
+
|
| 114 |
+
---
|
| 115 |
+
**Branch**: ${BRANCH_NAME}
|
| 116 |
+
**Environment**: ${ENV_TITLE}
|
| 117 |
+
**Last Deploy**: $(date)
|
| 118 |
+
EOF
|
| 119 |
+
|
| 120 |
+
- name: Clone or update Hugging Face Space
|
| 121 |
+
shell: bash
|
| 122 |
+
env:
|
| 123 |
+
HF_USERNAME: ${{ inputs.hf-username }}
|
| 124 |
+
HF_TOKEN: ${{ inputs.hf-token }}
|
| 125 |
+
HF_SPACE_NAME: ${{ inputs.hf-space-name }}
|
| 126 |
+
run: |
|
| 127 |
+
# Skip LFS for initial clone to avoid errors
|
| 128 |
+
export GIT_LFS_SKIP_SMUDGE=1
|
| 129 |
+
|
| 130 |
+
# Try to clone the space with authentication
|
| 131 |
+
if git clone https://user:${HF_TOKEN}@huggingface.co/spaces/${HF_USERNAME}/${HF_SPACE_NAME} hf-space; then
|
| 132 |
+
echo "✅ Space exists, updating..."
|
| 133 |
+
cd hf-space
|
| 134 |
+
# Reset any failed LFS state
|
| 135 |
+
git lfs install --skip-smudge
|
| 136 |
+
else
|
| 137 |
+
echo "🆕 Creating new space..."
|
| 138 |
+
# Create space using Python API (more reliable)
|
| 139 |
+
python3 -c "import os; from huggingface_hub import HfApi; api = HfApi(token=os.environ['HF_TOKEN']); api.create_repo(repo_id=f\"{os.environ['HF_USERNAME']}/{os.environ['HF_SPACE_NAME']}\", repo_type='space', space_sdk='docker')" 2>&1 || echo "Space creation failed or already exists"
|
| 140 |
+
sleep 5
|
| 141 |
+
git clone https://user:${HF_TOKEN}@huggingface.co/spaces/${HF_USERNAME}/${HF_SPACE_NAME} hf-space
|
| 142 |
+
cd hf-space
|
| 143 |
+
fi
|
| 144 |
+
|
| 145 |
+
- name: Copy files to Hugging Face Space
|
| 146 |
+
shell: bash
|
| 147 |
+
run: |
|
| 148 |
+
cd hf-space
|
| 149 |
+
|
| 150 |
+
# Remove existing files except .git
|
| 151 |
+
find . -name '.git' -prune -o -type f -exec rm -f {} \;
|
| 152 |
+
find . -name '.git' -prune -o -type d -empty -exec rmdir {} \; 2>/dev/null || true
|
| 153 |
+
|
| 154 |
+
# Copy project files (excluding .git and other unnecessary files)
|
| 155 |
+
rsync -av --exclude='.git' --exclude='hf-space' --exclude='node_modules' --exclude='.next' ../ ./
|
| 156 |
+
|
| 157 |
+
# Copy the built .next directory if it exists
|
| 158 |
+
if [ -d "../.next" ]; then
|
| 159 |
+
cp -r ../.next ./
|
| 160 |
+
fi
|
| 161 |
+
|
| 162 |
+
- name: Configure git in Hugging Face Space
|
| 163 |
+
shell: bash
|
| 164 |
+
run: |
|
| 165 |
+
cd hf-space
|
| 166 |
+
git config user.name "GitHub Actions"
|
| 167 |
+
git config user.email "actions@github.com"
|
| 168 |
+
|
| 169 |
+
- name: Commit and push to Hugging Face Space
|
| 170 |
+
shell: bash
|
| 171 |
+
env:
|
| 172 |
+
ENVIRONMENT: ${{ inputs.environment }}
|
| 173 |
+
run: |
|
| 174 |
+
cd hf-space
|
| 175 |
+
git add .
|
| 176 |
+
|
| 177 |
+
# Check if there are changes to commit
|
| 178 |
+
if git diff --staged --quiet; then
|
| 179 |
+
echo "No changes to commit"
|
| 180 |
+
else
|
| 181 |
+
git commit -m "Deploy from GitHub Actions [${ENVIRONMENT}] - $(date '+%Y-%m-%d %H:%M:%S')"
|
| 182 |
+
git push origin main
|
| 183 |
+
fi
|
| 184 |
+
|
| 185 |
+
- name: Deployment status
|
| 186 |
+
shell: bash
|
| 187 |
+
env:
|
| 188 |
+
ENVIRONMENT: ${{ inputs.environment }}
|
| 189 |
+
HF_USERNAME: ${{ inputs.hf-username }}
|
| 190 |
+
HF_SPACE_NAME: ${{ inputs.hf-space-name }}
|
| 191 |
+
run: |
|
| 192 |
+
case "${ENVIRONMENT}" in
|
| 193 |
+
dev)
|
| 194 |
+
echo "🧪 Development deployment completed!"
|
| 195 |
+
;;
|
| 196 |
+
test)
|
| 197 |
+
echo "🧪 Mock/Testing deployment completed!"
|
| 198 |
+
;;
|
| 199 |
+
stg)
|
| 200 |
+
echo "🚧 Staging deployment completed!"
|
| 201 |
+
;;
|
| 202 |
+
prod)
|
| 203 |
+
echo "🚀 Production deployment completed!"
|
| 204 |
+
;;
|
| 205 |
+
esac
|
| 206 |
+
echo "Your app should be available at: https://huggingface.co/spaces/${HF_USERNAME}/${HF_SPACE_NAME}"
|
.github/workflows/deploy-dev.yml
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Deploy to Hugging Face - Development
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches:
|
| 6 |
+
- dev
|
| 7 |
+
|
| 8 |
+
env:
|
| 9 |
+
HF_USERNAME: ${{ secrets.HF_USERNAME }}
|
| 10 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 11 |
+
HF_SPACE_NAME: ${{ secrets.HF_SPACE_NAME_DEV }}
|
| 12 |
+
|
| 13 |
+
jobs:
|
| 14 |
+
deploy:
|
| 15 |
+
runs-on: ubuntu-latest
|
| 16 |
+
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.merged == true)
|
| 17 |
+
|
| 18 |
+
steps:
|
| 19 |
+
- name: Checkout repository
|
| 20 |
+
uses: actions/checkout@v4
|
| 21 |
+
with:
|
| 22 |
+
lfs: true
|
| 23 |
+
|
| 24 |
+
- name: Setup Node.js
|
| 25 |
+
uses: actions/setup-node@v4
|
| 26 |
+
with:
|
| 27 |
+
node-version: '18'
|
| 28 |
+
cache: 'npm'
|
| 29 |
+
|
| 30 |
+
- name: Install dependencies
|
| 31 |
+
run: npm ci
|
| 32 |
+
|
| 33 |
+
- name: Build application
|
| 34 |
+
run: npm run build
|
| 35 |
+
|
| 36 |
+
- name: Deploy to Hugging Face Space
|
| 37 |
+
uses: ./.github/actions/deploy-to-hf
|
| 38 |
+
with:
|
| 39 |
+
environment: dev
|
| 40 |
+
branch: ${{ github.ref_name }}
|
| 41 |
+
hf-username: ${{ env.HF_USERNAME }}
|
| 42 |
+
hf-token: ${{ env.HF_TOKEN }}
|
| 43 |
+
hf-space-name: ${{ env.HF_SPACE_NAME }}
|
.github/workflows/deploy-prod.yml
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Deploy to Hugging Face - Production
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches:
|
| 6 |
+
- main
|
| 7 |
+
|
| 8 |
+
env:
|
| 9 |
+
HF_USERNAME: ${{ secrets.HF_USERNAME }}
|
| 10 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 11 |
+
HF_SPACE_NAME: ${{ secrets.HF_SPACE_NAME_PROD }}
|
| 12 |
+
|
| 13 |
+
jobs:
|
| 14 |
+
deploy:
|
| 15 |
+
runs-on: ubuntu-latest
|
| 16 |
+
|
| 17 |
+
steps:
|
| 18 |
+
- name: Checkout repository
|
| 19 |
+
uses: actions/checkout@v4
|
| 20 |
+
with:
|
| 21 |
+
lfs: true
|
| 22 |
+
|
| 23 |
+
- name: Setup Node.js
|
| 24 |
+
uses: actions/setup-node@v4
|
| 25 |
+
with:
|
| 26 |
+
node-version: '18'
|
| 27 |
+
cache: 'npm'
|
| 28 |
+
|
| 29 |
+
- name: Install dependencies
|
| 30 |
+
run: npm ci
|
| 31 |
+
|
| 32 |
+
- name: Build application
|
| 33 |
+
run: npm run build
|
| 34 |
+
|
| 35 |
+
- name: Deploy to Hugging Face Space
|
| 36 |
+
uses: ./.github/actions/deploy-to-hf
|
| 37 |
+
with:
|
| 38 |
+
environment: prod
|
| 39 |
+
branch: ${{ github.ref_name }}
|
| 40 |
+
hf-username: ${{ env.HF_USERNAME }}
|
| 41 |
+
hf-token: ${{ env.HF_TOKEN }}
|
| 42 |
+
hf-space-name: ${{ env.HF_SPACE_NAME }}
|
| 43 |
+
|
| 44 |
+
- name: Create Tag and Release
|
| 45 |
+
uses: ./.github/actions/create-release
|
| 46 |
+
with:
|
| 47 |
+
branch: main
|
| 48 |
+
tag-prefix: main-
|
| 49 |
+
prerelease: false
|
| 50 |
+
release-name-prefix: Production Release
|
.github/workflows/deploy-stg.yml
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Deploy to Hugging Face Staging
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches:
|
| 6 |
+
- stg
|
| 7 |
+
|
| 8 |
+
env:
|
| 9 |
+
HF_USERNAME: ${{ secrets.HF_USERNAME }}
|
| 10 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 11 |
+
HF_SPACE_NAME: ${{ secrets.HF_SPACE_NAME_STG }}
|
| 12 |
+
|
| 13 |
+
jobs:
|
| 14 |
+
deploy:
|
| 15 |
+
runs-on: ubuntu-latest
|
| 16 |
+
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.merged == true)
|
| 17 |
+
|
| 18 |
+
steps:
|
| 19 |
+
- name: Checkout repository
|
| 20 |
+
uses: actions/checkout@v4
|
| 21 |
+
with:
|
| 22 |
+
lfs: true
|
| 23 |
+
|
| 24 |
+
- name: Setup Node.js
|
| 25 |
+
uses: actions/setup-node@v4
|
| 26 |
+
with:
|
| 27 |
+
node-version: '18'
|
| 28 |
+
cache: 'npm'
|
| 29 |
+
|
| 30 |
+
- name: Install dependencies
|
| 31 |
+
run: npm ci
|
| 32 |
+
|
| 33 |
+
- name: Build application
|
| 34 |
+
run: npm run build
|
| 35 |
+
|
| 36 |
+
- name: Deploy to Hugging Face Space
|
| 37 |
+
uses: ./.github/actions/deploy-to-hf
|
| 38 |
+
with:
|
| 39 |
+
environment: stg
|
| 40 |
+
branch: ${{ github.ref_name }}
|
| 41 |
+
hf-username: ${{ env.HF_USERNAME }}
|
| 42 |
+
hf-token: ${{ env.HF_TOKEN }}
|
| 43 |
+
hf-space-name: ${{ env.HF_SPACE_NAME }}
|
| 44 |
+
|
| 45 |
+
- name: Create Tag and Pre-release
|
| 46 |
+
uses: ./.github/actions/create-release
|
| 47 |
+
with:
|
| 48 |
+
branch: stg
|
| 49 |
+
tag-prefix: stg-
|
| 50 |
+
prerelease: true
|
| 51 |
+
release-name-prefix: Staging Pre-release
|
.github/workflows/deploy-test.yml
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Deploy to Hugging Face Test
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches:
|
| 6 |
+
- feature/ui-test
|
| 7 |
+
workflow_dispatch:
|
| 8 |
+
|
| 9 |
+
env:
|
| 10 |
+
HF_USERNAME: ${{ secrets.HF_USERNAME }}
|
| 11 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 12 |
+
HF_SPACE_NAME: FE_Test
|
| 13 |
+
|
| 14 |
+
jobs:
|
| 15 |
+
sync-to-huggingface-mock:
|
| 16 |
+
runs-on: ubuntu-latest
|
| 17 |
+
steps:
|
| 18 |
+
- name: Checkout repository
|
| 19 |
+
uses: actions/checkout@v4
|
| 20 |
+
with:
|
| 21 |
+
lfs: true
|
| 22 |
+
|
| 23 |
+
- name: Setup Node.js
|
| 24 |
+
uses: actions/setup-node@v4
|
| 25 |
+
with:
|
| 26 |
+
node-version: '18'
|
| 27 |
+
cache: 'npm'
|
| 28 |
+
|
| 29 |
+
- name: Install dependencies
|
| 30 |
+
run: npm ci
|
| 31 |
+
|
| 32 |
+
- name: Build application
|
| 33 |
+
run: npm run build
|
| 34 |
+
|
| 35 |
+
- name: Deploy to Hugging Face Space
|
| 36 |
+
uses: ./.github/actions/deploy-to-hf
|
| 37 |
+
with:
|
| 38 |
+
environment: test
|
| 39 |
+
branch: ${{ github.ref_name }}
|
| 40 |
+
hf-username: ${{ env.HF_USERNAME }}
|
| 41 |
+
hf-token: ${{ env.HF_TOKEN }}
|
| 42 |
+
hf-space-name: ${{ env.HF_SPACE_NAME }}
|
.gitignore
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
| 2 |
+
|
| 3 |
+
# dependencies
|
| 4 |
+
/node_modules
|
| 5 |
+
/.pnp
|
| 6 |
+
.pnp.*
|
| 7 |
+
.yarn/*
|
| 8 |
+
!.yarn/patches
|
| 9 |
+
!.yarn/plugins
|
| 10 |
+
!.yarn/releases
|
| 11 |
+
!.yarn/versions
|
| 12 |
+
|
| 13 |
+
# testing
|
| 14 |
+
/coverage
|
| 15 |
+
|
| 16 |
+
# next.js
|
| 17 |
+
/.next/
|
| 18 |
+
/out/
|
| 19 |
+
|
| 20 |
+
.okta.env
|
| 21 |
+
|
| 22 |
+
# production
|
| 23 |
+
/build
|
| 24 |
+
|
| 25 |
+
# misc
|
| 26 |
+
.DS_Store
|
| 27 |
+
*.pem
|
| 28 |
+
|
| 29 |
+
# debug
|
| 30 |
+
npm-debug.log*
|
| 31 |
+
yarn-debug.log*
|
| 32 |
+
yarn-error.log*
|
| 33 |
+
.pnpm-debug.log*
|
| 34 |
+
|
| 35 |
+
# env files (can opt-in for committing if needed)
|
| 36 |
+
.env
|
| 37 |
+
|
| 38 |
+
# vercel
|
| 39 |
+
.vercel
|
| 40 |
+
|
| 41 |
+
# typescript
|
| 42 |
+
*.tsbuildinfo
|
| 43 |
+
next-env.d.ts
|
| 44 |
+
|
| 45 |
+
# IDE
|
| 46 |
+
.idea/
|
| 47 |
+
.vscode/
|
| 48 |
+
|
| 49 |
+
# logs
|
| 50 |
+
*.log
|
| 51 |
+
|
| 52 |
+
# MCP and development tools
|
| 53 |
+
.kiro/
|
| 54 |
+
.playwright-mcp/
|
| 55 |
+
.serena/
|
| 56 |
+
.mcp.local.json
|
| 57 |
+
|
| 58 |
+
# git worktree
|
| 59 |
+
/worktree/
|
| 60 |
+
|
| 61 |
+
# temporary files
|
| 62 |
+
/tmp/
|
| 63 |
+
|
| 64 |
+
# sample pages (development only)
|
| 65 |
+
/app/cn-sample/
|
| 66 |
+
docs/steering/guideline/
|
.mcp.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c95b9e188bc5421a0df205e4d5efac8ad62d164e02cfe5324839cd0192cadda5
|
| 3 |
+
size 676
|
.prettierignore
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
| 2 |
+
|
| 3 |
+
# dependencies
|
| 4 |
+
/node_modules
|
| 5 |
+
/.pnp
|
| 6 |
+
.pnp.*
|
| 7 |
+
.yarn/*
|
| 8 |
+
!.yarn/patches
|
| 9 |
+
!.yarn/plugins
|
| 10 |
+
!.yarn/releases
|
| 11 |
+
!.yarn/versions
|
| 12 |
+
|
| 13 |
+
# testing
|
| 14 |
+
/coverage
|
| 15 |
+
|
| 16 |
+
# next.js
|
| 17 |
+
/.next/
|
| 18 |
+
/out/
|
| 19 |
+
|
| 20 |
+
# production
|
| 21 |
+
/build
|
| 22 |
+
|
| 23 |
+
# misc
|
| 24 |
+
.DS_Store
|
| 25 |
+
*.pem
|
| 26 |
+
|
| 27 |
+
# debug
|
| 28 |
+
npm-debug.log*
|
| 29 |
+
yarn-debug.log*
|
| 30 |
+
yarn-error.log*
|
| 31 |
+
.pnpm-debug.log*
|
| 32 |
+
|
| 33 |
+
# env files (can opt-in for committing if needed)
|
| 34 |
+
.env*
|
| 35 |
+
|
| 36 |
+
# vercel
|
| 37 |
+
.vercel
|
| 38 |
+
|
| 39 |
+
# typescript
|
| 40 |
+
*.tsbuildinfo
|
| 41 |
+
next-env.d.ts
|
.prettierrc
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"semi": true,
|
| 3 |
+
"singleQuote": true,
|
| 4 |
+
"singleAttributePerLine": false,
|
| 5 |
+
"htmlWhitespaceSensitivity": "css",
|
| 6 |
+
"printWidth": 150,
|
| 7 |
+
"plugins": ["prettier-plugin-organize-imports", "prettier-plugin-tailwindcss"],
|
| 8 |
+
"tailwindFunctions": ["clsx", "cn"],
|
| 9 |
+
"tabWidth": 2,
|
| 10 |
+
"overrides": [
|
| 11 |
+
{
|
| 12 |
+
"files": "**/*.yml",
|
| 13 |
+
"options": {
|
| 14 |
+
"tabWidth": 2
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
]
|
| 18 |
+
}
|
AGENTS.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Repository Guidelines
|
| 2 |
+
|
| 3 |
+
## Project Structure & Module Organization
|
| 4 |
+
- `app/` hosts Next.js routes, layouts, and server actions for the web UI.
|
| 5 |
+
- `components/`, `hooks/`, `store/`, and `utils/` group reusable UI pieces, custom hooks, Zustand stores, and shared helpers.
|
| 6 |
+
- `services/`, `api-client/`, and `server/` encapsulate API integrations, background jobs, and Hono handlers; keep external calls here.
|
| 7 |
+
- `public/` stores static assets, while `docs/` and `schema/` capture product and API documentation.
|
| 8 |
+
- Tests and fixtures live under `test/`; mirror the runtime folder structure when adding coverage.
|
| 9 |
+
|
| 10 |
+
## Build, Test, and Development Commands
|
| 11 |
+
- `npm run dev` boots the local Next.js app on port 3200; use `npm run dev:restart` when you need a clean background process.
|
| 12 |
+
- `npm run build` compiles the production bundle, and `npm start` serves the build output.
|
| 13 |
+
- `npm run lint` runs ESLint with the Next.js config; `npm run lintfix` chains Prettier and ESLint for autofixes.
|
| 14 |
+
- `npm test`, `npm run test:watch`, and `npm run test:ui` execute Vitest in batch, watch, or UI mode.
|
| 15 |
+
- Use `npm run generate:dummy-screenshots` or `npm run prepare:dummy-images` to refresh design placeholders referenced in demos.
|
| 16 |
+
|
| 17 |
+
## Coding Style & Naming Conventions
|
| 18 |
+
- TypeScript and modern React patterns are expected; default to server components unless client interactivity is required.
|
| 19 |
+
- Formatting is enforced by Prettier (2-space indentation, single quotes, trailing commas) and ESLint; run lint before committing.
|
| 20 |
+
- Name React components and Zustand stores in PascalCase, hooks with the `use` prefix, and internal helpers in camelCase.
|
| 21 |
+
- Keep import paths absolute via the `@` alias rather than deep relative chains, and group CSS or Tailwind utilities near their components.
|
| 22 |
+
|
| 23 |
+
## Testing Guidelines
|
| 24 |
+
- Write unit and integration specs with Vitest; locate files beside the code (`*.test.ts[x]`) or under `test/` mirroring the module path.
|
| 25 |
+
- Mock external services through helpers in `services/` to avoid real network calls, and assert both success and error flows.
|
| 26 |
+
- Run `npm test` plus `npm run lint` before pushing; add snapshots only when the UI surface is stable.
|
| 27 |
+
|
| 28 |
+
## Commit & Pull Request Guidelines
|
| 29 |
+
- Follow Conventional Commits (`feat:`, `fix:`, `chore:`) as in the git log; include localization context when touching Japanese copy.
|
| 30 |
+
- Each pull request should link the relevant issue, describe behavior changes, note required environment updates, and attach UI screenshots or recordings.
|
| 31 |
+
- Confirm CI passes locally (`npm run lint`, `npm test`, `npm run build`) and mention any skipped checks or follow-up tasks in the description.
|
| 32 |
+
|
| 33 |
+
## Configuration Tips
|
| 34 |
+
- Secrets live in `.env.local`; never commit credentials or API tokens—consult `api-config.json` for expected keys.
|
| 35 |
+
- Update documentation in `docs/` when endpoints or flows change, and surface breaking environment changes in the pull request summary.
|
CLAUDE.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Claude Project Settings
|
| 2 |
+
|
| 3 |
+
## 言語設定
|
| 4 |
+
|
| 5 |
+
すべての回答は日本語で行ってください。
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
## 開発サーバー実行方法
|
| 9 |
+
|
| 10 |
+
開発サーバーを起動する際は、以下のコマンドを使用してください:
|
| 11 |
+
|
| 12 |
+
```bash
|
| 13 |
+
npm run dev:restart
|
| 14 |
+
```
|
| 15 |
+
|
| 16 |
+
このコマンドは自動的に:
|
| 17 |
+
|
| 18 |
+
1. ポート3200で動作中のプロセスをkill
|
| 19 |
+
2. 開発サーバーを起動
|
| 20 |
+
3. ログをdev.logファイルとコンソール両方に出力
|
| 21 |
+
|
| 22 |
+
## コード編集時の自動フォーマット
|
| 23 |
+
|
| 24 |
+
ファイルを編集した後は、必ずPrettierでフォーマットしてください:
|
| 25 |
+
|
| 26 |
+
- `npx prettier --write <編集したファイル>` - Prettierによる自動フォーマット(import順序の整理も含む)
|
| 27 |
+
|
| 28 |
+
## ハルシネーション禁止
|
| 29 |
+
|
| 30 |
+
特別な指示がない限り、推測や憶測に基づいたコード生成は禁止します。不明な点がある場合は必ず確認を求めてください。
|
| 31 |
+
|
| 32 |
+
## TypeScriptにおけるany型の使用禁止
|
| 33 |
+
|
| 34 |
+
TypeScriptコードにおいて、any型の使用を固く禁じます。型安全性を保つため、必ず適切な型定義を行ってください。
|
| 35 |
+
|
| 36 |
+
## インポート時のパス指定ルール
|
| 37 |
+
|
| 38 |
+
相対パスでのインポートを禁止します。必ず以下のパスエイリアスを使用してください:
|
| 39 |
+
|
| 40 |
+
- `@/` - プロジェクトルートからのパス(例:`@/components/ui/button`)
|
| 41 |
+
- 相対パス禁止例:`../../../components/ui/button`、`./utils/helper`
|
| 42 |
+
|
| 43 |
+
正しい例:
|
| 44 |
+
|
| 45 |
+
```typescript
|
| 46 |
+
import { Button } from '@/components/ui/button';
|
| 47 |
+
import { useGlobalStore } from '@/store/global';
|
| 48 |
+
import type { SwotData } from '@/schema/pox';
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
誤った例(使用禁止):
|
| 52 |
+
|
| 53 |
+
```typescript
|
| 54 |
+
import { Button } from '../../../components/ui/button';
|
| 55 |
+
import { useGlobalStore } from './store/global';
|
| 56 |
+
import type { SwotData } from '../../schema/pox';
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
# 開発の流れ
|
| 60 |
+
|
| 61 |
+
## 開発フロー
|
| 62 |
+
|
| 63 |
+
1. **コード修正**
|
| 64 |
+
- 必要な機能追加やバグ修正を実施
|
| 65 |
+
- TypeScript型定義を正確に行う(any型禁止)
|
| 66 |
+
|
| 67 |
+
2. **自動フォーマット**
|
| 68 |
+
- 修正したファイルをPrettierでフォーマット
|
| 69 |
+
|
| 70 |
+
```bash
|
| 71 |
+
npx prettier --write <編集したファイル>
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
3. **動作確認**
|
| 75 |
+
- 下記の「動作確認手順」に従ってPlaywriteMCPやCurlで動作を確認
|
| 76 |
+
- エラーや不具合が発生した場合は修正
|
| 77 |
+
|
| 78 |
+
4. **エラー対応**
|
| 79 |
+
- ログを確認して問題を特定
|
| 80 |
+
- 修正後、再度動作確認を実施
|
| 81 |
+
- 正常動作するまで3-4を繰り返す
|
| 82 |
+
|
| 83 |
+
5. **ビルド確認**
|
| 84 |
+
- コミット前に必ずビルドを実行
|
| 85 |
+
|
| 86 |
+
```bash
|
| 87 |
+
npm run build
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
- ビルドエラーがある場合は修正してから再度ビルド
|
| 91 |
+
- ビルドが成功したことを確認してからコミット
|
| 92 |
+
|
| 93 |
+
# HTML生成機能の基本動作確認
|
| 94 |
+
|
| 95 |
+
## 画面リンク集
|
| 96 |
+
|
| 97 |
+
### メイン機能ページ
|
| 98 |
+
- **トップページ**: `http://localhost:3200/`
|
| 99 |
+
- **リフレッシュモーメンツ選択**: `http://localhost:3200/refresh-moments`
|
| 100 |
+
- **改善案生成結果**: `http://localhost:3200/refresh-moments/improvement-result`
|
| 101 |
+
- **ダミーモード改善案生成結果**: `http://localhost:3200/refresh-moments/improvement-result?dummyMode=true`
|
| 102 |
+
|
| 103 |
+
### その他のページ
|
| 104 |
+
- **新UIプレビュー**: `http://localhost:3200/updatedui`
|
| 105 |
+
|
| 106 |
+
## 動作確認手順
|
| 107 |
+
|
| 108 |
+
1. **開発サーバー起動**
|
| 109 |
+
|
| 110 |
+
```bash
|
| 111 |
+
npm run dev:restart
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
2. **画面アクセス**
|
| 115 |
+
- ブラウザで `http://localhost:3200/refresh-moments` にアクセス
|
| 116 |
+
- モーメントを選択してから `http://localhost:3200/refresh-moments/improvement-result` に遷移
|
| 117 |
+
- **ダミーモード**: `http://localhost:3200/refresh-moments/improvement-result?dummyMode=true` で直接アクセス可能
|
| 118 |
+
- PlaywriteMCPで確認
|
| 119 |
+
- APIのみの修正の場合はCurlでAPI打鍵確認
|
| 120 |
+
|
| 121 |
+
3. **モーメント選択操作**
|
| 122 |
+
- refresh-moments画面でモーメントを選択
|
| 123 |
+
- 改善案生成ボタンをクリックして improvement-result 画面に遷移
|
| 124 |
+
|
| 125 |
+
4. **ログ確認**
|
| 126 |
+
```bash
|
| 127 |
+
tail -f dev.log
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
## 動作確認ポイント
|
| 131 |
+
|
| 132 |
+
### 正常な処理フロー
|
| 133 |
+
|
| 134 |
+
1. スクリーンショット取得完了
|
| 135 |
+
2. FV HTML生成開始
|
| 136 |
+
3. Adobe Firefly画像生成開始(バックグラウンド)
|
| 137 |
+
4. gpt-image-1パイプライン試行(組織未認証の場合失敗)
|
| 138 |
+
5. 簡易版gpt-image-1フォールバック(403エラーの場合失敗)
|
| 139 |
+
6. 最終フォールバック:モックHTML表示
|
| 140 |
+
|
| 141 |
+
### エラーパターン
|
| 142 |
+
|
| 143 |
+
- **gpt-image-1エラー**: `403 Forbidden` - 組織未認証
|
| 144 |
+
- **セグメンテーションエラー**: `TypeError: Cannot read properties of undefined`
|
| 145 |
+
- **画像フォーマットエラー**: `400 Bad Request` - PNG変換で解決済み
|
| 146 |
+
|
| 147 |
+
### 成功判定
|
| 148 |
+
|
| 149 |
+
- 画面に「FVとコンテンツを生成中...」が表示される
|
| 150 |
+
- Adobe Fireflyバッチ処理が開始される
|
| 151 |
+
- ログに `[CONTENTS HTML] Images generated successfully` が出力さ���る(2分程度)
|
| 152 |
+
|
| 153 |
+
# 重要な指示のリマインダー
|
| 154 |
+
|
| 155 |
+
要求されたことだけを実行してください。それ以上でも以下でもありません。
|
| 156 |
+
目的達成のために絶対に必要でない限り、ファイルを作成しないでください。
|
| 157 |
+
新規ファイルを作成するよりも、既存ファイルの編集を常に優先してください。
|
| 158 |
+
ドキュメントファイル(\*.md)やREADMEファイルを自発的に作成しないでください。ユーザーから明示的に要求された場合のみドキュメントファイルを作成してください。
|
| 159 |
+
|
| 160 |
+
## Serena MCP使用ガイドライン
|
| 161 |
+
|
| 162 |
+
- **既存コード解析**や**アーキテクチャ理解**が必要な場面では原則Serena MCPを使用してコードの解析を行う
|
| 163 |
+
- **Serena推奨場面**:
|
| 164 |
+
- コードベース全体の構造理解
|
| 165 |
+
- シンボル間の参照関係調査
|
| 166 |
+
- クラス・関数・変数の使用箇所特定
|
| 167 |
+
- ファイル間の依存関係分析
|
| 168 |
+
- リファクタリングや機能追加の影響範囲調査
|
| 169 |
+
- 設計パターンの実装箇所探索
|
| 170 |
+
- バグの原因となる関連コード特定
|
| 171 |
+
- **通常ツール推奨場面**:
|
| 172 |
+
- 単純なファイル読み込み・編集
|
| 173 |
+
- 既知のファイル・関数への直接的な変更
|
| 174 |
+
- シンプルな文字列検索・置換
|
| 175 |
+
- Serena MCPの機能を活用して効率的で正確なコード分析を心がける
|
| 176 |
+
|
| 177 |
+
# Gradio API打鍵方法
|
| 178 |
+
|
| 179 |
+
Gradio APIのエンドポイントを直接確認する場合は、以下の方法を使用してください:
|
| 180 |
+
|
| 181 |
+
```javascript
|
| 182 |
+
const { Client } = require('@gradio/client');
|
| 183 |
+
|
| 184 |
+
async function testGradioAPI() {
|
| 185 |
+
try {
|
| 186 |
+
const client = await Client.connect('dentsudigital/mugenAILP_dev', {
|
| 187 |
+
hf_token: process.env.GRADIO_TOKEN, // .envファイルのトークンを使用
|
| 188 |
+
});
|
| 189 |
+
|
| 190 |
+
// 例: get_proposal_prediction_dummyエンドポイントの呼び出し
|
| 191 |
+
const result = await client.predict('/get_proposal_prediction_dummy', {});
|
| 192 |
+
console.log(JSON.stringify(result.data, null, 2));
|
| 193 |
+
} catch (error) {
|
| 194 |
+
console.error('Error:', error.message);
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
testGradioAPI();
|
| 199 |
+
```
|
| 200 |
+
|
| 201 |
+
**注意事項:**
|
| 202 |
+
|
| 203 |
+
- Gradio APIのレスポンスは通常`result.data`に配列形式で格納される
|
| 204 |
+
- 配列の最初の要素が実際のレスポンスデータであることが多い
|
| 205 |
+
- HuggingFaceのトークン(`hf_`で始まる)が必要な場合がある
|
Dockerfile
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# syntax=docker.io/docker/dockerfile:1
|
| 2 |
+
|
| 3 |
+
# Force cache invalidation for HuggingFace Spaces
|
| 4 |
+
ARG CACHEBUST=1
|
| 5 |
+
|
| 6 |
+
FROM node:slim AS base
|
| 7 |
+
|
| 8 |
+
# Install Python and build tools for native dependencies
|
| 9 |
+
# Install Chromium and dependencies for Puppeteer
|
| 10 |
+
RUN apt-get update && apt-get install -y \
|
| 11 |
+
python3 make g++ \
|
| 12 |
+
chromium \
|
| 13 |
+
fonts-noto-cjk \
|
| 14 |
+
fonts-liberation \
|
| 15 |
+
fonts-freefont-ttf \
|
| 16 |
+
ca-certificates \
|
| 17 |
+
libnss3 \
|
| 18 |
+
libatk-bridge2.0-0 \
|
| 19 |
+
libdrm2 \
|
| 20 |
+
libxcomposite1 \
|
| 21 |
+
libxdamage1 \
|
| 22 |
+
libxfixes3 \
|
| 23 |
+
libxrandr2 \
|
| 24 |
+
libgbm1 \
|
| 25 |
+
libxkbcommon0 \
|
| 26 |
+
libasound2 \
|
| 27 |
+
libatspi2.0-0 \
|
| 28 |
+
libgtk-3-0 \
|
| 29 |
+
libglib2.0-0 \
|
| 30 |
+
libxss1 \
|
| 31 |
+
libgconf-2-4 \
|
| 32 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 33 |
+
|
| 34 |
+
# Set Puppeteer to use installed Chromium
|
| 35 |
+
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
| 36 |
+
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
| 37 |
+
|
| 38 |
+
# Install dependencies only when needed
|
| 39 |
+
FROM base AS deps
|
| 40 |
+
WORKDIR /app
|
| 41 |
+
|
| 42 |
+
# Install dependencies based on the preferred package manager
|
| 43 |
+
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
|
| 44 |
+
RUN \
|
| 45 |
+
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
|
| 46 |
+
elif [ -f package-lock.json ]; then npm ci --include=optional; \
|
| 47 |
+
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
|
| 48 |
+
else echo "Lockfile not found." && exit 1; \
|
| 49 |
+
fi
|
| 50 |
+
|
| 51 |
+
# Install the correct platform-specific packages based on architecture
|
| 52 |
+
RUN if [ "$(uname -m)" = "x86_64" ]; then \
|
| 53 |
+
npm install lightningcss-linux-x64-gnu@1.29.1 --no-save; \
|
| 54 |
+
elif [ "$(uname -m)" = "aarch64" ]; then \
|
| 55 |
+
npm install lightningcss-linux-arm64-gnu@1.29.1 --no-save; \
|
| 56 |
+
fi && \
|
| 57 |
+
npm rebuild @tailwindcss/oxide --update-binary
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
# Rebuild the source code only when needed
|
| 61 |
+
FROM base AS builder
|
| 62 |
+
WORKDIR /app
|
| 63 |
+
# Clear all cache to avoid build issues
|
| 64 |
+
RUN rm -rf .next node_modules/.cache
|
| 65 |
+
|
| 66 |
+
COPY --from=deps /app/node_modules ./node_modules
|
| 67 |
+
|
| 68 |
+
COPY . .
|
| 69 |
+
|
| 70 |
+
# Rebuild native dependencies for the builder stage
|
| 71 |
+
# Rebuild sharp and other native modules for the platform
|
| 72 |
+
RUN if [ "$(uname -m)" = "x86_64" ]; then \
|
| 73 |
+
npm install lightningcss-linux-x64-gnu@1.29.1 --no-save; \
|
| 74 |
+
elif [ "$(uname -m)" = "aarch64" ]; then \
|
| 75 |
+
npm install lightningcss-linux-arm64-gnu@1.29.1 --no-save; \
|
| 76 |
+
fi && \
|
| 77 |
+
npm rebuild sharp && \
|
| 78 |
+
npm rebuild @tailwindcss/oxide --update-binary
|
| 79 |
+
|
| 80 |
+
# Clean up build artifacts
|
| 81 |
+
RUN rm -rf .next/cache node_modules/.cache
|
| 82 |
+
|
| 83 |
+
# Build arguments for API keys (passed from HuggingFace Spaces environment)
|
| 84 |
+
ARG OPENAI_TEMPLATE_AI_DEV
|
| 85 |
+
ARG OPENAI_GENERATE_FV_AI
|
| 86 |
+
ARG CLAUDE_TEMPLATE_AI_DEV
|
| 87 |
+
ARG ANTHROPIC_API_KEY
|
| 88 |
+
ARG GOOGLE_API_KEY
|
| 89 |
+
ARG NEXT_PUBLIC_APP_ENV
|
| 90 |
+
ARG OKTA_OAUTH2_ISSUER
|
| 91 |
+
ARG OKTA_OAUTH2_CLIENT_ID
|
| 92 |
+
ARG OKTA_OAUTH2_CLIENT_SECRET
|
| 93 |
+
ARG SECRET
|
| 94 |
+
|
| 95 |
+
# Set environment variables for build time
|
| 96 |
+
ENV NEXT_TELEMETRY_DISABLED=1
|
| 97 |
+
ENV NEXT_PUBLIC_APP_ENV=$NEXT_PUBLIC_APP_ENV
|
| 98 |
+
ENV OPENAI_TEMPLATE_AI_DEV=$OPENAI_TEMPLATE_AI_DEV
|
| 99 |
+
ENV OPENAI_GENERATE_FV_AI=$OPENAI_GENERATE_FV_AI
|
| 100 |
+
ENV CLAUDE_TEMPLATE_AI_DEV=$CLAUDE_TEMPLATE_AI_DEV
|
| 101 |
+
ENV ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY
|
| 102 |
+
ENV GOOGLE_API_KEY=$GOOGLE_API_KEY
|
| 103 |
+
ENV OKTA_OAUTH2_ISSUER=$OKTA_OAUTH2_ISSUER
|
| 104 |
+
ENV OKTA_OAUTH2_CLIENT_ID=$OKTA_OAUTH2_CLIENT_ID
|
| 105 |
+
ENV OKTA_OAUTH2_CLIENT_SECRET=$OKTA_OAUTH2_CLIENT_SECRET
|
| 106 |
+
ENV SECRET=$SECRET
|
| 107 |
+
|
| 108 |
+
# Set memory limit for build process
|
| 109 |
+
ENV NODE_OPTIONS="--max-old-space-size=2048"
|
| 110 |
+
RUN \
|
| 111 |
+
if [ -f yarn.lock ]; then yarn run build; \
|
| 112 |
+
elif [ -f package-lock.json ]; then npm run build; \
|
| 113 |
+
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
|
| 114 |
+
else echo "Lockfile not found." && exit 1; \
|
| 115 |
+
fi
|
| 116 |
+
|
| 117 |
+
# Remove build cache to reduce image size (keep only necessary runtime files)
|
| 118 |
+
RUN rm -rf .next/cache/webpack .next/cache/swc node_modules/.cache
|
| 119 |
+
|
| 120 |
+
# Production image, copy all the files and run next
|
| 121 |
+
FROM base AS runner
|
| 122 |
+
WORKDIR /app
|
| 123 |
+
|
| 124 |
+
ENV NODE_ENV=production
|
| 125 |
+
# Disable Next.js telemetry and set cache to writable location
|
| 126 |
+
ENV NEXT_TELEMETRY_DISABLED=1
|
| 127 |
+
ENV TMPDIR=/tmp
|
| 128 |
+
|
| 129 |
+
# Runtime environment variables for API keys
|
| 130 |
+
ARG OPENAI_TEMPLATE_AI_DEV
|
| 131 |
+
ARG OPENAI_GENERATE_FV_AI
|
| 132 |
+
ARG CLAUDE_TEMPLATE_AI_DEV
|
| 133 |
+
ARG ANTHROPIC_API_KEY
|
| 134 |
+
ARG GOOGLE_API_KEY
|
| 135 |
+
ARG NEXT_PUBLIC_APP_ENV
|
| 136 |
+
ARG OKTA_OAUTH2_ISSUER
|
| 137 |
+
ARG OKTA_OAUTH2_CLIENT_ID
|
| 138 |
+
ARG OKTA_OAUTH2_CLIENT_SECRET
|
| 139 |
+
ARG SECRET
|
| 140 |
+
|
| 141 |
+
ENV NEXT_PUBLIC_APP_ENV=$NEXT_PUBLIC_APP_ENV
|
| 142 |
+
ENV OPENAI_TEMPLATE_AI_DEV=$OPENAI_TEMPLATE_AI_DEV
|
| 143 |
+
ENV OPENAI_GENERATE_FV_AI=$OPENAI_GENERATE_FV_AI
|
| 144 |
+
ENV CLAUDE_TEMPLATE_AI_DEV=$CLAUDE_TEMPLATE_AI_DEV
|
| 145 |
+
ENV ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY
|
| 146 |
+
ENV GOOGLE_API_KEY=$GOOGLE_API_KEY
|
| 147 |
+
ENV OKTA_OAUTH2_ISSUER=$OKTA_OAUTH2_ISSUER
|
| 148 |
+
ENV OKTA_OAUTH2_CLIENT_ID=$OKTA_OAUTH2_CLIENT_ID
|
| 149 |
+
ENV OKTA_OAUTH2_CLIENT_SECRET=$OKTA_OAUTH2_CLIENT_SECRET
|
| 150 |
+
ENV SECRET=$SECRET
|
| 151 |
+
|
| 152 |
+
RUN addgroup --system --gid 1001 nodejs
|
| 153 |
+
RUN adduser --system --uid 1001 nextjs --home /home/nextjs
|
| 154 |
+
|
| 155 |
+
# Copy node_modules for runtime (including Puppeteer)
|
| 156 |
+
COPY --from=builder /app/node_modules ./node_modules
|
| 157 |
+
RUN chown -R nextjs:nodejs ./node_modules
|
| 158 |
+
|
| 159 |
+
# Ensure nextjs user has access to home directory
|
| 160 |
+
RUN chown -R nextjs:nodejs /home/nextjs
|
| 161 |
+
|
| 162 |
+
COPY --from=builder /app/public ./public
|
| 163 |
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
| 164 |
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
| 165 |
+
|
| 166 |
+
# Copy .next directory but exclude cache to avoid permission issues
|
| 167 |
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/server ./.next/server
|
| 168 |
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/required-server-files.json ./.next/required-server-files.json
|
| 169 |
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/routes-manifest.json ./.next/routes-manifest.json
|
| 170 |
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/build-manifest.json ./.next/build-manifest.json
|
| 171 |
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/package.json ./.next/package.json
|
| 172 |
+
# Copy optional manifest files (may not exist in all builds)
|
| 173 |
+
RUN --mount=type=bind,from=builder,source=/app/.next,target=/tmp/source \
|
| 174 |
+
if [ -f /tmp/source/prerender-manifest.json ]; then cp /tmp/source/prerender-manifest.json ./.next/; fi && \
|
| 175 |
+
if [ -f /tmp/source/react-loadable-manifest.json ]; then cp /tmp/source/react-loadable-manifest.json ./.next/; fi && \
|
| 176 |
+
chown -R nextjs:nodejs ./.next/
|
| 177 |
+
|
| 178 |
+
# Create cache directories and ensure full permissions (combining both approaches)
|
| 179 |
+
RUN mkdir -p /tmp/.next/cache/images && \
|
| 180 |
+
mkdir -p /app/.next/cache/images && \
|
| 181 |
+
chmod -R 777 /tmp && \
|
| 182 |
+
chmod -R 755 /app/.next && \
|
| 183 |
+
chmod -R 777 /app/.next/cache/images && \
|
| 184 |
+
chown -R nextjs:nodejs /tmp/.next && \
|
| 185 |
+
chown -R nextjs:nodejs /app/.next
|
| 186 |
+
|
| 187 |
+
USER nextjs
|
| 188 |
+
|
| 189 |
+
EXPOSE 7860
|
| 190 |
+
|
| 191 |
+
ENV PORT=7860
|
| 192 |
+
ENV HOSTNAME="0.0.0.0"
|
| 193 |
+
# Optimize for HuggingFace free tier
|
| 194 |
+
ENV NODE_OPTIONS="--max-old-space-size=1024"
|
| 195 |
+
|
| 196 |
+
CMD ["node", "server.js"]
|
README.md
CHANGED
|
@@ -1,10 +1,39 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
| 7 |
pinned: false
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: FE_Dev
|
| 3 |
+
emoji: 🧪
|
| 4 |
+
colorFrom: pink
|
| 5 |
+
colorTo: yellow
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
pinned: false
|
| 9 |
+
license: mit
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# FE_Dev (Development)
|
| 13 |
+
|
| 14 |
+
🧪 **This is a development environment** - Features may be unstable
|
| 15 |
+
|
| 16 |
+
This is a Next.js application deployed to Hugging Face Spaces from the `dev` branch.
|
| 17 |
+
|
| 18 |
+
## Features
|
| 19 |
+
|
| 20 |
+
- Built with Next.js and React
|
| 21 |
+
- Containerized with Docker
|
| 22 |
+
- Automatic deployment from GitHub dev branch
|
| 23 |
+
- Development environment
|
| 24 |
+
|
| 25 |
+
## Development
|
| 26 |
+
|
| 27 |
+
To run locally:
|
| 28 |
+
|
| 29 |
+
```bash
|
| 30 |
+
npm install
|
| 31 |
+
npm run dev
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
The application will be available at http://localhost:3200
|
| 35 |
+
|
| 36 |
+
---
|
| 37 |
+
**Branch**: dev
|
| 38 |
+
**Environment**: Development
|
| 39 |
+
**Last Deploy**: Fri Oct 31 07:28:41 UTC 2025
|
analyze-components.mjs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import fs from 'fs';
|
| 2 |
+
|
| 3 |
+
const config = JSON.parse(fs.readFileSync('api-config.json', 'utf-8'));
|
| 4 |
+
|
| 5 |
+
// get_proposal_cn inputs
|
| 6 |
+
const cnInputs = [153, 108, 133, 68, 69, 237, 240, 1, 247, 288];
|
| 7 |
+
// get_proposal_fv inputs
|
| 8 |
+
const fvInputs = [153, 108, 133, 68, 69, 231, 232, 117, 288, 1];
|
| 9 |
+
|
| 10 |
+
console.log('=== get_proposal_cn Parameters ===\n');
|
| 11 |
+
cnInputs.forEach((id, index) => {
|
| 12 |
+
const component = config.components.find(c => c.id === id + 1); // Components array is 0-indexed but IDs are 1-indexed
|
| 13 |
+
if (component) {
|
| 14 |
+
console.log(`Parameter ${index}: ID=${id}`);
|
| 15 |
+
console.log(` Type: ${component.type}`);
|
| 16 |
+
if (component.props?.label) console.log(` Label: ${component.props.label}`);
|
| 17 |
+
if (component.props?.elem_id) console.log(` Elem ID: ${component.props.elem_id}`);
|
| 18 |
+
if (component.api_info) console.log(` API Info:`, component.api_info);
|
| 19 |
+
console.log();
|
| 20 |
+
}
|
| 21 |
+
});
|
| 22 |
+
|
| 23 |
+
console.log('\n=== get_proposal_fv Parameters ===\n');
|
| 24 |
+
fvInputs.forEach((id, index) => {
|
| 25 |
+
const component = config.components.find(c => c.id === id + 1);
|
| 26 |
+
if (component) {
|
| 27 |
+
console.log(`Parameter ${index}: ID=${id}`);
|
| 28 |
+
console.log(` Type: ${component.type}`);
|
| 29 |
+
if (component.props?.label) console.log(` Label: ${component.props.label}`);
|
| 30 |
+
if (component.props?.elem_id) console.log(` Elem ID: ${component.props.elem_id}`);
|
| 31 |
+
if (component.api_info) console.log(` API Info:`, component.api_info);
|
| 32 |
+
console.log();
|
| 33 |
+
}
|
| 34 |
+
});
|
| 35 |
+
|
| 36 |
+
// Find components with meaningful labels
|
| 37 |
+
console.log('\n=== All Components with Labels ===\n');
|
| 38 |
+
config.components.forEach(c => {
|
| 39 |
+
if (c.props?.label && !c.skip_api) {
|
| 40 |
+
console.log(`ID ${c.id - 1}: ${c.props.label} (${c.type})`);
|
| 41 |
+
}
|
| 42 |
+
});
|
api-client/gradio-proxy/check-url.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { rpcClient } from '@/api-client/utils/rpc-client';
|
| 2 |
+
import type { CheckUrlRequest, CheckUrlResponse } from '@/schema/gradio-proxy/check-url';
|
| 3 |
+
import { useMutation } from '@tanstack/react-query';
|
| 4 |
+
|
| 5 |
+
/**
|
| 6 |
+
* check_url APIを呼び出すカスタムフック(mutation版)
|
| 7 |
+
* @returns mutationオブジェクト
|
| 8 |
+
*/
|
| 9 |
+
export function useCheckUrl() {
|
| 10 |
+
return useMutation({
|
| 11 |
+
mutationFn: async (request: CheckUrlRequest): Promise<CheckUrlResponse> => {
|
| 12 |
+
console.log('[useCheckUrl] API呼び出し開始:', {
|
| 13 |
+
ownUrl: request.ownUrl,
|
| 14 |
+
urlCount: request.urlText.length,
|
| 15 |
+
dummyMode: request.dummyMode,
|
| 16 |
+
});
|
| 17 |
+
|
| 18 |
+
try {
|
| 19 |
+
const response = await rpcClient['gradio-proxy']['check-url'].$post({
|
| 20 |
+
json: request,
|
| 21 |
+
});
|
| 22 |
+
|
| 23 |
+
if (!response.ok) {
|
| 24 |
+
const errorData = (await response.json()) as { message?: string };
|
| 25 |
+
console.error('[useCheckUrl] API エラー:', errorData);
|
| 26 |
+
throw new Error(errorData.message || 'check_url APIの呼び出しに失敗しました');
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
const data = await response.json();
|
| 30 |
+
console.log('[useCheckUrl] API呼び出し成功');
|
| 31 |
+
return data as CheckUrlResponse;
|
| 32 |
+
} catch (error) {
|
| 33 |
+
console.error('[useCheckUrl] API呼び出しエラー:', error);
|
| 34 |
+
throw error;
|
| 35 |
+
}
|
| 36 |
+
},
|
| 37 |
+
});
|
| 38 |
+
}
|
api-client/gradio-proxy/excel.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createQueryOptions } from '@/api-client/query-config';
|
| 2 |
+
import { rpcClient } from '@/api-client/utils/rpc-client';
|
| 3 |
+
import type { ExcelRequest, ExcelResponse } from '@/schema/gradio-proxy/excel';
|
| 4 |
+
import type { FetchQueryOptions } from '@tanstack/react-query';
|
| 5 |
+
import { useQueryClient } from '@tanstack/react-query';
|
| 6 |
+
import { useCallback } from 'react';
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* excel APIを呼び出すカスタムフック
|
| 10 |
+
* @returns キャッシュ対応のAPI呼び出し関数(タイムアウト20分、キャッシュ24時間)
|
| 11 |
+
*/
|
| 12 |
+
export function useExcel() {
|
| 13 |
+
const queryClient = useQueryClient();
|
| 14 |
+
|
| 15 |
+
return useCallback(
|
| 16 |
+
async (request: ExcelRequest): Promise<ExcelResponse> => {
|
| 17 |
+
console.log('[useExcel] API呼び出し開始:', {
|
| 18 |
+
ownUrl: request.ownUrl,
|
| 19 |
+
urlCount: request.urlText.length,
|
| 20 |
+
hasCommonDict: !!request.commonDict,
|
| 21 |
+
hasScoreDict: !!request.scoreDict,
|
| 22 |
+
hasSummary: !!request.summary,
|
| 23 |
+
hasSwot: !!request.swot,
|
| 24 |
+
hasProposalFV: !!request.proposal_fv,
|
| 25 |
+
hasProposalCN: !!request.proposal_cn,
|
| 26 |
+
hasProposalPrediction: !!request.proposal_prediction,
|
| 27 |
+
hasProposalIntent: !!request.proposal_intent,
|
| 28 |
+
dummyMode: request.dummyMode,
|
| 29 |
+
});
|
| 30 |
+
|
| 31 |
+
return queryClient.fetchQuery(
|
| 32 |
+
createQueryOptions<ExcelResponse>({
|
| 33 |
+
queryKey: ['excel', request],
|
| 34 |
+
queryFn: async (): Promise<ExcelResponse> => {
|
| 35 |
+
try {
|
| 36 |
+
const response = await rpcClient['gradio-proxy']['excel'].$post({
|
| 37 |
+
json: request,
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
if (!response.ok) {
|
| 41 |
+
console.error('[useExcel] Response status:', response.status);
|
| 42 |
+
const errorData = (await response.json()) as { message?: string; status?: string };
|
| 43 |
+
console.error('[useExcel] API エラー:', errorData);
|
| 44 |
+
throw new Error(errorData.message || 'excel APIの呼び出しに失敗しました');
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
const data = await response.json();
|
| 48 |
+
console.log('[useExcel] API呼び出し成功');
|
| 49 |
+
return data as ExcelResponse;
|
| 50 |
+
} catch (error) {
|
| 51 |
+
console.error('[useExcel] API呼び出しエラー:', error);
|
| 52 |
+
throw error;
|
| 53 |
+
}
|
| 54 |
+
},
|
| 55 |
+
}) as FetchQueryOptions<ExcelResponse>,
|
| 56 |
+
);
|
| 57 |
+
},
|
| 58 |
+
[queryClient],
|
| 59 |
+
);
|
| 60 |
+
}
|
api-client/gradio-proxy/pox.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createQueryOptions } from '@/api-client/query-config';
|
| 2 |
+
import { rpcClient } from '@/api-client/utils/rpc-client';
|
| 3 |
+
import type { PoxRequest, PoxResponse } from '@/schema/gradio-proxy/pox';
|
| 4 |
+
import type { FetchQueryOptions } from '@tanstack/react-query';
|
| 5 |
+
import { useQueryClient } from '@tanstack/react-query';
|
| 6 |
+
import { useCallback } from 'react';
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* pox APIを呼び出すカスタムフック
|
| 10 |
+
* @returns キャッシュ対応のAPI呼び出し関数(タイムアウト20分、キャッシュ24時間)
|
| 11 |
+
*/
|
| 12 |
+
export function usePox() {
|
| 13 |
+
const queryClient = useQueryClient();
|
| 14 |
+
|
| 15 |
+
return useCallback(
|
| 16 |
+
async (request: PoxRequest): Promise<PoxResponse> => {
|
| 17 |
+
console.log('[usePox] API呼び出し開始:', {
|
| 18 |
+
hasCommonDict: !!request.commonDict,
|
| 19 |
+
hasScoreDict: !!request.scoreDict,
|
| 20 |
+
hasScoreTotal: !!request.score_total,
|
| 21 |
+
dummyMode: request.dummyMode,
|
| 22 |
+
});
|
| 23 |
+
|
| 24 |
+
return queryClient.fetchQuery(
|
| 25 |
+
createQueryOptions<PoxResponse>({
|
| 26 |
+
queryKey: ['pox', request],
|
| 27 |
+
queryFn: async (): Promise<PoxResponse> => {
|
| 28 |
+
try {
|
| 29 |
+
const response = await rpcClient['gradio-proxy']['pox'].$post({
|
| 30 |
+
json: request,
|
| 31 |
+
});
|
| 32 |
+
|
| 33 |
+
if (!response.ok) {
|
| 34 |
+
console.error('[usePox] Response status:', response.status);
|
| 35 |
+
const errorData = (await response.json()) as { message?: string; status?: string };
|
| 36 |
+
console.error('[usePox] API エラー:', errorData);
|
| 37 |
+
throw new Error(errorData.message || 'pox APIの呼び出しに失敗しました');
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
const data = await response.json();
|
| 41 |
+
console.log('[usePox] API呼び出し成功');
|
| 42 |
+
return data as PoxResponse;
|
| 43 |
+
} catch (error) {
|
| 44 |
+
console.error('[usePox] API呼び出しエラー:', error);
|
| 45 |
+
throw error;
|
| 46 |
+
}
|
| 47 |
+
},
|
| 48 |
+
}) as FetchQueryOptions<PoxResponse>,
|
| 49 |
+
);
|
| 50 |
+
},
|
| 51 |
+
[queryClient],
|
| 52 |
+
);
|
| 53 |
+
}
|
api-client/gradio-proxy/score-step3.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createQueryOptions } from '@/api-client/query-config';
|
| 2 |
+
import { rpcClient } from '@/api-client/utils/rpc-client';
|
| 3 |
+
import type { ScoreStep3Request, ScoreStep3Response } from '@/schema/gradio-proxy/score-step3';
|
| 4 |
+
import type { InputData } from '@/types/api';
|
| 5 |
+
import type { FetchQueryOptions } from '@tanstack/react-query';
|
| 6 |
+
import { useQueryClient } from '@tanstack/react-query';
|
| 7 |
+
import { useCallback } from 'react';
|
| 8 |
+
|
| 9 |
+
/**
|
| 10 |
+
* score_step3 APIを呼び出すカスタムフック
|
| 11 |
+
* @returns キャッシュ対応のAPI呼び出し関数(タイムアウト10秒、キャッシュ24時間)
|
| 12 |
+
*/
|
| 13 |
+
export function useScoreStep3() {
|
| 14 |
+
const queryClient = useQueryClient();
|
| 15 |
+
|
| 16 |
+
return useCallback(
|
| 17 |
+
async (request: ScoreStep3Request): Promise<ScoreStep3Response> => {
|
| 18 |
+
console.log('[useScoreStep3] API呼び出し開始:', {
|
| 19 |
+
ownUrl: (request.commonDict as InputData)?.own_url,
|
| 20 |
+
urlCount: (request.commonDict as InputData)?.competitor_urls?.length,
|
| 21 |
+
tempImagesCount: request.tempImages ? Object.keys(request.tempImages).length : 0,
|
| 22 |
+
hasCommonDict: !!request.commonDict,
|
| 23 |
+
dummyMode: request.dummyMode,
|
| 24 |
+
});
|
| 25 |
+
|
| 26 |
+
return queryClient.fetchQuery(
|
| 27 |
+
createQueryOptions<ScoreStep3Response>({
|
| 28 |
+
queryKey: ['score-step3', request],
|
| 29 |
+
queryFn: async (): Promise<ScoreStep3Response> => {
|
| 30 |
+
try {
|
| 31 |
+
const response = await rpcClient['gradio-proxy']['score-step3'].$post({
|
| 32 |
+
json: request,
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
if (!response.ok) {
|
| 36 |
+
console.error('[useScoreStep3] Response status:', response.status);
|
| 37 |
+
const errorData = (await response.json()) as { message?: string; status?: string };
|
| 38 |
+
console.error('[useScoreStep3] API エラー:', errorData);
|
| 39 |
+
throw new Error(errorData.message || 'score_step3 APIの呼び出しに失敗しました');
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
const data = await response.json();
|
| 43 |
+
console.log('[useScoreStep3] API呼び出し成功');
|
| 44 |
+
return data as ScoreStep3Response;
|
| 45 |
+
} catch (error) {
|
| 46 |
+
console.error('[useScoreStep3] API呼び出しエラー:', error);
|
| 47 |
+
throw error;
|
| 48 |
+
}
|
| 49 |
+
},
|
| 50 |
+
}) as FetchQueryOptions<ScoreStep3Response>,
|
| 51 |
+
);
|
| 52 |
+
},
|
| 53 |
+
[queryClient],
|
| 54 |
+
);
|
| 55 |
+
}
|
api-client/gradio-proxy/summary.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createQueryOptions } from '@/api-client/query-config';
|
| 2 |
+
import { rpcClient } from '@/api-client/utils/rpc-client';
|
| 3 |
+
import type { SummaryRequest, SummaryResponse } from '@/schema/gradio-proxy/summary';
|
| 4 |
+
import type { FetchQueryOptions } from '@tanstack/react-query';
|
| 5 |
+
import { useQueryClient } from '@tanstack/react-query';
|
| 6 |
+
import { useCallback } from 'react';
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* summary APIを呼び出すカスタムフック
|
| 10 |
+
* @returns キャッシュ対応のAPI呼び出し関数(タイムアウト20分、キャッシュ24時間)
|
| 11 |
+
*/
|
| 12 |
+
export function useSummary() {
|
| 13 |
+
const queryClient = useQueryClient();
|
| 14 |
+
|
| 15 |
+
return useCallback(
|
| 16 |
+
async (request: SummaryRequest): Promise<SummaryResponse> => {
|
| 17 |
+
console.log('[useSummary] API呼び出し開始:', {
|
| 18 |
+
hasCommonDict: !!request.commonDict,
|
| 19 |
+
hasScoreDict: !!request.scoreDict,
|
| 20 |
+
hasScoreTotal: !!request.score_total,
|
| 21 |
+
dummyMode: request.dummyMode,
|
| 22 |
+
});
|
| 23 |
+
|
| 24 |
+
return queryClient.fetchQuery(
|
| 25 |
+
createQueryOptions<SummaryResponse>({
|
| 26 |
+
queryKey: ['summary', request],
|
| 27 |
+
queryFn: async (): Promise<SummaryResponse> => {
|
| 28 |
+
try {
|
| 29 |
+
const response = await rpcClient['gradio-proxy']['summary'].$post({
|
| 30 |
+
json: request,
|
| 31 |
+
});
|
| 32 |
+
|
| 33 |
+
if (!response.ok) {
|
| 34 |
+
console.error('[useSummary] Response status:', response.status);
|
| 35 |
+
const errorData = (await response.json()) as { message?: string; status?: string };
|
| 36 |
+
console.error('[useSummary] API エラー:', errorData);
|
| 37 |
+
throw new Error(errorData.message || 'summary APIの呼び出しに失敗しました');
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
const data = await response.json();
|
| 41 |
+
console.log('[useSummary] API呼び出し成功');
|
| 42 |
+
return data as SummaryResponse;
|
| 43 |
+
} catch (error) {
|
| 44 |
+
console.error('[useSummary] API呼び出しエラー:', error);
|
| 45 |
+
throw error;
|
| 46 |
+
}
|
| 47 |
+
},
|
| 48 |
+
}) as FetchQueryOptions<SummaryResponse>,
|
| 49 |
+
);
|
| 50 |
+
},
|
| 51 |
+
[queryClient],
|
| 52 |
+
);
|
| 53 |
+
}
|
api-client/gradio-proxy/use-pre-analysis.ts
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ExcelRequest } from '@/schema/gradio-proxy/excel';
|
| 2 |
+
import type { PoxRequest } from '@/schema/gradio-proxy/pox';
|
| 3 |
+
import type { SummaryRequest } from '@/schema/gradio-proxy/summary';
|
| 4 |
+
import type { VisScoreRequest } from '@/schema/gradio-proxy/vis-score';
|
| 5 |
+
import { useResultStore } from '@/store/result';
|
| 6 |
+
import { useStateStore } from '@/store/state';
|
| 7 |
+
import { InputData } from '@/types/api';
|
| 8 |
+
import { useCallback, useMemo, useRef, useState } from 'react';
|
| 9 |
+
import { useExcel } from './excel';
|
| 10 |
+
import { usePox } from './pox';
|
| 11 |
+
import { useScoreStep3 } from './score-step3';
|
| 12 |
+
import { useSummary } from './summary';
|
| 13 |
+
import { useVisScore } from './vis-score';
|
| 14 |
+
|
| 15 |
+
interface UsePreAnalysisParams {
|
| 16 |
+
tempImages: Record<string, string>;
|
| 17 |
+
fvInfos: Record<string, unknown> | null;
|
| 18 |
+
cnInfo: Record<string, unknown> | null;
|
| 19 |
+
commonDict: InputData | null;
|
| 20 |
+
dummyMode?: boolean;
|
| 21 |
+
userEmail: string | null;
|
| 22 |
+
userIdentifier: string;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
interface ApiStatus {
|
| 26 |
+
duration: number;
|
| 27 |
+
status: 'idle' | 'loading' | 'success' | 'error';
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
export function usePreAnalysis(params: UsePreAnalysisParams) {
|
| 31 |
+
const { tempImages, fvInfos, cnInfo, commonDict, dummyMode = false, userEmail, userIdentifier } = params;
|
| 32 |
+
|
| 33 |
+
// Zustandストアから取得
|
| 34 |
+
const {
|
| 35 |
+
setScoreDict,
|
| 36 |
+
setCommonDict,
|
| 37 |
+
setScoreTotal,
|
| 38 |
+
setUrlCategoryScores,
|
| 39 |
+
setOwnCategoryScores,
|
| 40 |
+
setSummaryData,
|
| 41 |
+
setSummaryBaseData,
|
| 42 |
+
setSwotData75,
|
| 43 |
+
setSwotData78,
|
| 44 |
+
setSwotData79,
|
| 45 |
+
setSwotTable,
|
| 46 |
+
setDownloadData,
|
| 47 |
+
} = useResultStore();
|
| 48 |
+
|
| 49 |
+
const { updateApiStatus: updateStateStoreApiStatus } = useStateStore();
|
| 50 |
+
|
| 51 |
+
// API実行時間追跡
|
| 52 |
+
const apiStartTimesRef = useRef<Record<string, number>>({});
|
| 53 |
+
const [apiStatuses, setApiStatuses] = useState<Record<string, ApiStatus>>({
|
| 54 |
+
scoreStep3: { duration: 0, status: 'idle' },
|
| 55 |
+
visScore: { duration: 0, status: 'idle' },
|
| 56 |
+
summary: { duration: 0, status: 'idle' },
|
| 57 |
+
pox: { duration: 0, status: 'idle' },
|
| 58 |
+
excel: { duration: 0, status: 'idle' },
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
// API呼び出し用フック
|
| 62 |
+
const scoreStep3Api = useScoreStep3();
|
| 63 |
+
const visScoreApi = useVisScore();
|
| 64 |
+
const summaryApi = useSummary();
|
| 65 |
+
const poxApi = usePox();
|
| 66 |
+
const excelApi = useExcel();
|
| 67 |
+
|
| 68 |
+
// ステップごとのデータとエラー状態
|
| 69 |
+
const [scoreStep3Data, setScoreStep3Data] = useState<any>(null);
|
| 70 |
+
const [visScoreData, setVisScoreData] = useState<any>(null);
|
| 71 |
+
const [summaryData, setSummaryDataLocal] = useState<any>(null);
|
| 72 |
+
const [poxData, setPoxData] = useState<any>(null);
|
| 73 |
+
const [excelData, setExcelData] = useState<any>(null);
|
| 74 |
+
|
| 75 |
+
const [scoreStep3Error, setScoreStep3Error] = useState<Error | null>(null);
|
| 76 |
+
const [visScoreError, setVisScoreError] = useState<Error | null>(null);
|
| 77 |
+
const [summaryError, setSummaryError] = useState<Error | null>(null);
|
| 78 |
+
const [poxError, setPoxError] = useState<Error | null>(null);
|
| 79 |
+
const [excelError, setExcelError] = useState<Error | null>(null);
|
| 80 |
+
|
| 81 |
+
// API実行中フラグ
|
| 82 |
+
const [isExecuting, setIsExecuting] = useState(false);
|
| 83 |
+
const executionRef = useRef(false);
|
| 84 |
+
|
| 85 |
+
const updateApiStatus = useCallback(
|
| 86 |
+
(apiName: string, status: ApiStatus['status']) => {
|
| 87 |
+
// Map API names to match StateStore expectations
|
| 88 |
+
const stateStoreApiName =
|
| 89 |
+
{
|
| 90 |
+
scoreStep3: 'getScore',
|
| 91 |
+
visScore: 'getVisScore',
|
| 92 |
+
summary: 'getSummary',
|
| 93 |
+
pox: 'getPox',
|
| 94 |
+
excel: 'getExcel',
|
| 95 |
+
}[apiName] || apiName;
|
| 96 |
+
|
| 97 |
+
if (status === 'loading') {
|
| 98 |
+
apiStartTimesRef.current[apiName] = Date.now();
|
| 99 |
+
setApiStatuses((prev) => ({
|
| 100 |
+
...prev,
|
| 101 |
+
[apiName]: { duration: 0, status: 'loading' },
|
| 102 |
+
}));
|
| 103 |
+
// Sync to StateStore
|
| 104 |
+
updateStateStoreApiStatus(stateStoreApiName, { duration: 0, status: 'loading' });
|
| 105 |
+
} else if (status === 'success' || status === 'error') {
|
| 106 |
+
const startTime = apiStartTimesRef.current[apiName];
|
| 107 |
+
const duration = startTime ? (Date.now() - startTime) / 1000 : 0;
|
| 108 |
+
setApiStatuses((prev) => ({
|
| 109 |
+
...prev,
|
| 110 |
+
[apiName]: { duration, status },
|
| 111 |
+
}));
|
| 112 |
+
// Sync to StateStore
|
| 113 |
+
updateStateStoreApiStatus(stateStoreApiName, { duration, status });
|
| 114 |
+
}
|
| 115 |
+
},
|
| 116 |
+
[updateStateStoreApiStatus],
|
| 117 |
+
);
|
| 118 |
+
|
| 119 |
+
// メイン実行関数
|
| 120 |
+
const execute = useCallback(async () => {
|
| 121 |
+
// 手動実行なのでenabledはチェックしない(重複実行のみ防ぐ)
|
| 122 |
+
if (executionRef.current) {
|
| 123 |
+
console.log('[usePreAnalysis] Execution skipped: already running');
|
| 124 |
+
return;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
if (!commonDict) {
|
| 128 |
+
console.warn('[usePreAnalysis] commonDict is null, proceeding with empty object');
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
executionRef.current = true;
|
| 132 |
+
setIsExecuting(true);
|
| 133 |
+
|
| 134 |
+
try {
|
| 135 |
+
// Step 1: ScoreStep3
|
| 136 |
+
updateApiStatus('scoreStep3', 'loading');
|
| 137 |
+
const score = await scoreStep3Api({
|
| 138 |
+
tempImages: tempImages || {},
|
| 139 |
+
fvInfos: fvInfos || null,
|
| 140 |
+
cnInfo: cnInfo || null,
|
| 141 |
+
commonDict: commonDict || {},
|
| 142 |
+
dummyMode,
|
| 143 |
+
userEmail: userEmail ?? undefined,
|
| 144 |
+
userIdentifier,
|
| 145 |
+
});
|
| 146 |
+
|
| 147 |
+
if (Array.isArray(score)) {
|
| 148 |
+
setScoreStep3Data(score);
|
| 149 |
+
setScoreDict(score[0]);
|
| 150 |
+
setCommonDict(score[1]);
|
| 151 |
+
updateApiStatus('scoreStep3', 'success');
|
| 152 |
+
setScoreStep3Error(null);
|
| 153 |
+
} else {
|
| 154 |
+
throw new Error('scoreStep3の結果が不正な形式です');
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
// Step 2: VisScore
|
| 158 |
+
updateApiStatus('visScore', 'loading');
|
| 159 |
+
const vis: VisScoreRequest = {
|
| 160 |
+
commonDict: score[1] as Record<string, unknown>,
|
| 161 |
+
scoreDict: score[0] as Record<string, unknown>,
|
| 162 |
+
dummyMode,
|
| 163 |
+
userEmail: userEmail ?? undefined,
|
| 164 |
+
userIdentifier,
|
| 165 |
+
};
|
| 166 |
+
const visResult = await visScoreApi(vis);
|
| 167 |
+
|
| 168 |
+
if (Array.isArray(visResult)) {
|
| 169 |
+
setVisScoreData(visResult);
|
| 170 |
+
setScoreTotal(visResult[0]);
|
| 171 |
+
setUrlCategoryScores(visResult[1]);
|
| 172 |
+
setOwnCategoryScores(visResult[2]);
|
| 173 |
+
updateApiStatus('visScore', 'success');
|
| 174 |
+
setVisScoreError(null);
|
| 175 |
+
} else {
|
| 176 |
+
throw new Error('visScoreの結果が不正な形式です');
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
// Step 3: Summary & Pox(並列実行)
|
| 180 |
+
const summaryReq: SummaryRequest = {
|
| 181 |
+
commonDict: score[1] as Record<string, unknown>,
|
| 182 |
+
scoreDict: score[0] as Record<string, unknown>,
|
| 183 |
+
score_total: visResult[0] as Record<string, unknown>,
|
| 184 |
+
url_category_scores: visResult[1] as Record<string, unknown>,
|
| 185 |
+
own_category_score: visResult[2] as Record<string, unknown>,
|
| 186 |
+
dummyMode,
|
| 187 |
+
userEmail: userEmail ?? undefined,
|
| 188 |
+
userIdentifier,
|
| 189 |
+
};
|
| 190 |
+
|
| 191 |
+
const poxReq: PoxRequest = {
|
| 192 |
+
commonDict: score[1] as Record<string, unknown>,
|
| 193 |
+
scoreDict: score[0] as Record<string, unknown>,
|
| 194 |
+
score_total: visResult[0] as Record<string, unknown>,
|
| 195 |
+
dummyMode,
|
| 196 |
+
userEmail: userEmail ?? undefined,
|
| 197 |
+
userIdentifier,
|
| 198 |
+
};
|
| 199 |
+
|
| 200 |
+
updateApiStatus('summary', 'loading');
|
| 201 |
+
updateApiStatus('pox', 'loading');
|
| 202 |
+
|
| 203 |
+
const [summaryResult, poxResult] = await Promise.all([summaryApi(summaryReq), poxApi(poxReq)]);
|
| 204 |
+
|
| 205 |
+
if (Array.isArray(summaryResult)) {
|
| 206 |
+
setSummaryDataLocal(summaryResult);
|
| 207 |
+
setSummaryData(summaryResult[0]);
|
| 208 |
+
setSummaryBaseData(summaryResult[2]);
|
| 209 |
+
updateApiStatus('summary', 'success');
|
| 210 |
+
setSummaryError(null);
|
| 211 |
+
} else {
|
| 212 |
+
throw new Error('summaryの結果が不正な形式です');
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
if (Array.isArray(poxResult)) {
|
| 216 |
+
setPoxData(poxResult);
|
| 217 |
+
setSwotData75(poxResult[0]);
|
| 218 |
+
setSwotData78(poxResult[1]);
|
| 219 |
+
setSwotData79(poxResult[2]);
|
| 220 |
+
setSwotTable(poxResult[3]);
|
| 221 |
+
updateApiStatus('pox', 'success');
|
| 222 |
+
setPoxError(null);
|
| 223 |
+
} else {
|
| 224 |
+
throw new Error('poxの結果が不正な形式です');
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
// Step 4: Excel
|
| 228 |
+
updateApiStatus('excel', 'loading');
|
| 229 |
+
const excelReq: ExcelRequest = {
|
| 230 |
+
ownUrl: ((score[1] as Record<string, unknown>)?.own_url as string) || '',
|
| 231 |
+
urlText: ((score[1] as Record<string, unknown>)?.urls as string[]) || [],
|
| 232 |
+
commonDict: score[1] as Record<string, unknown>,
|
| 233 |
+
scoreDict: score[0] as Record<string, unknown>,
|
| 234 |
+
summary: summaryResult[0] as Record<string, unknown>,
|
| 235 |
+
swot: poxResult[0] as Record<string, unknown>,
|
| 236 |
+
dummyMode,
|
| 237 |
+
userEmail: userEmail ?? undefined,
|
| 238 |
+
userIdentifier,
|
| 239 |
+
};
|
| 240 |
+
|
| 241 |
+
const excelResult = await excelApi(excelReq);
|
| 242 |
+
|
| 243 |
+
if (Array.isArray(excelResult)) {
|
| 244 |
+
setExcelData(excelResult);
|
| 245 |
+
setDownloadData(excelResult[0]);
|
| 246 |
+
updateApiStatus('excel', 'success');
|
| 247 |
+
setExcelError(null);
|
| 248 |
+
} else {
|
| 249 |
+
throw new Error('excelの結果が不正な形式です');
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
console.log('[usePreAnalysis] All API calls completed successfully');
|
| 253 |
+
} catch (error) {
|
| 254 |
+
console.error('[usePreAnalysis] Error during execution:', error);
|
| 255 |
+
|
| 256 |
+
// エラー状態を設定
|
| 257 |
+
if (!scoreStep3Data) {
|
| 258 |
+
setScoreStep3Error(error instanceof Error ? error : new Error(String(error)));
|
| 259 |
+
updateApiStatus('scoreStep3', 'error');
|
| 260 |
+
} else if (!visScoreData) {
|
| 261 |
+
setVisScoreError(error instanceof Error ? error : new Error(String(error)));
|
| 262 |
+
updateApiStatus('visScore', 'error');
|
| 263 |
+
} else if (!summaryData || !poxData) {
|
| 264 |
+
if (!summaryData) {
|
| 265 |
+
setSummaryError(error instanceof Error ? error : new Error(String(error)));
|
| 266 |
+
updateApiStatus('summary', 'error');
|
| 267 |
+
}
|
| 268 |
+
if (!poxData) {
|
| 269 |
+
setPoxError(error instanceof Error ? error : new Error(String(error)));
|
| 270 |
+
updateApiStatus('pox', 'error');
|
| 271 |
+
}
|
| 272 |
+
} else {
|
| 273 |
+
setExcelError(error instanceof Error ? error : new Error(String(error)));
|
| 274 |
+
updateApiStatus('excel', 'error');
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
// エラーを再スローしてonSubmitのcatchブロックでキャッチできるようにする
|
| 278 |
+
throw error;
|
| 279 |
+
} finally {
|
| 280 |
+
setIsExecuting(false);
|
| 281 |
+
executionRef.current = false;
|
| 282 |
+
}
|
| 283 |
+
}, [
|
| 284 |
+
tempImages,
|
| 285 |
+
fvInfos,
|
| 286 |
+
cnInfo,
|
| 287 |
+
commonDict,
|
| 288 |
+
dummyMode,
|
| 289 |
+
userEmail,
|
| 290 |
+
userIdentifier,
|
| 291 |
+
scoreStep3Api,
|
| 292 |
+
visScoreApi,
|
| 293 |
+
summaryApi,
|
| 294 |
+
poxApi,
|
| 295 |
+
excelApi,
|
| 296 |
+
setScoreDict,
|
| 297 |
+
setCommonDict,
|
| 298 |
+
setScoreTotal,
|
| 299 |
+
setUrlCategoryScores,
|
| 300 |
+
setOwnCategoryScores,
|
| 301 |
+
setSummaryData,
|
| 302 |
+
setSummaryBaseData,
|
| 303 |
+
setSwotData75,
|
| 304 |
+
setSwotData78,
|
| 305 |
+
setSwotData79,
|
| 306 |
+
setSwotTable,
|
| 307 |
+
setDownloadData,
|
| 308 |
+
updateApiStatus,
|
| 309 |
+
]);
|
| 310 |
+
|
| 311 |
+
// 統合されたローディング状態
|
| 312 |
+
const isLoading = useMemo(() => {
|
| 313 |
+
return Object.values(apiStatuses).some((s) => s.status === 'loading');
|
| 314 |
+
}, [apiStatuses]);
|
| 315 |
+
|
| 316 |
+
// 統合されたエラー状態
|
| 317 |
+
const isError = useMemo(() => {
|
| 318 |
+
return !!scoreStep3Error || !!visScoreError || !!summaryError || !!poxError || !!excelError;
|
| 319 |
+
}, [scoreStep3Error, visScoreError, summaryError, poxError, excelError]);
|
| 320 |
+
|
| 321 |
+
// エラーメッセージ取得
|
| 322 |
+
const getErrorMessage = useCallback(() => {
|
| 323 |
+
if (scoreStep3Error) return `スコア分析に失敗しました (1/5): ${scoreStep3Error.message}`;
|
| 324 |
+
if (visScoreError) return `ビジュアル分析に失敗しました (2/5): ${visScoreError.message}`;
|
| 325 |
+
if (summaryError) return `サマリー生成に失敗しました (3/5): ${summaryError.message}`;
|
| 326 |
+
if (poxError) return `SWOT分析に失敗しました (4/5): ${poxError.message}`;
|
| 327 |
+
if (excelError) return `Excelファイル生成に失敗しました (5/5): ${excelError.message}`;
|
| 328 |
+
return 'データの取得に失敗しました';
|
| 329 |
+
}, [scoreStep3Error, visScoreError, summaryError, poxError, excelError]);
|
| 330 |
+
|
| 331 |
+
// リトライ関数
|
| 332 |
+
const refetch = useCallback(() => {
|
| 333 |
+
// エラー状態をリセット
|
| 334 |
+
setScoreStep3Error(null);
|
| 335 |
+
setVisScoreError(null);
|
| 336 |
+
setSummaryError(null);
|
| 337 |
+
setPoxError(null);
|
| 338 |
+
setExcelError(null);
|
| 339 |
+
|
| 340 |
+
// 再実行
|
| 341 |
+
execute();
|
| 342 |
+
}, [execute]);
|
| 343 |
+
|
| 344 |
+
return {
|
| 345 |
+
execute,
|
| 346 |
+
isLoading,
|
| 347 |
+
isError,
|
| 348 |
+
error: isError ? new Error(getErrorMessage()) : null,
|
| 349 |
+
apiStatuses,
|
| 350 |
+
refetch,
|
| 351 |
+
// 個別データ(デバッグ用)
|
| 352 |
+
data: {
|
| 353 |
+
scoreStep3: scoreStep3Data,
|
| 354 |
+
visScore: visScoreData,
|
| 355 |
+
summary: summaryData,
|
| 356 |
+
pox: poxData,
|
| 357 |
+
excel: excelData,
|
| 358 |
+
},
|
| 359 |
+
};
|
| 360 |
+
}
|
api-client/gradio-proxy/vis-score.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createQueryOptions } from '@/api-client/query-config';
|
| 2 |
+
import { rpcClient } from '@/api-client/utils/rpc-client';
|
| 3 |
+
import type { VisScoreRequest, VisScoreResponse } from '@/schema/gradio-proxy/vis-score';
|
| 4 |
+
import type { FetchQueryOptions } from '@tanstack/react-query';
|
| 5 |
+
import { useQueryClient } from '@tanstack/react-query';
|
| 6 |
+
import { useCallback } from 'react';
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* vis_score APIを呼び出すカスタムフック
|
| 10 |
+
* @returns キャッシュ対応のAPI呼び出し関数(タイムアウト20分、キャッシュ24時間)
|
| 11 |
+
*/
|
| 12 |
+
export function useVisScore() {
|
| 13 |
+
const queryClient = useQueryClient();
|
| 14 |
+
|
| 15 |
+
return useCallback(
|
| 16 |
+
async (request: VisScoreRequest): Promise<VisScoreResponse> => {
|
| 17 |
+
console.log('[useVisScore] API呼び出し開始:', {
|
| 18 |
+
hasCommonDict: !!request.commonDict,
|
| 19 |
+
hasScoreDict: !!request.scoreDict,
|
| 20 |
+
dummyMode: request.dummyMode,
|
| 21 |
+
});
|
| 22 |
+
|
| 23 |
+
return queryClient.fetchQuery(
|
| 24 |
+
createQueryOptions<VisScoreResponse>({
|
| 25 |
+
queryKey: ['vis-score', request],
|
| 26 |
+
queryFn: async (): Promise<VisScoreResponse> => {
|
| 27 |
+
try {
|
| 28 |
+
const response = await rpcClient['gradio-proxy']['vis-score'].$post({
|
| 29 |
+
json: request,
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
if (!response.ok) {
|
| 33 |
+
console.error('[useVisScore] Response status:', response.status);
|
| 34 |
+
const errorData = (await response.json()) as { message?: string; status?: string };
|
| 35 |
+
console.error('[useVisScore] API エラー:', errorData);
|
| 36 |
+
throw new Error(errorData.message || 'vis_score APIの呼び出しに失敗しました');
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
const data = await response.json();
|
| 40 |
+
console.log('[useVisScore] API呼び出し成功');
|
| 41 |
+
return data as VisScoreResponse;
|
| 42 |
+
} catch (error) {
|
| 43 |
+
console.error('[useVisScore] API呼び出しエラー:', error);
|
| 44 |
+
throw error;
|
| 45 |
+
}
|
| 46 |
+
},
|
| 47 |
+
}) as FetchQueryOptions<VisScoreResponse>,
|
| 48 |
+
);
|
| 49 |
+
},
|
| 50 |
+
[queryClient],
|
| 51 |
+
);
|
| 52 |
+
}
|
api-client/pox/queries.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { SwotData } from '@/schema/pox';
|
| 2 |
+
import { getPox } from '@/services/gradio-with-dummy';
|
| 3 |
+
import { useGlobalStore } from '@/store/global';
|
| 4 |
+
import { useQuery } from '@tanstack/react-query';
|
| 5 |
+
|
| 6 |
+
export function usePox(commonDict: unknown, scoreDict: unknown, scoreTotal: unknown, userEmail: string) {
|
| 7 |
+
const { dummyMode, userIdentifier } = useGlobalStore();
|
| 8 |
+
return useQuery<SwotData>({
|
| 9 |
+
queryKey: ['pox', commonDict, scoreDict, scoreTotal, userEmail, dummyMode],
|
| 10 |
+
queryFn: async () => {
|
| 11 |
+
const userData = {
|
| 12 |
+
commonDict: commonDict as object,
|
| 13 |
+
scoreDict: scoreDict as object,
|
| 14 |
+
score_total: scoreTotal as object,
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
// Gradio APIを直接呼び出し
|
| 18 |
+
const result = await getPox(userData, userEmail || null, userIdentifier);
|
| 19 |
+
|
| 20 |
+
// 結果の最初の要素(基本SWOT)を返す
|
| 21 |
+
if (Array.isArray(result) && result.length > 0) {
|
| 22 |
+
return result[0] as SwotData;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
throw new Error('Invalid POX response format');
|
| 26 |
+
},
|
| 27 |
+
enabled:
|
| 28 |
+
(dummyMode || userEmail !== '') &&
|
| 29 |
+
(dummyMode ||
|
| 30 |
+
(commonDict !== undefined &&
|
| 31 |
+
commonDict !== null &&
|
| 32 |
+
scoreDict !== undefined &&
|
| 33 |
+
scoreDict !== null &&
|
| 34 |
+
scoreTotal !== undefined &&
|
| 35 |
+
scoreTotal !== null)),
|
| 36 |
+
staleTime: 1000 * 60 * 5, // 5分間はキャッシュを使用
|
| 37 |
+
});
|
| 38 |
+
}
|
api-client/proposal/html-preview/index.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { parseApiResponse } from '@/api-client/utils/parseApiResponse';
|
| 2 |
+
import { rpcClient } from '@/api-client/utils/rpc-client';
|
| 3 |
+
import type { SwotData, SwotDataWithoutSummary } from '@/schema/pox';
|
| 4 |
+
import {
|
| 5 |
+
generateContentsHtmlResponseSchema,
|
| 6 |
+
generateFvHtmlResponseSchema,
|
| 7 |
+
type ContentSection,
|
| 8 |
+
type FvData,
|
| 9 |
+
type GenerateContentsHtmlResponse,
|
| 10 |
+
type GenerateFvHtmlResponse,
|
| 11 |
+
} from '@/schema/proposal';
|
| 12 |
+
import type { ThemeExtractionResult } from '@/schema/theme-extraction';
|
| 13 |
+
|
| 14 |
+
/**
|
| 15 |
+
* FV提案のHTML生成
|
| 16 |
+
*/
|
| 17 |
+
export async function generateFvHtml(params: {
|
| 18 |
+
tabName: string;
|
| 19 |
+
fvData: FvData;
|
| 20 |
+
provider?: 'openai' | 'gemini' | 'claude';
|
| 21 |
+
referenceUrl?: string;
|
| 22 |
+
screenshot?: string;
|
| 23 |
+
dummyMode?: boolean;
|
| 24 |
+
swotData?: SwotData | SwotDataWithoutSummary;
|
| 25 |
+
userEmail?: string;
|
| 26 |
+
sourcePage?: string;
|
| 27 |
+
forceFallbackMode?: boolean;
|
| 28 |
+
themeData?: ThemeExtractionResult | null;
|
| 29 |
+
}): Promise<GenerateFvHtmlResponse> {
|
| 30 |
+
// テーマデータがある場合はログ出力のみ(将来のAPI対応用)
|
| 31 |
+
if (params.themeData) {
|
| 32 |
+
console.log('[FV HTML] テーマデータが適用されます:', params.themeData);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
const response = await rpcClient.proposal['html-preview']['generate-fv-html'].$post({
|
| 36 |
+
json: {
|
| 37 |
+
tabName: params.tabName,
|
| 38 |
+
fvData: params.fvData,
|
| 39 |
+
provider: params.provider,
|
| 40 |
+
referenceUrl: params.referenceUrl,
|
| 41 |
+
screenshotUrl: params.screenshot, // パラメータ名をscreenshotUrlに変換
|
| 42 |
+
dummyMode: params.dummyMode,
|
| 43 |
+
swotData: params.swotData,
|
| 44 |
+
userEmail: params.userEmail,
|
| 45 |
+
sourcePage: params.sourcePage,
|
| 46 |
+
forceFallbackMode: params.forceFallbackMode,
|
| 47 |
+
themeData: params.themeData ?? undefined, // nullをundefinedに変換
|
| 48 |
+
},
|
| 49 |
+
});
|
| 50 |
+
return parseApiResponse(response, generateFvHtmlResponseSchema);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
/**
|
| 54 |
+
* コンテンツ提案のHTML生成
|
| 55 |
+
*/
|
| 56 |
+
export async function generateContentsHtml(params: {
|
| 57 |
+
tabName: string;
|
| 58 |
+
cnData: ContentSection[];
|
| 59 |
+
provider?: 'openai' | 'gemini' | 'claude';
|
| 60 |
+
referenceUrl?: string;
|
| 61 |
+
screenshot?: string;
|
| 62 |
+
dummyMode?: boolean;
|
| 63 |
+
userEmail?: string;
|
| 64 |
+
themeData?: ThemeExtractionResult | null;
|
| 65 |
+
}): Promise<GenerateContentsHtmlResponse> {
|
| 66 |
+
// テーマデータがある場合はログ出力のみ(将来のAPI対応用)
|
| 67 |
+
if (params.themeData) {
|
| 68 |
+
console.log('[CONTENTS HTML] テーマデータが適用されます:', params.themeData);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
const response = await rpcClient.proposal['html-preview']['generate-contents-html'].$post({
|
| 72 |
+
json: {
|
| 73 |
+
tabName: params.tabName,
|
| 74 |
+
cnData: params.cnData,
|
| 75 |
+
provider: params.provider,
|
| 76 |
+
referenceUrl: params.referenceUrl,
|
| 77 |
+
screenshotUrl: params.screenshot, // パラメータ名をscreenshotUrlに変換
|
| 78 |
+
dummyMode: params.dummyMode,
|
| 79 |
+
userEmail: params.userEmail,
|
| 80 |
+
themeData: params.themeData ?? undefined, // nullをundefinedに変換
|
| 81 |
+
},
|
| 82 |
+
});
|
| 83 |
+
return parseApiResponse(response, generateContentsHtmlResponseSchema);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
/**
|
| 87 |
+
* 画像生成を開始
|
| 88 |
+
*/
|
| 89 |
+
export async function startImageGeneration(
|
| 90 |
+
html: string,
|
| 91 |
+
tabName: string,
|
| 92 |
+
cnData: ContentSection[],
|
| 93 |
+
provider: 'openai' | 'claude' = 'claude',
|
| 94 |
+
): Promise<{
|
| 95 |
+
batchId: string;
|
| 96 |
+
estimatedCompletionTime: string;
|
| 97 |
+
message: string;
|
| 98 |
+
status: string;
|
| 99 |
+
}> {
|
| 100 |
+
const response = await fetch('/api/rpc/proposal/start-contents-image-generation', {
|
| 101 |
+
method: 'POST',
|
| 102 |
+
headers: {
|
| 103 |
+
'Content-Type': 'application/json',
|
| 104 |
+
},
|
| 105 |
+
body: JSON.stringify({
|
| 106 |
+
html,
|
| 107 |
+
tabName,
|
| 108 |
+
cnData,
|
| 109 |
+
provider,
|
| 110 |
+
}),
|
| 111 |
+
});
|
| 112 |
+
|
| 113 |
+
if (!response.ok) {
|
| 114 |
+
throw new Error(`画像生成開始に失敗しました: ${response.statusText}`);
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
return response.json();
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
/**
|
| 121 |
+
* 画像生成状況を確認
|
| 122 |
+
*/
|
| 123 |
+
export async function getImageGenerationStatus(batchId: string): Promise<{
|
| 124 |
+
batchId: string;
|
| 125 |
+
status: 'pending' | 'processing' | 'completed' | 'failed';
|
| 126 |
+
jobs: Array<{
|
| 127 |
+
jobId: string;
|
| 128 |
+
imageId: string;
|
| 129 |
+
status: 'pending' | 'generating' | 'completed' | 'failed';
|
| 130 |
+
generatedImageUrl?: string;
|
| 131 |
+
errorMessage?: string;
|
| 132 |
+
}>;
|
| 133 |
+
createdAt: string;
|
| 134 |
+
completedAt?: string;
|
| 135 |
+
} | null> {
|
| 136 |
+
const response = await fetch(`/api/rpc/proposal/image-generation-status/${batchId}`);
|
| 137 |
+
|
| 138 |
+
if (!response.ok) {
|
| 139 |
+
if (response.status === 404) {
|
| 140 |
+
// バッチIDが存在しない場合は null を返す
|
| 141 |
+
return null;
|
| 142 |
+
}
|
| 143 |
+
throw new Error(`ステータス確認に失敗しました: ${response.statusText}`);
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
return response.json();
|
| 147 |
+
}
|
api-client/proposal/html-preview/queries.ts
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createQueryOptions } from '@/api-client/query-config';
|
| 2 |
+
import type { SwotData, SwotDataWithoutSummary } from '@/schema/pox';
|
| 3 |
+
import type { ContentSection, FvData, GenerateContentsHtmlResponse, GenerateFvHtmlResponse } from '@/schema/proposal';
|
| 4 |
+
import type { ThemeExtractionResult } from '@/schema/theme-extraction';
|
| 5 |
+
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
| 6 |
+
import React from 'react';
|
| 7 |
+
import { generateContentsHtml, generateFvHtml, getImageGenerationStatus, startImageGeneration } from './index';
|
| 8 |
+
|
| 9 |
+
// FV提案HTMLを取得するフック(タブごと)
|
| 10 |
+
export function useFvHtmlGeneration(
|
| 11 |
+
tabName: string,
|
| 12 |
+
fvData?: FvData,
|
| 13 |
+
enabled: boolean = false,
|
| 14 |
+
provider?: 'openai' | 'gemini' | 'claude',
|
| 15 |
+
referenceUrl?: string,
|
| 16 |
+
screenshot?: string,
|
| 17 |
+
dummyMode?: boolean,
|
| 18 |
+
swotData?: SwotData | SwotDataWithoutSummary,
|
| 19 |
+
userEmail?: string,
|
| 20 |
+
sourcePage?: string,
|
| 21 |
+
forceFallbackMode?: boolean,
|
| 22 |
+
themeData?: ThemeExtractionResult | null,
|
| 23 |
+
) {
|
| 24 |
+
// パラメータから安定したハッシュを生成
|
| 25 |
+
const generateParamsHash = () => {
|
| 26 |
+
if (!fvData) return 'no-fv-data';
|
| 27 |
+
|
| 28 |
+
// FVデータの主要パラメータからハッシュを生成(実際の構造に合わせる)
|
| 29 |
+
const relevantParams = {
|
| 30 |
+
strategy: fvData.strategy,
|
| 31 |
+
need: fvData.need,
|
| 32 |
+
// FVデータの主要部分
|
| 33 |
+
mainCopy: fvData.data?.fv?.コピー?.メインコピー || '',
|
| 34 |
+
subCopy1: fvData.data?.fv?.コピー?.サブコピー1 || '',
|
| 35 |
+
subCopy2: fvData.data?.fv?.コピー?.サブコピー2 || '',
|
| 36 |
+
subCopy3: fvData.data?.fv?.コピー?.サブコピー3 || '',
|
| 37 |
+
ctaButton: fvData.data?.fv?.CTA?.ボタンテキスト || '',
|
| 38 |
+
microCopy: fvData.data?.fv?.CTA?.マイクロコピー || '',
|
| 39 |
+
visualCreationInstruction: fvData.data?.fv?.ビジュアル?.作成指示 || '',
|
| 40 |
+
authorityCreationInstruction: fvData.data?.fv?.権威付け?.作成指示 || '',
|
| 41 |
+
fvCreationIntent: fvData.fvCreationIntent || '',
|
| 42 |
+
// SWOTデータの主要部分
|
| 43 |
+
swotKeys: swotData ? Object.keys(swotData).sort().join('_') : '',
|
| 44 |
+
swotSample: swotData ? `${swotData['業界'] || ''}_${swotData['商材'] || ''}_${swotData['強み'] || ''}_${swotData['弱み'] || ''}` : '',
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
+
const str = JSON.stringify(relevantParams);
|
| 48 |
+
let hash = 0;
|
| 49 |
+
for (let i = 0; i < str.length; i++) {
|
| 50 |
+
hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0;
|
| 51 |
+
}
|
| 52 |
+
return Math.abs(hash).toString(36);
|
| 53 |
+
};
|
| 54 |
+
|
| 55 |
+
const paramsHash = generateParamsHash();
|
| 56 |
+
const fvDataKey = `${tabName}_${provider || 'default'}_${dummyMode ? 'dummy' : 'live'}_${paramsHash}`;
|
| 57 |
+
|
| 58 |
+
// 前回のキーを保存するためのRef
|
| 59 |
+
const prevKeyRef = React.useRef<string>('');
|
| 60 |
+
|
| 61 |
+
// 現在のqueryKeyを生成
|
| 62 |
+
// 画像再生成を防ぐため、themeDataはクエリキーに含めない
|
| 63 |
+
const currentQueryKey = ['fvHtml', tabName, fvDataKey, provider, referenceUrl, dummyMode];
|
| 64 |
+
|
| 65 |
+
return useQuery<GenerateFvHtmlResponse>({
|
| 66 |
+
...createQueryOptions<GenerateFvHtmlResponse>(),
|
| 67 |
+
queryKey: currentQueryKey,
|
| 68 |
+
queryFn: () => {
|
| 69 |
+
if (!fvData) {
|
| 70 |
+
throw new Error('fvData is required');
|
| 71 |
+
}
|
| 72 |
+
return generateFvHtml({
|
| 73 |
+
tabName,
|
| 74 |
+
fvData,
|
| 75 |
+
provider,
|
| 76 |
+
referenceUrl,
|
| 77 |
+
screenshot,
|
| 78 |
+
dummyMode,
|
| 79 |
+
swotData,
|
| 80 |
+
userEmail,
|
| 81 |
+
sourcePage,
|
| 82 |
+
forceFallbackMode,
|
| 83 |
+
themeData: null, // テーマはCSSのみで適用するため、画像生成時には渡さない
|
| 84 |
+
});
|
| 85 |
+
},
|
| 86 |
+
enabled: enabled && !!fvData,
|
| 87 |
+
});
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
// コンテンツ提案HTMLを取得するフック(タブごと)
|
| 91 |
+
export function useContentsHtmlGeneration(
|
| 92 |
+
tabName: string,
|
| 93 |
+
cnData?: ContentSection[],
|
| 94 |
+
enabled: boolean = false,
|
| 95 |
+
provider?: 'openai' | 'gemini' | 'claude',
|
| 96 |
+
referenceUrl?: string,
|
| 97 |
+
screenshot?: string,
|
| 98 |
+
dummyMode?: boolean,
|
| 99 |
+
swotData?: SwotData | SwotDataWithoutSummary,
|
| 100 |
+
userEmail?: string,
|
| 101 |
+
themeData?: ThemeExtractionResult | null,
|
| 102 |
+
) {
|
| 103 |
+
// パラメータから安定したハッシュを生成
|
| 104 |
+
const generateParamsHash = () => {
|
| 105 |
+
if (!cnData) return 'no-cn-data';
|
| 106 |
+
|
| 107 |
+
// CNデータの主要パラメータからハッシュを生成
|
| 108 |
+
const relevantParams = {
|
| 109 |
+
// CNデータのサンプル(最初の3要素)
|
| 110 |
+
sectionsCount: cnData.length || 0,
|
| 111 |
+
firstSections:
|
| 112 |
+
cnData
|
| 113 |
+
.slice(0, 3)
|
| 114 |
+
.map((s) => s.中区分)
|
| 115 |
+
.join('_') || '',
|
| 116 |
+
// SWOTデータの主要部分
|
| 117 |
+
swotKeys: swotData ? Object.keys(swotData).sort().join('_') : '',
|
| 118 |
+
swotSample: swotData ? `${swotData['業界'] || ''}_${swotData['商材'] || ''}` : '',
|
| 119 |
+
};
|
| 120 |
+
|
| 121 |
+
const str = JSON.stringify(relevantParams);
|
| 122 |
+
let hash = 0;
|
| 123 |
+
for (let i = 0; i < str.length; i++) {
|
| 124 |
+
hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0;
|
| 125 |
+
}
|
| 126 |
+
return Math.abs(hash).toString(36);
|
| 127 |
+
};
|
| 128 |
+
|
| 129 |
+
const paramsHash = generateParamsHash();
|
| 130 |
+
const cnDataKey = `${tabName}_${provider || 'default'}_${dummyMode ? 'dummy' : 'live'}_${paramsHash}`;
|
| 131 |
+
|
| 132 |
+
// 前回のキーを保存するためのRef
|
| 133 |
+
const prevKeyRef = React.useRef<string>('');
|
| 134 |
+
|
| 135 |
+
// 現在のqueryKeyを生成
|
| 136 |
+
// 画像再生成を防ぐため、themeDataはクエリキーに含めない
|
| 137 |
+
const currentQueryKey = ['cnHtml', tabName, cnDataKey, provider, referenceUrl, dummyMode];
|
| 138 |
+
|
| 139 |
+
return useQuery<GenerateContentsHtmlResponse>({
|
| 140 |
+
...createQueryOptions<GenerateContentsHtmlResponse>(),
|
| 141 |
+
queryKey: currentQueryKey,
|
| 142 |
+
queryFn: () => {
|
| 143 |
+
if (!cnData) {
|
| 144 |
+
throw new Error('cnData is required');
|
| 145 |
+
}
|
| 146 |
+
console.log(`[CN HTML] 生成開始: ${tabName}`);
|
| 147 |
+
return generateContentsHtml({
|
| 148 |
+
tabName,
|
| 149 |
+
cnData,
|
| 150 |
+
provider,
|
| 151 |
+
referenceUrl,
|
| 152 |
+
screenshot,
|
| 153 |
+
dummyMode,
|
| 154 |
+
userEmail,
|
| 155 |
+
themeData: null, // テーマはCSSのみで適用するため、画像生成時には渡さない
|
| 156 |
+
});
|
| 157 |
+
},
|
| 158 |
+
enabled: enabled && !!cnData,
|
| 159 |
+
});
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
// 画像生成を別途実行するフック
|
| 163 |
+
export function useContentsImageGeneration(
|
| 164 |
+
tabName: string,
|
| 165 |
+
cnData?: ContentSection[],
|
| 166 |
+
htmlContent?: string,
|
| 167 |
+
enabled: boolean = false,
|
| 168 |
+
provider?: 'openai' | 'gemini' | 'claude',
|
| 169 |
+
) {
|
| 170 |
+
const queryClient = useQueryClient();
|
| 171 |
+
|
| 172 |
+
return useQuery({
|
| 173 |
+
queryKey: ['contentsImageGeneration', tabName, cnData, provider],
|
| 174 |
+
queryFn: async () => {
|
| 175 |
+
if (!cnData || !htmlContent) {
|
| 176 |
+
throw new Error('cnData and htmlContent are required');
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
try {
|
| 180 |
+
const batchResult = await startImageGeneration(
|
| 181 |
+
htmlContent,
|
| 182 |
+
tabName,
|
| 183 |
+
cnData,
|
| 184 |
+
provider === 'openai' || provider === 'claude' ? provider : 'claude',
|
| 185 |
+
);
|
| 186 |
+
|
| 187 |
+
if (batchResult.batchId) {
|
| 188 |
+
// ポーリング処理を開始
|
| 189 |
+
const pollInterval = setInterval(async () => {
|
| 190 |
+
try {
|
| 191 |
+
const status = await getImageGenerationStatus(batchResult.batchId);
|
| 192 |
+
|
| 193 |
+
if (!status) {
|
| 194 |
+
clearInterval(pollInterval);
|
| 195 |
+
return;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
if (status.status === 'completed' && status.jobs) {
|
| 199 |
+
clearInterval(pollInterval);
|
| 200 |
+
|
| 201 |
+
const imageReplacements = status.jobs
|
| 202 |
+
.filter((job) => job.status === 'completed' && job.generatedImageUrl)
|
| 203 |
+
.map((job) => ({
|
| 204 |
+
imageId: job.imageId,
|
| 205 |
+
imageUrl: job.generatedImageUrl || '',
|
| 206 |
+
}));
|
| 207 |
+
|
| 208 |
+
if (imageReplacements.length > 0) {
|
| 209 |
+
let updatedHtml = htmlContent;
|
| 210 |
+
imageReplacements.forEach(({ imageId, imageUrl }) => {
|
| 211 |
+
const regex = new RegExp(`src="placeholder:${imageId}"`, 'g');
|
| 212 |
+
updatedHtml = updatedHtml.replace(regex, `src="${imageUrl}"`);
|
| 213 |
+
});
|
| 214 |
+
|
| 215 |
+
// キャッシュを更新
|
| 216 |
+
const currentData = queryClient.getQueryData<GenerateContentsHtmlResponse>(['contentsHtml', tabName, cnData, provider]);
|
| 217 |
+
|
| 218 |
+
if (currentData) {
|
| 219 |
+
queryClient.setQueryData(['contentsHtml', tabName, cnData, provider], {
|
| 220 |
+
...currentData,
|
| 221 |
+
html: updatedHtml,
|
| 222 |
+
});
|
| 223 |
+
}
|
| 224 |
+
}
|
| 225 |
+
} else if (status.status === 'failed') {
|
| 226 |
+
clearInterval(pollInterval);
|
| 227 |
+
console.error('[Contents Image Batch] Failed:', status);
|
| 228 |
+
}
|
| 229 |
+
} catch (error) {
|
| 230 |
+
console.error('[Contents Image Polling] Error:', error);
|
| 231 |
+
}
|
| 232 |
+
}, 20000); // 20秒ごとにポーリング(負荷軽減)
|
| 233 |
+
|
| 234 |
+
// 最大20分後にタイムアウト(画像生成の実際の処理時間を考慮)
|
| 235 |
+
setTimeout(() => {
|
| 236 |
+
clearInterval(pollInterval);
|
| 237 |
+
console.warn('[Contents Image Polling] Timeout after 20 minutes');
|
| 238 |
+
}, 1200000); // 20分 = 1200秒
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
return batchResult;
|
| 242 |
+
} catch (error) {
|
| 243 |
+
console.error('[Contents Image Batch] Failed to start:', error);
|
| 244 |
+
throw error;
|
| 245 |
+
}
|
| 246 |
+
},
|
| 247 |
+
enabled: enabled && !!cnData && !!htmlContent,
|
| 248 |
+
staleTime: Infinity, // 一度実行したら再実行しない
|
| 249 |
+
});
|
| 250 |
+
}
|
api-client/proposal/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Re-export everything from subdirectories
|
| 2 |
+
export * from './html-preview/index';
|
| 3 |
+
export * from './html-preview/queries';
|
| 4 |
+
export * from './proposal/queries';
|
| 5 |
+
export * from './proposal/translator';
|
| 6 |
+
export * from './screenshot/queries';
|
api-client/proposal/proposal/individual-queries.ts
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createQueryOptions } from '@/api-client/query-config';
|
| 2 |
+
import { rpcClient } from '@/api-client/utils/rpc-client';
|
| 3 |
+
import type { ProposalCnGradioResponse } from '@/schema/gradio-proxy/proposal-cn';
|
| 4 |
+
import type { ProposalFvGradioResponse } from '@/schema/gradio-proxy/proposal-fv';
|
| 5 |
+
import type { ProposalIntentGradioResponse } from '@/schema/gradio-proxy/proposal-intent';
|
| 6 |
+
import type { ProposalPredictionGradioResponse } from '@/schema/gradio-proxy/proposal-prediction';
|
| 7 |
+
import type { ThemeByMomentStrategy } from '@/schema/gradio-proxy/theme-by-moment';
|
| 8 |
+
import { useGlobalStore } from '@/store/global';
|
| 9 |
+
import { useQuery, type UseQueryResult } from '@tanstack/react-query';
|
| 10 |
+
|
| 11 |
+
// 個別のuseQueryフック
|
| 12 |
+
interface BaseProposalParams {
|
| 13 |
+
selectionHash: string;
|
| 14 |
+
userEmail?: string | null;
|
| 15 |
+
dummyMode?: boolean;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
interface ProposalFvParams extends BaseProposalParams {
|
| 19 |
+
strategy_x_moment?: Record<string, unknown>;
|
| 20 |
+
commonDict?: Record<string, unknown>;
|
| 21 |
+
fv_infos?: Record<string, unknown>;
|
| 22 |
+
strategies?: Record<string, unknown>;
|
| 23 |
+
cnall_json?: Record<string, unknown>;
|
| 24 |
+
url_category_scores?: Record<string, unknown>;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
export function useProposalFv(params: ProposalFvParams, enabled: boolean = true): UseQueryResult<ProposalFvGradioResponse> {
|
| 28 |
+
const { selectionHash, dummyMode, ...apiParams } = params;
|
| 29 |
+
const { userIdentifier } = useGlobalStore();
|
| 30 |
+
|
| 31 |
+
return useQuery({
|
| 32 |
+
queryKey: ['proposalFv', selectionHash],
|
| 33 |
+
...createQueryOptions<ProposalFvGradioResponse>({
|
| 34 |
+
queryFn: async () => {
|
| 35 |
+
const response = await rpcClient['gradio-proxy']['proposal-fv'].$post({
|
| 36 |
+
json: {
|
| 37 |
+
strategy_x_moment: apiParams.strategy_x_moment,
|
| 38 |
+
commonDict: apiParams.commonDict,
|
| 39 |
+
fv_infos: apiParams.fv_infos,
|
| 40 |
+
strategies: apiParams.strategies,
|
| 41 |
+
cnall_json: apiParams.cnall_json,
|
| 42 |
+
url_category_scores: apiParams.url_category_scores,
|
| 43 |
+
dummyMode: dummyMode,
|
| 44 |
+
userEmail: apiParams.userEmail ?? undefined,
|
| 45 |
+
userIdentifier,
|
| 46 |
+
},
|
| 47 |
+
});
|
| 48 |
+
|
| 49 |
+
if (!response.ok) {
|
| 50 |
+
console.error('[useProposalFv] Response status:', response.status);
|
| 51 |
+
const errorData = (await response.json()) as { message?: string; status?: string };
|
| 52 |
+
console.error('[useProposalFv] API エラー:', errorData);
|
| 53 |
+
throw new Error(errorData.message || 'proposal_fv APIの呼び出しに失敗しました');
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
const data = await response.json();
|
| 57 |
+
return data as ProposalFvGradioResponse;
|
| 58 |
+
},
|
| 59 |
+
enabled: enabled,
|
| 60 |
+
staleTime: 30 * 60 * 1000, // 30分
|
| 61 |
+
gcTime: 60 * 60 * 1000, // 1時間
|
| 62 |
+
retry: false,
|
| 63 |
+
}),
|
| 64 |
+
});
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
interface ProposalCnParams extends BaseProposalParams {
|
| 68 |
+
strategy_x_moment?: Record<string, unknown>;
|
| 69 |
+
commonDict?: Record<string, unknown>;
|
| 70 |
+
cnall_json?: Record<string, unknown>;
|
| 71 |
+
strategies?: Record<string, unknown>;
|
| 72 |
+
fv_infos?: Record<string, unknown>;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
export function useProposalCn(params: ProposalCnParams, enabled: boolean = true): UseQueryResult<ProposalCnGradioResponse> {
|
| 76 |
+
const { selectionHash, dummyMode, ...apiParams } = params;
|
| 77 |
+
const { userIdentifier } = useGlobalStore();
|
| 78 |
+
|
| 79 |
+
return useQuery({
|
| 80 |
+
queryKey: ['proposalCn', selectionHash],
|
| 81 |
+
...createQueryOptions<ProposalCnGradioResponse>({
|
| 82 |
+
queryFn: async () => {
|
| 83 |
+
const response = await rpcClient['gradio-proxy']['proposal-cn'].$post({
|
| 84 |
+
json: {
|
| 85 |
+
strategy_x_moment: apiParams.strategy_x_moment,
|
| 86 |
+
commonDict: apiParams.commonDict,
|
| 87 |
+
cnall_json: apiParams.cnall_json,
|
| 88 |
+
strategies: apiParams.strategies,
|
| 89 |
+
fv_infos: apiParams.fv_infos,
|
| 90 |
+
dummyMode: dummyMode,
|
| 91 |
+
userEmail: apiParams.userEmail ?? undefined,
|
| 92 |
+
userIdentifier,
|
| 93 |
+
},
|
| 94 |
+
});
|
| 95 |
+
|
| 96 |
+
if (!response.ok) {
|
| 97 |
+
console.error('[useProposalCn] Response status:', response.status);
|
| 98 |
+
const errorData = (await response.json()) as { message?: string; status?: string };
|
| 99 |
+
console.error('[useProposalCn] API エラー:', errorData);
|
| 100 |
+
throw new Error(errorData.message || 'proposal_cn APIの呼び出しに失敗しました');
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
const data = await response.json();
|
| 104 |
+
return data as ProposalCnGradioResponse;
|
| 105 |
+
},
|
| 106 |
+
enabled: enabled,
|
| 107 |
+
staleTime: 30 * 60 * 1000,
|
| 108 |
+
gcTime: 60 * 60 * 1000,
|
| 109 |
+
retry: false,
|
| 110 |
+
}),
|
| 111 |
+
});
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
interface ProposalPredictionParams extends BaseProposalParams {
|
| 115 |
+
proposal_fv?: ProposalFvGradioResponse;
|
| 116 |
+
proposal_cn?: ProposalCnGradioResponse;
|
| 117 |
+
commonDict?: Record<string, unknown>;
|
| 118 |
+
fv_infos?: Record<string, unknown>;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
export function useProposalPrediction(params: ProposalPredictionParams, enabled: boolean = true): UseQueryResult<ProposalPredictionGradioResponse> {
|
| 122 |
+
const { dummyMode, selectionHash, ...apiParams } = params;
|
| 123 |
+
const { userIdentifier } = useGlobalStore();
|
| 124 |
+
|
| 125 |
+
return useQuery({
|
| 126 |
+
// selectionHashのみでキ���ッシュ管理(FV/CNが更新されても同じ選択肢なら再フェッチしない)
|
| 127 |
+
queryKey: ['proposalPrediction', selectionHash],
|
| 128 |
+
...createQueryOptions<ProposalPredictionGradioResponse>({
|
| 129 |
+
queryFn: async () => {
|
| 130 |
+
try {
|
| 131 |
+
const response = await rpcClient['gradio-proxy']['proposal-prediction'].$post({
|
| 132 |
+
json: {
|
| 133 |
+
proposal_fv: apiParams.proposal_fv,
|
| 134 |
+
proposal_cn: apiParams.proposal_cn,
|
| 135 |
+
commonDict: apiParams.commonDict,
|
| 136 |
+
fv_infos: apiParams.fv_infos,
|
| 137 |
+
dummyMode: dummyMode,
|
| 138 |
+
userEmail: apiParams.userEmail ?? undefined,
|
| 139 |
+
userIdentifier,
|
| 140 |
+
},
|
| 141 |
+
});
|
| 142 |
+
|
| 143 |
+
if (!response.ok) {
|
| 144 |
+
console.error('[useProposalPrediction] Response status:', response.status);
|
| 145 |
+
const errorData = (await response.json()) as { message?: string; status?: string };
|
| 146 |
+
console.error('[useProposalPrediction] API エラー:', errorData);
|
| 147 |
+
throw new Error(errorData.message || 'proposal_prediction APIの呼び出しに失敗しました');
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
const data = await response.json();
|
| 151 |
+
return data as ProposalPredictionGradioResponse;
|
| 152 |
+
} catch (error: any) {
|
| 153 |
+
console.error('[useProposalPrediction] Error during fetch or processing:', {
|
| 154 |
+
message: error?.message,
|
| 155 |
+
stack: error?.stack,
|
| 156 |
+
code: error?.code,
|
| 157 |
+
});
|
| 158 |
+
throw error;
|
| 159 |
+
}
|
| 160 |
+
},
|
| 161 |
+
enabled: enabled && !!apiParams.proposal_fv && !!apiParams.proposal_cn,
|
| 162 |
+
staleTime: 30 * 60 * 1000,
|
| 163 |
+
gcTime: 60 * 60 * 1000,
|
| 164 |
+
// デフォルトのリトライ設定を使用(retry: 1)
|
| 165 |
+
// retry: false を削除して、ネットワークエラー等での適切なリトライを許可
|
| 166 |
+
}),
|
| 167 |
+
});
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
interface ProposalIntentParams extends BaseProposalParams {
|
| 171 |
+
proposal_fv?: ProposalFvGradioResponse;
|
| 172 |
+
proposal_cn?: ProposalCnGradioResponse;
|
| 173 |
+
commonDict?: Record<string, unknown>;
|
| 174 |
+
swot?: Record<string, unknown>;
|
| 175 |
+
q_summary?: Record<string, unknown>;
|
| 176 |
+
strategy_x_moment?: Record<string, unknown>;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
export function useProposalIntent(params: ProposalIntentParams, enabled: boolean = true): UseQueryResult<ProposalIntentGradioResponse> {
|
| 180 |
+
const { dummyMode, selectionHash, ...apiParams } = params;
|
| 181 |
+
const { userIdentifier } = useGlobalStore();
|
| 182 |
+
|
| 183 |
+
return useQuery({
|
| 184 |
+
// selectionHashのみでキャッシュ管理(FV/CNが更新されても同じ選択肢なら再フェッチしない)
|
| 185 |
+
queryKey: ['proposalIntent', selectionHash],
|
| 186 |
+
...createQueryOptions<ProposalIntentGradioResponse>({
|
| 187 |
+
queryFn: async () => {
|
| 188 |
+
const response = await rpcClient['gradio-proxy']['proposal-intent'].$post({
|
| 189 |
+
json: {
|
| 190 |
+
proposal_fv: apiParams.proposal_fv,
|
| 191 |
+
proposal_cn: apiParams.proposal_cn,
|
| 192 |
+
commonDict: apiParams.commonDict,
|
| 193 |
+
swot: apiParams.swot,
|
| 194 |
+
q_summary: apiParams.q_summary,
|
| 195 |
+
strategy_x_moment: apiParams.strategy_x_moment as ThemeByMomentStrategy | undefined,
|
| 196 |
+
dummyMode: dummyMode,
|
| 197 |
+
userEmail: apiParams.userEmail ?? undefined,
|
| 198 |
+
userIdentifier,
|
| 199 |
+
},
|
| 200 |
+
});
|
| 201 |
+
|
| 202 |
+
if (!response.ok) {
|
| 203 |
+
console.error('[useProposalIntent] Response status:', response.status);
|
| 204 |
+
const errorData = (await response.json()) as { message?: string; status?: string };
|
| 205 |
+
console.error('[useProposalIntent] API エラー:', errorData);
|
| 206 |
+
throw new Error(errorData.message || 'proposal_intent APIの呼び出しに失敗しました');
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
const data = await response.json();
|
| 210 |
+
return data as ProposalIntentGradioResponse;
|
| 211 |
+
},
|
| 212 |
+
enabled: enabled && !!apiParams.proposal_fv && !!apiParams.proposal_cn,
|
| 213 |
+
staleTime: 30 * 60 * 1000,
|
| 214 |
+
gcTime: 60 * 60 * 1000,
|
| 215 |
+
retry: false, // デバッグのためリトライを無効化
|
| 216 |
+
refetchOnWindowFocus: false, // ウィンドウフォーカス時の再フェッチを無効化
|
| 217 |
+
}),
|
| 218 |
+
});
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
/**
|
| 222 |
+
* useThemeByMomentも個別フック化
|
| 223 |
+
*/
|
| 224 |
+
interface ThemeByMomentParams extends BaseProposalParams {
|
| 225 |
+
commonDict?: Record<string, unknown>;
|
| 226 |
+
scoreDict?: Record<string, unknown>;
|
| 227 |
+
swot?: Record<string, unknown>;
|
| 228 |
+
strategies?: Record<string, unknown>;
|
| 229 |
+
fv_infos?: Record<string, unknown>;
|
| 230 |
+
moments?: string[];
|
| 231 |
+
own_theme?: string;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
export function useThemeByMomentOptimized(params: ThemeByMomentParams, enabled: boolean = true): UseQueryResult<unknown[]> {
|
| 235 |
+
const { selectionHash, dummyMode, userEmail, ...apiParams } = params;
|
| 236 |
+
const { userIdentifier } = useGlobalStore();
|
| 237 |
+
|
| 238 |
+
const queryResult = useQuery({
|
| 239 |
+
queryKey: ['themeByMoment', selectionHash],
|
| 240 |
+
...createQueryOptions<unknown[]>({
|
| 241 |
+
queryFn: async () => {
|
| 242 |
+
// 通常モードで必要なデータが不足している場合はエラーをthrow
|
| 243 |
+
if (!dummyMode) {
|
| 244 |
+
if (!apiParams.commonDict || Object.keys(apiParams.commonDict).length === 0) {
|
| 245 |
+
console.error('[useThemeByMoment] ERROR: commonDict is missing in normal mode');
|
| 246 |
+
throw new Error('分析データ(commonDict)が設定されていません。詳細画面から正しく遷移してください。');
|
| 247 |
+
}
|
| 248 |
+
if (!apiParams.scoreDict || Object.keys(apiParams.scoreDict).length === 0) {
|
| 249 |
+
console.error('[useThemeByMoment] ERROR: scoreDict is missing in normal mode');
|
| 250 |
+
throw new Error('スコアデータ(scoreDict)が設定されていません。詳細画面から正しく遷移してください。');
|
| 251 |
+
}
|
| 252 |
+
if (!apiParams.moments || apiParams.moments.length === 0) {
|
| 253 |
+
console.error('[useThemeByMoment] ERROR: moments is empty in normal mode');
|
| 254 |
+
throw new Error('改善案の方向性が選択されていません。');
|
| 255 |
+
}
|
| 256 |
+
// swotData78 (strategies)は必須ではないが、警告を出す
|
| 257 |
+
if (!apiParams.strategies) {
|
| 258 |
+
console.warn('[useThemeByMoment] WARNING: strategies (swotData78) is missing, using empty object');
|
| 259 |
+
}
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
const response = await rpcClient['gradio-proxy']['theme-by-moment'].$post({
|
| 263 |
+
json: {
|
| 264 |
+
commonDict: apiParams.commonDict || {},
|
| 265 |
+
scoreDict: apiParams.scoreDict || {},
|
| 266 |
+
swot: apiParams.swot || {},
|
| 267 |
+
strategies: apiParams.strategies || {},
|
| 268 |
+
moments: apiParams.moments || [],
|
| 269 |
+
fv_infos: apiParams.fv_infos,
|
| 270 |
+
own_theme: apiParams.own_theme || '',
|
| 271 |
+
dummyMode: dummyMode,
|
| 272 |
+
userEmail: userEmail || '',
|
| 273 |
+
userIdentifier,
|
| 274 |
+
},
|
| 275 |
+
});
|
| 276 |
+
|
| 277 |
+
if (!response.ok) {
|
| 278 |
+
console.error('[useThemeByMoment] Response status:', response.status);
|
| 279 |
+
const errorData = (await response.json()) as { message?: string; status?: string };
|
| 280 |
+
console.error('[useThemeByMoment] API エラー:', errorData);
|
| 281 |
+
throw new Error(errorData.message || 'theme_by_moment APIの呼び出しに失敗しました');
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
const data = await response.json();
|
| 285 |
+
return data as unknown[];
|
| 286 |
+
},
|
| 287 |
+
enabled: enabled && (dummyMode || (!!apiParams.moments && apiParams.moments.length > 0)),
|
| 288 |
+
staleTime: 30 * 60 * 1000,
|
| 289 |
+
gcTime: 60 * 60 * 1000,
|
| 290 |
+
retry: false,
|
| 291 |
+
}),
|
| 292 |
+
});
|
| 293 |
+
|
| 294 |
+
// デバッグ用ログ
|
| 295 |
+
if (queryResult.isError) {
|
| 296 |
+
console.error('[useThemeByMoment] Query is in error state:', {
|
| 297 |
+
error: queryResult.error,
|
| 298 |
+
errorMessage: queryResult.error?.message,
|
| 299 |
+
});
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
return queryResult;
|
| 303 |
+
}
|
api-client/proposal/proposal/optimized-queries.ts
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ProposalTranslator } from '@/api-client/proposal/proposal/translator';
|
| 2 |
+
import type { ProposalCnGradioResponse } from '@/schema/gradio-proxy/proposal-cn';
|
| 3 |
+
import type { ProposalFvGradioResponse } from '@/schema/gradio-proxy/proposal-fv';
|
| 4 |
+
import type { ProposalIntentGradioResponse } from '@/schema/gradio-proxy/proposal-intent';
|
| 5 |
+
import type { ProposalPredictionGradioResponse } from '@/schema/gradio-proxy/proposal-prediction';
|
| 6 |
+
import type { ThemeByMomentStrategy } from '@/schema/gradio-proxy/theme-by-moment';
|
| 7 |
+
import type { ExtendedProposalTabsResponse } from '@/schema/proposal';
|
| 8 |
+
import { useGlobalStore } from '@/store/global';
|
| 9 |
+
import { useInputStore } from '@/store/input';
|
| 10 |
+
import { generateSelectionHash } from '@/store/proposal-trigger';
|
| 11 |
+
import { useResultStore } from '@/store/result';
|
| 12 |
+
import { useUserStore } from '@/store/user';
|
| 13 |
+
import { useQueryClient } from '@tanstack/react-query';
|
| 14 |
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
| 15 |
+
import { useProposalCn, useProposalFv, useProposalIntent, useProposalPrediction, useThemeByMomentOptimized } from './individual-queries';
|
| 16 |
+
|
| 17 |
+
interface UseProposalTabsOptimizedParams {
|
| 18 |
+
// 外部から指定可能なパラメータ
|
| 19 |
+
strategy_x_moment?: ThemeByMomentStrategy;
|
| 20 |
+
enabled?: boolean;
|
| 21 |
+
dummyMode?: boolean;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
export function useProposalTabsOptimized(params: UseProposalTabsOptimizedParams = {}) {
|
| 25 |
+
const { strategy_x_moment: externalStrategyXMoment, enabled: externalEnabled = true, dummyMode: paramDummyMode } = params;
|
| 26 |
+
|
| 27 |
+
// Store からの必要なデータを取得
|
| 28 |
+
const { dummyMode: storeDummyMode } = useGlobalStore();
|
| 29 |
+
const { fvInfos, selectedMoments, cnInfo } = useInputStore();
|
| 30 |
+
const { commonDict, scoreDict, swotData75, swotData78, summaryData, urlCategoryScores } = useResultStore();
|
| 31 |
+
const { user } = useUserStore();
|
| 32 |
+
const queryClient = useQueryClient();
|
| 33 |
+
const prevContentHashRef = useRef<string>('');
|
| 34 |
+
|
| 35 |
+
// API実行時間追跡用
|
| 36 |
+
const apiStartTimesRef = useRef<Record<string, number>>({});
|
| 37 |
+
const [apiStatuses, setApiStatuses] = useState<Record<string, { duration: number; status: 'loading' | 'success' | 'error' }>>({});
|
| 38 |
+
|
| 39 |
+
// ダミーモードの判定
|
| 40 |
+
const isDummyMode = paramDummyMode !== undefined ? paramDummyMode : storeDummyMode;
|
| 41 |
+
|
| 42 |
+
// ダミーモード用のデータ
|
| 43 |
+
const dummyCommonDict = {
|
| 44 |
+
dummy: 'data',
|
| 45 |
+
regulation: 'test',
|
| 46 |
+
mode: true,
|
| 47 |
+
};
|
| 48 |
+
const effectiveCommonDict = isDummyMode ? commonDict || dummyCommonDict : commonDict;
|
| 49 |
+
|
| 50 |
+
// コンテンツデータからハッシュを生成(キャッシュキーとして使用)
|
| 51 |
+
// selectedMomentsも含めて、選択肢が変わった場合に新しいキャッシュキーを生成
|
| 52 |
+
const contentData = useMemo(
|
| 53 |
+
() => ({
|
| 54 |
+
fvInfos: fvInfos ? JSON.parse(JSON.stringify(fvInfos)) : null, // ディープコピーで参照を除去
|
| 55 |
+
cnInfo: cnInfo ? JSON.parse(JSON.stringify(cnInfo)) : null, // ディープコピーで参照を除去
|
| 56 |
+
selectedMoments: selectedMoments ? [...selectedMoments] : [], // 選択肢も含める
|
| 57 |
+
}),
|
| 58 |
+
[fvInfos, cnInfo, selectedMoments],
|
| 59 |
+
);
|
| 60 |
+
|
| 61 |
+
const contentHash = useMemo(() => generateSelectionHash(contentData), [contentData]);
|
| 62 |
+
|
| 63 |
+
// キャッシュキーの変更を追跡
|
| 64 |
+
useEffect(() => {
|
| 65 |
+
if (prevContentHashRef.current !== contentHash) {
|
| 66 |
+
if (prevContentHashRef.current) {
|
| 67 |
+
console.log('[useProposalTabsOptimized] ⚠️ Content hash changed - will trigger new queries', {
|
| 68 |
+
oldHash: prevContentHashRef.current.substring(0, 8),
|
| 69 |
+
newHash: contentHash.substring(0, 8),
|
| 70 |
+
selectedMoments,
|
| 71 |
+
fvInfos: !!fvInfos,
|
| 72 |
+
cnInfo: !!cnInfo,
|
| 73 |
+
});
|
| 74 |
+
}
|
| 75 |
+
prevContentHashRef.current = contentHash;
|
| 76 |
+
}
|
| 77 |
+
}, [contentHash, selectedMoments, fvInfos, cnInfo, externalEnabled, isDummyMode]);
|
| 78 |
+
|
| 79 |
+
// コンポーネントのアンマウント時にクエリをキャンセル
|
| 80 |
+
useEffect(() => {
|
| 81 |
+
return () => {
|
| 82 |
+
// 現在実行中のクエリをキャンセル
|
| 83 |
+
if (contentHash) {
|
| 84 |
+
const currentKeys = [
|
| 85 |
+
['themeByMoment', contentHash],
|
| 86 |
+
['proposalFv', contentHash],
|
| 87 |
+
['proposalCn', contentHash],
|
| 88 |
+
['proposalPrediction', contentHash],
|
| 89 |
+
['proposalIntent', contentHash],
|
| 90 |
+
];
|
| 91 |
+
|
| 92 |
+
currentKeys.forEach((key) => {
|
| 93 |
+
queryClient.cancelQueries({ queryKey: key });
|
| 94 |
+
});
|
| 95 |
+
}
|
| 96 |
+
};
|
| 97 |
+
}, [contentHash, queryClient]);
|
| 98 |
+
|
| 99 |
+
// 基本パラメータ
|
| 100 |
+
const baseParams = {
|
| 101 |
+
selectionHash: contentHash, // 内部的にはselectionHashという名前を維持(API互換性のため)
|
| 102 |
+
userEmail: user?.user?.email || null,
|
| 103 |
+
dummyMode: isDummyMode,
|
| 104 |
+
};
|
| 105 |
+
|
| 106 |
+
// Step 1: ThemeByMoment(ダミーモードでも実行)
|
| 107 |
+
const themeByMoment = useThemeByMomentOptimized(
|
| 108 |
+
{
|
| 109 |
+
...baseParams,
|
| 110 |
+
commonDict: effectiveCommonDict || undefined,
|
| 111 |
+
scoreDict: scoreDict || undefined,
|
| 112 |
+
swot: swotData75 || undefined,
|
| 113 |
+
strategies: swotData78 || {}, // undefinedの���合は空オブジェクトを渡す
|
| 114 |
+
fv_infos: fvInfos as Record<string, unknown> | undefined,
|
| 115 |
+
moments: selectedMoments,
|
| 116 |
+
own_theme: '', // RefreshMomentから取得する必要がある場合は追加
|
| 117 |
+
},
|
| 118 |
+
externalEnabled && (isDummyMode || (!isDummyMode && selectedMoments.length > 0)),
|
| 119 |
+
);
|
| 120 |
+
|
| 121 |
+
// strategy_x_moment の決定
|
| 122 |
+
const strategy_x_moment = useMemo((): ThemeByMomentStrategy | null => {
|
| 123 |
+
if (externalStrategyXMoment) {
|
| 124 |
+
return externalStrategyXMoment;
|
| 125 |
+
}
|
| 126 |
+
// themeByMomentの結果から取得(ダミーモードも含む)
|
| 127 |
+
const result =
|
| 128 |
+
themeByMoment.data && Array.isArray(themeByMoment.data) && themeByMoment.data.length > 2
|
| 129 |
+
? (themeByMoment.data[2] as ThemeByMomentStrategy)
|
| 130 |
+
: null;
|
| 131 |
+
|
| 132 |
+
return result;
|
| 133 |
+
}, [externalStrategyXMoment, themeByMoment.data]);
|
| 134 |
+
|
| 135 |
+
// APIステータス追跡用のヘルパー関数
|
| 136 |
+
const updateApiStatus = useCallback((apiName: string, status: 'loading' | 'success' | 'error') => {
|
| 137 |
+
if (status === 'loading') {
|
| 138 |
+
apiStartTimesRef.current[apiName] = Date.now();
|
| 139 |
+
setApiStatuses((prev) => ({
|
| 140 |
+
...prev,
|
| 141 |
+
[apiName]: { duration: 0, status: 'loading' },
|
| 142 |
+
}));
|
| 143 |
+
} else {
|
| 144 |
+
const startTime = apiStartTimesRef.current[apiName];
|
| 145 |
+
const duration = startTime ? (Date.now() - startTime) / 1000 : 0;
|
| 146 |
+
setApiStatuses((prev) => ({
|
| 147 |
+
...prev,
|
| 148 |
+
[apiName]: { duration, status },
|
| 149 |
+
}));
|
| 150 |
+
}
|
| 151 |
+
}, []);
|
| 152 |
+
|
| 153 |
+
// Step 2: ProposalFv & ProposalCn(並列実行)
|
| 154 |
+
const hasStrategyXMoment = !!strategy_x_moment && Object.keys(strategy_x_moment).length > 0;
|
| 155 |
+
const canStartFvCn = isDummyMode || (hasStrategyXMoment && !themeByMoment.isLoading);
|
| 156 |
+
|
| 157 |
+
const proposalFv = useProposalFv(
|
| 158 |
+
{
|
| 159 |
+
...baseParams,
|
| 160 |
+
strategy_x_moment: strategy_x_moment || undefined,
|
| 161 |
+
commonDict: effectiveCommonDict || undefined,
|
| 162 |
+
fv_infos: (fvInfos as Record<string, unknown>) || undefined,
|
| 163 |
+
strategies: swotData78 || {}, // undefinedの場合は空オブジェクトを渡す
|
| 164 |
+
cnall_json: (cnInfo as Record<string, unknown>) || undefined,
|
| 165 |
+
url_category_scores: urlCategoryScores || undefined,
|
| 166 |
+
},
|
| 167 |
+
externalEnabled && canStartFvCn,
|
| 168 |
+
);
|
| 169 |
+
|
| 170 |
+
const proposalCn = useProposalCn(
|
| 171 |
+
{
|
| 172 |
+
...baseParams,
|
| 173 |
+
strategy_x_moment: strategy_x_moment || undefined,
|
| 174 |
+
commonDict: effectiveCommonDict || undefined,
|
| 175 |
+
cnall_json: (cnInfo as Record<string, unknown>) || undefined,
|
| 176 |
+
strategies: swotData78 || {}, // undefinedの場合は空オブジェクトを渡す
|
| 177 |
+
fv_infos: (fvInfos as Record<string, unknown>) || undefined,
|
| 178 |
+
},
|
| 179 |
+
externalEnabled && canStartFvCn,
|
| 180 |
+
);
|
| 181 |
+
|
| 182 |
+
// Step 3: ProposalPrediction & ProposalIntent(FV/CN完了後)
|
| 183 |
+
// シンプルに、FV/CNが成功したらPrediction/Intentを実行
|
| 184 |
+
const canStartPredictionIntent = proposalFv.isSuccess && proposalCn.isSuccess;
|
| 185 |
+
|
| 186 |
+
const proposalPrediction = useProposalPrediction(
|
| 187 |
+
{
|
| 188 |
+
...baseParams,
|
| 189 |
+
proposal_fv: proposalFv.data as ProposalFvGradioResponse,
|
| 190 |
+
proposal_cn: proposalCn.data as ProposalCnGradioResponse,
|
| 191 |
+
commonDict: effectiveCommonDict || undefined,
|
| 192 |
+
fv_infos: (fvInfos as Record<string, unknown>) || undefined,
|
| 193 |
+
},
|
| 194 |
+
externalEnabled && canStartPredictionIntent,
|
| 195 |
+
);
|
| 196 |
+
|
| 197 |
+
const proposalIntent = useProposalIntent(
|
| 198 |
+
{
|
| 199 |
+
...baseParams,
|
| 200 |
+
proposal_fv: proposalFv.data as ProposalFvGradioResponse,
|
| 201 |
+
proposal_cn: proposalCn.data as ProposalCnGradioResponse,
|
| 202 |
+
commonDict: effectiveCommonDict || undefined,
|
| 203 |
+
swot: swotData75 || undefined,
|
| 204 |
+
q_summary: summaryData || undefined,
|
| 205 |
+
strategy_x_moment: strategy_x_moment || undefined,
|
| 206 |
+
},
|
| 207 |
+
externalEnabled && canStartPredictionIntent,
|
| 208 |
+
);
|
| 209 |
+
|
| 210 |
+
// 各APIのステータスを追跡
|
| 211 |
+
useEffect(() => {
|
| 212 |
+
if (themeByMoment.isLoading) updateApiStatus('themeByMoment', 'loading');
|
| 213 |
+
else if (themeByMoment.isSuccess) updateApiStatus('themeByMoment', 'success');
|
| 214 |
+
else if (themeByMoment.isError) updateApiStatus('themeByMoment', 'error');
|
| 215 |
+
}, [themeByMoment.isLoading, themeByMoment.isSuccess, themeByMoment.isError, updateApiStatus]);
|
| 216 |
+
|
| 217 |
+
useEffect(() => {
|
| 218 |
+
if (proposalFv.isLoading) updateApiStatus('proposalFv', 'loading');
|
| 219 |
+
else if (proposalFv.isSuccess) updateApiStatus('proposalFv', 'success');
|
| 220 |
+
else if (proposalFv.isError) updateApiStatus('proposalFv', 'error');
|
| 221 |
+
}, [proposalFv.isLoading, proposalFv.isSuccess, proposalFv.isError, updateApiStatus]);
|
| 222 |
+
|
| 223 |
+
useEffect(() => {
|
| 224 |
+
if (proposalCn.isLoading) updateApiStatus('proposalCn', 'loading');
|
| 225 |
+
else if (proposalCn.isSuccess) updateApiStatus('proposalCn', 'success');
|
| 226 |
+
else if (proposalCn.isError) updateApiStatus('proposalCn', 'error');
|
| 227 |
+
}, [proposalCn.isLoading, proposalCn.isSuccess, proposalCn.isError, updateApiStatus]);
|
| 228 |
+
|
| 229 |
+
useEffect(() => {
|
| 230 |
+
if (proposalPrediction.isLoading) updateApiStatus('proposalPrediction', 'loading');
|
| 231 |
+
else if (proposalPrediction.isSuccess) updateApiStatus('proposalPrediction', 'success');
|
| 232 |
+
else if (proposalPrediction.isError) updateApiStatus('proposalPrediction', 'error');
|
| 233 |
+
}, [proposalPrediction.isLoading, proposalPrediction.isSuccess, proposalPrediction.isError, updateApiStatus]);
|
| 234 |
+
|
| 235 |
+
useEffect(() => {
|
| 236 |
+
if (proposalIntent.isLoading) updateApiStatus('proposalIntent', 'loading');
|
| 237 |
+
else if (proposalIntent.isSuccess) updateApiStatus('proposalIntent', 'success');
|
| 238 |
+
else if (proposalIntent.isError) {
|
| 239 |
+
updateApiStatus('proposalIntent', 'error');
|
| 240 |
+
// エラーの詳細をログ出力(開発環境のみ)
|
| 241 |
+
if (process.env.NODE_ENV === 'development') {
|
| 242 |
+
console.error('[useProposalTabsOptimized] proposalIntent error:', proposalIntent.error);
|
| 243 |
+
}
|
| 244 |
+
}
|
| 245 |
+
}, [proposalIntent.isLoading, proposalIntent.isSuccess, proposalIntent.isError, proposalIntent.error, updateApiStatus]);
|
| 246 |
+
|
| 247 |
+
// データの変換(シリアライズ可能なオブジェクトのみを返すように安全に処理)
|
| 248 |
+
const translatedData = useMemo(() => {
|
| 249 |
+
if (!proposalFv.data || !proposalCn.data || !proposalIntent.data || !proposalPrediction.data) {
|
| 250 |
+
return null;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
try {
|
| 254 |
+
const result = ProposalTranslator.translateToTabsData(
|
| 255 |
+
proposalFv.data as ProposalFvGradioResponse,
|
| 256 |
+
proposalCn.data as ProposalCnGradioResponse,
|
| 257 |
+
proposalIntent.data as ProposalIntentGradioResponse,
|
| 258 |
+
proposalPrediction.data as ProposalPredictionGradioResponse,
|
| 259 |
+
strategy_x_moment || undefined,
|
| 260 |
+
);
|
| 261 |
+
|
| 262 |
+
return result;
|
| 263 |
+
} catch (error) {
|
| 264 |
+
if (process.env.NODE_ENV === 'development') {
|
| 265 |
+
console.error('[useProposalTabsOptimized] Translation error:', error);
|
| 266 |
+
}
|
| 267 |
+
return null;
|
| 268 |
+
}
|
| 269 |
+
}, [proposalFv.data, proposalCn.data, proposalIntent.data, proposalPrediction.data, strategy_x_moment]);
|
| 270 |
+
|
| 271 |
+
// 統合されたローディング状態
|
| 272 |
+
const isLoading =
|
| 273 |
+
themeByMoment.isLoading || proposalFv.isLoading || proposalCn.isLoading || proposalPrediction.isLoading || proposalIntent.isLoading;
|
| 274 |
+
|
| 275 |
+
// 統合されたエラー状態
|
| 276 |
+
const isError = themeByMoment.isError || proposalFv.isError || proposalCn.isError || proposalPrediction.isError || proposalIntent.isError;
|
| 277 |
+
|
| 278 |
+
// エラーメッセージの取得
|
| 279 |
+
const getErrorMessage = useCallback(() => {
|
| 280 |
+
if (themeByMoment.isError) {
|
| 281 |
+
// themeByMomentのエラーメッセージを優先的に返す
|
| 282 |
+
const errorMsg = themeByMoment.error?.message || 'テーマ取得に失敗しました';
|
| 283 |
+
return errorMsg;
|
| 284 |
+
}
|
| 285 |
+
if (proposalFv.isError) return 'FV提案の取得に失敗しました';
|
| 286 |
+
if (proposalCn.isError) return 'コンテンツ提案の取得に失敗しました';
|
| 287 |
+
if (proposalPrediction.isError) return '予測データの取得に失敗しました';
|
| 288 |
+
if (proposalIntent.isError) return 'インテント提案の取得に失敗しました';
|
| 289 |
+
return 'データの取得に失敗しました';
|
| 290 |
+
}, [
|
| 291 |
+
themeByMoment.isError,
|
| 292 |
+
themeByMoment.error?.message,
|
| 293 |
+
proposalFv.isError,
|
| 294 |
+
proposalCn.isError,
|
| 295 |
+
proposalPrediction.isError,
|
| 296 |
+
proposalIntent.isError,
|
| 297 |
+
]);
|
| 298 |
+
|
| 299 |
+
// 個別の手動リトライ関数
|
| 300 |
+
const refetchFailed = () => {
|
| 301 |
+
if (themeByMoment.isError) themeByMoment.refetch();
|
| 302 |
+
if (proposalFv.isError) proposalFv.refetch();
|
| 303 |
+
if (proposalCn.isError) proposalCn.refetch();
|
| 304 |
+
if (proposalPrediction.isError) proposalPrediction.refetch();
|
| 305 |
+
if (proposalIntent.isError) proposalIntent.refetch();
|
| 306 |
+
};
|
| 307 |
+
|
| 308 |
+
// 安全にシリアライズ可能なデータのみを返す
|
| 309 |
+
const safeRawData = useMemo(() => {
|
| 310 |
+
if (!proposalFv.data || !proposalCn.data || !proposalIntent.data || !proposalPrediction.data) {
|
| 311 |
+
return undefined;
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
try {
|
| 315 |
+
// React Queryからの返り値を確実にシリアライズして非シリアライズ可能オブジェクトを除去
|
| 316 |
+
const serializedData = {
|
| 317 |
+
proposal_fv: proposalFv.data,
|
| 318 |
+
proposal_cn: proposalCn.data,
|
| 319 |
+
proposal_intent: proposalIntent.data,
|
| 320 |
+
proposal_prediction: proposalPrediction.data,
|
| 321 |
+
strategy_x_moment: strategy_x_moment || undefined,
|
| 322 |
+
};
|
| 323 |
+
|
| 324 |
+
// JSON.parse(JSON.stringify())でシリアライズ
|
| 325 |
+
return JSON.parse(JSON.stringify(serializedData));
|
| 326 |
+
} catch (error) {
|
| 327 |
+
if (process.env.NODE_ENV === 'development') {
|
| 328 |
+
console.error('[useProposalTabsOptimized] Serialization error in safeRawData:', error);
|
| 329 |
+
}
|
| 330 |
+
return undefined;
|
| 331 |
+
}
|
| 332 |
+
}, [proposalFv.data, proposalCn.data, proposalIntent.data, proposalPrediction.data, strategy_x_moment]);
|
| 333 |
+
|
| 334 |
+
// 返却値の構造は既存のuseProposalTabsと互換性を保つ
|
| 335 |
+
const result: ExtendedProposalTabsResponse | undefined = translatedData
|
| 336 |
+
? {
|
| 337 |
+
data: translatedData,
|
| 338 |
+
rawData: safeRawData,
|
| 339 |
+
}
|
| 340 |
+
: undefined;
|
| 341 |
+
|
| 342 |
+
// 最終的な返り値もシリアライズして確実に安全にする
|
| 343 |
+
const serializedResult = useMemo(() => {
|
| 344 |
+
if (!result) return undefined;
|
| 345 |
+
|
| 346 |
+
try {
|
| 347 |
+
return JSON.parse(
|
| 348 |
+
JSON.stringify({
|
| 349 |
+
data: result.data,
|
| 350 |
+
rawData: result.rawData,
|
| 351 |
+
}),
|
| 352 |
+
);
|
| 353 |
+
} catch (error) {
|
| 354 |
+
if (process.env.NODE_ENV === 'development') {
|
| 355 |
+
console.error('[useProposalTabsOptimized] Final serialization error:', error);
|
| 356 |
+
}
|
| 357 |
+
return undefined;
|
| 358 |
+
}
|
| 359 |
+
}, [result]);
|
| 360 |
+
|
| 361 |
+
return {
|
| 362 |
+
data: serializedResult?.data,
|
| 363 |
+
rawData: serializedResult?.rawData,
|
| 364 |
+
isLoading,
|
| 365 |
+
isError,
|
| 366 |
+
error: isError ? new Error(getErrorMessage()) : null,
|
| 367 |
+
refetch: refetchFailed,
|
| 368 |
+
apiStatuses, // API実行時間とステータス情報
|
| 369 |
+
selectionHash: contentHash, // コンテンツハッシュを外部で利用可能にする(互換性のためselectionHashという名前で公開)
|
| 370 |
+
// queries オブジェクトを削除(React Queryの内部オブジェクトを含みシリアライズできない)
|
| 371 |
+
};
|
| 372 |
+
}
|
api-client/proposal/proposal/queries.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createQueryOptions } from '@/api-client/query-config';
|
| 2 |
+
import { getThemeByMoment } from '@/services/gradio-with-dummy';
|
| 3 |
+
import { useGlobalStore } from '@/store/global';
|
| 4 |
+
import { useQuery } from '@tanstack/react-query';
|
| 5 |
+
|
| 6 |
+
// useProposalTabs関数は削除されました。
|
| 7 |
+
// useProposalTabsOptimizedを使用してください。
|
| 8 |
+
|
| 9 |
+
interface UseThemeByMomentParams {
|
| 10 |
+
commonDict?: Record<string, unknown>;
|
| 11 |
+
scoreDict?: Record<string, unknown>;
|
| 12 |
+
swot?: Record<string, unknown>;
|
| 13 |
+
strategies?: Record<string, unknown>;
|
| 14 |
+
moments?: string[];
|
| 15 |
+
fv_infos?: Record<string, unknown>;
|
| 16 |
+
own_theme?: string;
|
| 17 |
+
userEmail?: string;
|
| 18 |
+
enabled?: boolean;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
/**
|
| 22 |
+
* getThemeByMomentをReact Queryでキャッシュ化するフック
|
| 23 |
+
* 選択されたモーメントが同じ場合はキャッシュを使用
|
| 24 |
+
*/
|
| 25 |
+
export function useThemeByMoment(params: UseThemeByMomentParams) {
|
| 26 |
+
const { commonDict, scoreDict, swot, strategies, moments, fv_infos, own_theme, userEmail, enabled = true } = params;
|
| 27 |
+
const { userIdentifier } = useGlobalStore();
|
| 28 |
+
|
| 29 |
+
return useQuery({
|
| 30 |
+
...createQueryOptions(),
|
| 31 |
+
queryKey: [
|
| 32 |
+
'themeByMoment',
|
| 33 |
+
moments, // 選択されたモーメント(これが主要なキャッシュキー)
|
| 34 |
+
commonDict,
|
| 35 |
+
scoreDict,
|
| 36 |
+
swot,
|
| 37 |
+
strategies,
|
| 38 |
+
fv_infos,
|
| 39 |
+
own_theme,
|
| 40 |
+
userEmail,
|
| 41 |
+
],
|
| 42 |
+
queryFn: async () => {
|
| 43 |
+
if (!commonDict || !scoreDict || !swot || !moments || moments.length === 0) {
|
| 44 |
+
throw new Error('Required parameters are missing');
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
const response = await getThemeByMoment(
|
| 48 |
+
{
|
| 49 |
+
commonDict,
|
| 50 |
+
scoreDict,
|
| 51 |
+
swot,
|
| 52 |
+
strategies: strategies || {},
|
| 53 |
+
moments,
|
| 54 |
+
fv_infos: fv_infos || undefined,
|
| 55 |
+
own_theme: own_theme || '',
|
| 56 |
+
},
|
| 57 |
+
userEmail || '',
|
| 58 |
+
userIdentifier,
|
| 59 |
+
);
|
| 60 |
+
|
| 61 |
+
return response;
|
| 62 |
+
},
|
| 63 |
+
enabled: enabled && !!commonDict && !!scoreDict && !!swot && !!moments && moments.length > 0,
|
| 64 |
+
});
|
| 65 |
+
}
|
api-client/proposal/proposal/translator.spec.ts
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { describe, expect, it } from 'vitest';
|
| 2 |
+
import { ProposalTranslator } from './translator';
|
| 3 |
+
import type {
|
| 4 |
+
ContentSection,
|
| 5 |
+
ProposalCnGradioResponse,
|
| 6 |
+
ProposalFvGradioResponse,
|
| 7 |
+
ProposalIntentGradioResponse,
|
| 8 |
+
ProposalPredictionGradioResponse,
|
| 9 |
+
ThemeByMomentStrategy,
|
| 10 |
+
} from '@/schema/proposal';
|
| 11 |
+
|
| 12 |
+
describe('ProposalTranslator', () => {
|
| 13 |
+
const translator = ProposalTranslator;
|
| 14 |
+
|
| 15 |
+
describe('translateCnDataToTabs', () => {
|
| 16 |
+
it('ダミーデータの各戦略×フェーズが正しいタブにマッピングされる', () => {
|
| 17 |
+
// ダミーデータと同じ構造のテストデータ
|
| 18 |
+
const testData: ProposalCnGradioResponse = {
|
| 19 |
+
積極化戦略: {
|
| 20 |
+
ニーズ1: {
|
| 21 |
+
cn: [
|
| 22 |
+
{ 大区分: 'News/更新情報', 中区分: 'テスト1', 小区分json: [], 制作意図: '' },
|
| 23 |
+
{ 大区分: '商品/サービスの特徴', 中区分: 'テスト2', 小区分json: [], 制作意図: '' },
|
| 24 |
+
{ 大区分: '商品/サービスの詳細情報', 中区分: 'テスト3', 小区分json: [], 制作意図: '' },
|
| 25 |
+
{ 大区分: 'メディア掲載情報', 中区分: 'テスト4', 小区分json: [], 制作意図: '' },
|
| 26 |
+
],
|
| 27 |
+
},
|
| 28 |
+
ニーズ2: {
|
| 29 |
+
cn: [
|
| 30 |
+
{ 大区分: '問題提起/共感', 中区分: 'テスト5', 小区分json: [], 制作意図: '' },
|
| 31 |
+
{ 大区分: 'シミュレーション', 中区分: 'テスト6', 小区分json: [], 制作意図: '' },
|
| 32 |
+
{ 大区分: '解決策/ベネフィット提示', 中区分: 'テスト7', 小区分json: [], 制作意図: '' },
|
| 33 |
+
{ 大区分: 'SPオファー/限定特典', 中区分: 'テスト8', 小区分json: [], 制作意図: '' },
|
| 34 |
+
],
|
| 35 |
+
},
|
| 36 |
+
ニーズ3: {
|
| 37 |
+
cn: [
|
| 38 |
+
{ 大区分: '料金/プラン', 中区分: 'テスト9', 小区分json: [], 制作意図: '' },
|
| 39 |
+
{ 大区分: '成功事例/ユーザーボイス紹介', 中区分: 'テスト10', 小区分json: [], 制作意図: '' },
|
| 40 |
+
{ 大区分: 'ご利用の流れ/ステップ', 中区分: 'テスト11', 小区分json: [], 制作意図: '' },
|
| 41 |
+
{ 大区分: '競合比較', 中区分: 'テスト12', 小区分json: [], 制作意図: '' },
|
| 42 |
+
],
|
| 43 |
+
},
|
| 44 |
+
},
|
| 45 |
+
改善戦略: {
|
| 46 |
+
ニーズ4: {
|
| 47 |
+
cn: [
|
| 48 |
+
{ 大区分: 'スタッフ・メンバー紹介', 中区分: 'テスト13', 小区分json: [], 制作意図: '' },
|
| 49 |
+
{ 大区分: '権威訴求', 中区分: 'テスト14', 小区分json: [], 制作意図: '' },
|
| 50 |
+
{ 大区分: '店舗情報', 中区分: 'テスト15', 小区分json: [], 制作意図: '' },
|
| 51 |
+
{ 大区分: '活用シーン', 中区分: 'テスト16', 小区分json: [], 制作意図: '' },
|
| 52 |
+
{ 大区分: 'FAQ/よくある質問', 中区分: 'テスト17', 小区分json: [], 制作意図: '' },
|
| 53 |
+
],
|
| 54 |
+
},
|
| 55 |
+
ニーズ5: {
|
| 56 |
+
cn: [
|
| 57 |
+
{ 大区分: 'News/更新情報', 中区分: 'テスト18', 小区分json: [], 制作意図: '' },
|
| 58 |
+
{ 大区分: '商品/サービスの特徴', 中区分: 'テスト19', 小区分json: [], 制作意図: '' },
|
| 59 |
+
{ 大区分: '商品/サービスの詳細情報', 中区分: 'テスト20', 小区分json: [], 制作意図: '' },
|
| 60 |
+
{ 大区分: 'メディア掲載情報', 中区分: 'テスト21', 小区分json: [], 制作意図: '' },
|
| 61 |
+
{ 大区分: '問題提起/共感', 中区分: 'テスト22', 小区分json: [], 制作意図: '' },
|
| 62 |
+
],
|
| 63 |
+
},
|
| 64 |
+
ニーズ6: {
|
| 65 |
+
cn: [
|
| 66 |
+
{ 大区分: 'シミュレーション', 中区分: 'テスト23', 小区分json: [], 制作意図: '' },
|
| 67 |
+
{ 大区分: '解決策/ベネフィット提示', 中区分: 'テスト24', 小区分json: [], 制作意図: '' },
|
| 68 |
+
{ 大区分: 'SPオファー/限定特典', 中区分: 'テスト25', 小区分json: [], 制作意図: '' },
|
| 69 |
+
{ 大区分: '料金/プラン', 中区分: 'テスト26', 小区分json: [], 制作意図: '' },
|
| 70 |
+
{ 大区分: '成功事例/ユーザーボイス紹介', 中区分: 'テスト27', 小区分json: [], 制作意図: '' },
|
| 71 |
+
],
|
| 72 |
+
},
|
| 73 |
+
},
|
| 74 |
+
差別化戦略: {
|
| 75 |
+
ニーズ7: {
|
| 76 |
+
cn: [
|
| 77 |
+
{ 大区分: 'ご利用の流れ/ステップ', 中区分: 'テスト28', 小区分json: [], 制作意図: '' },
|
| 78 |
+
{ 大区分: '競合比較', 中区分: 'テスト29', 小区分json: [], 制作意図: '' },
|
| 79 |
+
{ 大区分: 'スタッフ・メンバー紹介', 中区分: 'テスト30', 小区分json: [], 制作意図: '' },
|
| 80 |
+
{ 大区分: '権威訴求', 中区分: 'テスト31', 小区分json: [], 制作意図: '' },
|
| 81 |
+
{ 大区分: '店舗情報', 中区分: 'テスト32', 小区分json: [], 制作意図: '' },
|
| 82 |
+
{ 大区分: '活用シーン', 中区分: 'テスト33', 小区分json: [], 制作意図: '' },
|
| 83 |
+
],
|
| 84 |
+
},
|
| 85 |
+
ニーズ8: {
|
| 86 |
+
cn: [
|
| 87 |
+
{ 大区分: 'FAQ/よくある質問', 中区分: 'テスト34', 小区分json: [], 制作意図: '' },
|
| 88 |
+
{ 大区分: 'News/更新情報', 中区分: 'テスト35', 小区分json: [], 制作意図: '' },
|
| 89 |
+
{ 大区分: '商品/サービスの特徴', 中区分: 'テスト36', 小区分json: [], 制作意図: '' },
|
| 90 |
+
{ 大区分: '商品/サービスの詳細情報', 中区分: 'テスト37', 小区分json: [], 制作意図: '' },
|
| 91 |
+
{ 大区分: 'メディア掲載情報', 中区分: 'テスト38', 小区分json: [], 制作意図: '' },
|
| 92 |
+
{ 大区分: '問題提起/共感', 中区分: 'テスト39', 小区分json: [], 制作意図: '' },
|
| 93 |
+
],
|
| 94 |
+
},
|
| 95 |
+
ニーズ9: {
|
| 96 |
+
cn: [
|
| 97 |
+
{ 大区分: 'シミュレーション', 中区分: 'テスト40', 小区分json: [], 制作意図: '' },
|
| 98 |
+
{ 大区分: '解決策/ベネフィット提示', 中区分: 'テスト41', 小区分json: [], 制作意図: '' },
|
| 99 |
+
{ 大区分: 'SPオファー/限定特典', 中区分: 'テスト42', 小区分json: [], 制作意図: '' },
|
| 100 |
+
{ 大区分: '料金/プラン', 中区分: 'テスト43', 小区分json: [], 制作意図: '' },
|
| 101 |
+
{ 大区分: '成功事例/ユーザーボイス紹介', 中区分: 'テスト44', 小区分json: [], 制作意図: '' },
|
| 102 |
+
{ 大区分: 'ご利用の流れ/ステップ', 中区分: 'テスト45', 小区分json: [], 制作意図: '' },
|
| 103 |
+
],
|
| 104 |
+
},
|
| 105 |
+
},
|
| 106 |
+
};
|
| 107 |
+
|
| 108 |
+
const result = translator.translateCnDataToTabs(testData);
|
| 109 |
+
|
| 110 |
+
// 9つのタブが生成されること
|
| 111 |
+
expect(Object.keys(result)).toHaveLength(9);
|
| 112 |
+
expect(Object.keys(result)).toEqual(['A案', 'B案', 'C案', 'D案', 'E案', 'F案', 'G案', 'H案', 'I案']);
|
| 113 |
+
|
| 114 |
+
// 各タブのセクション数を確認
|
| 115 |
+
expect(result['A案']).toHaveLength(4); // 積極化戦略・ニーズ1
|
| 116 |
+
expect(result['B案']).toHaveLength(4); // 積極化戦略・ニーズ2
|
| 117 |
+
expect(result['C案']).toHaveLength(4); // 積極化戦略・ニーズ3
|
| 118 |
+
expect(result['D案']).toHaveLength(5); // 改善戦略・ニーズ4
|
| 119 |
+
expect(result['E案']).toHaveLength(5); // 改善戦略・ニーズ5
|
| 120 |
+
expect(result['F案']).toHaveLength(5); // 改善戦略・ニーズ6
|
| 121 |
+
expect(result['G案']).toHaveLength(6); // 差別化戦略・ニーズ7
|
| 122 |
+
expect(result['H案']).toHaveLength(6); // 差別化戦略・ニーズ8
|
| 123 |
+
expect(result['I案']).toHaveLength(6); // 差別化戦略・ニーズ9
|
| 124 |
+
|
| 125 |
+
// 合計セクション数の確認
|
| 126 |
+
const totalSections = Object.values(result).reduce((sum, sections) => sum + sections.length, 0);
|
| 127 |
+
expect(totalSections).toBe(45); // 4+4+4+5+5+5+6+6+6 = 45
|
| 128 |
+
});
|
| 129 |
+
|
| 130 |
+
it('10セクション以上のデータが正しく処理される', () => {
|
| 131 |
+
const testData: ProposalCnGradioResponse = {
|
| 132 |
+
積極化戦略: {
|
| 133 |
+
大量セクションのニーズ: {
|
| 134 |
+
cn: Array.from({ length: 10 }, (_, i) => ({
|
| 135 |
+
大区分: 'News/更新情報' as ContentSection['大区分'],
|
| 136 |
+
中区分: `セクション${i + 1}の内容`,
|
| 137 |
+
小区分json: [],
|
| 138 |
+
制作意図: '',
|
| 139 |
+
})),
|
| 140 |
+
},
|
| 141 |
+
},
|
| 142 |
+
改善戦略: {},
|
| 143 |
+
差別化戦略: {},
|
| 144 |
+
};
|
| 145 |
+
|
| 146 |
+
const result = translator.translateCnDataToTabs(testData);
|
| 147 |
+
|
| 148 |
+
// A案に10セクション全てが含まれること
|
| 149 |
+
expect(result['A案']).toHaveLength(10);
|
| 150 |
+
|
| 151 |
+
// 各セクションの内容が正しいこと
|
| 152 |
+
result['A案'].forEach((section, index) => {
|
| 153 |
+
expect(section.大区分).toBe('News/更新情報');
|
| 154 |
+
expect(section.中区分).toBe(`セクション${index + 1}の内容`);
|
| 155 |
+
});
|
| 156 |
+
});
|
| 157 |
+
|
| 158 |
+
it('小区分jsonのフィールド変換が正しく行われる', () => {
|
| 159 |
+
const testData: ProposalCnGradioResponse = {
|
| 160 |
+
積極化戦略: {
|
| 161 |
+
テストニーズ: {
|
| 162 |
+
cn: [
|
| 163 |
+
{
|
| 164 |
+
大区分: 'News/更新情報',
|
| 165 |
+
中区分: '最新ニュース',
|
| 166 |
+
小区分json: [
|
| 167 |
+
{
|
| 168 |
+
見出し: {
|
| 169 |
+
value: 'タイトル1',
|
| 170 |
+
情報源: { 他社: '', 自社: '' }
|
| 171 |
+
},
|
| 172 |
+
内容: {
|
| 173 |
+
value: 'コンテンツ1',
|
| 174 |
+
情報源: { 他社: '', 自社: '' }
|
| 175 |
+
},
|
| 176 |
+
注釈: {
|
| 177 |
+
value: '注釈1',
|
| 178 |
+
情報源: { 他社: '', 自社: '' }
|
| 179 |
+
},
|
| 180 |
+
ユーザー情報: {
|
| 181 |
+
value: 'ユーザー1',
|
| 182 |
+
情報源: { 他社: '', 自社: '' }
|
| 183 |
+
},
|
| 184 |
+
},
|
| 185 |
+
],
|
| 186 |
+
制作意図: 'テスト意図',
|
| 187 |
+
},
|
| 188 |
+
],
|
| 189 |
+
},
|
| 190 |
+
},
|
| 191 |
+
改善戦略: {},
|
| 192 |
+
差別化戦略: {},
|
| 193 |
+
};
|
| 194 |
+
|
| 195 |
+
const result = translator.translateCnDataToTabs(testData);
|
| 196 |
+
|
| 197 |
+
const section = result['A案'][0];
|
| 198 |
+
expect(section.小区分json[0]).toEqual({
|
| 199 |
+
見出し: {
|
| 200 |
+
value: 'タイトル1',
|
| 201 |
+
情報源: { 他社: '', 自社: '' }
|
| 202 |
+
},
|
| 203 |
+
内容: {
|
| 204 |
+
value: 'コンテンツ1',
|
| 205 |
+
情報源: { 他社: '', 自社: '' }
|
| 206 |
+
},
|
| 207 |
+
注釈: {
|
| 208 |
+
value: '注釈1',
|
| 209 |
+
情報源: { 他社: '', 自社: '' }
|
| 210 |
+
},
|
| 211 |
+
ユーザー情報: {
|
| 212 |
+
value: 'ユーザー1',
|
| 213 |
+
情報源: { 他社: '', 自社: '' }
|
| 214 |
+
},
|
| 215 |
+
});
|
| 216 |
+
});
|
| 217 |
+
|
| 218 |
+
it('cnフィールドが存在しない場合、空配列を返す', () => {
|
| 219 |
+
const testData: ProposalCnGradioResponse = {
|
| 220 |
+
積極化戦略: {
|
| 221 |
+
cnなしニーズ: {} as any,
|
| 222 |
+
},
|
| 223 |
+
改善戦略: {},
|
| 224 |
+
差別化戦略: {},
|
| 225 |
+
};
|
| 226 |
+
|
| 227 |
+
const result = translator.translateCnDataToTabs(testData);
|
| 228 |
+
expect(result['A案']).toEqual([]);
|
| 229 |
+
});
|
| 230 |
+
|
| 231 |
+
it('空の戦略データでもエラーにならない', () => {
|
| 232 |
+
const testData: ProposalCnGradioResponse = {
|
| 233 |
+
積極化戦略: {},
|
| 234 |
+
改善戦略: {},
|
| 235 |
+
差別化戦略: {},
|
| 236 |
+
};
|
| 237 |
+
|
| 238 |
+
expect(() => translator.translateCnDataToTabs(testData)).not.toThrow();
|
| 239 |
+
const result = translator.translateCnDataToTabs(testData);
|
| 240 |
+
expect(Object.keys(result)).toHaveLength(0);
|
| 241 |
+
});
|
| 242 |
+
|
| 243 |
+
it('15セクションのデータが切り捨てられずに処理される', () => {
|
| 244 |
+
const testData: ProposalCnGradioResponse = {
|
| 245 |
+
積極化戦略: {
|
| 246 |
+
超大量セクション: {
|
| 247 |
+
cn: Array.from({ length: 15 }, (_, i) => ({
|
| 248 |
+
大区分: 'News/更新情報' as ContentSection['大区分'],
|
| 249 |
+
中区分: `中区分${i + 1}`,
|
| 250 |
+
小区分json: [
|
| 251 |
+
{
|
| 252 |
+
見出し: {
|
| 253 |
+
value: `見出し${i + 1}`,
|
| 254 |
+
情報源: { 他社: '', 自社: '' }
|
| 255 |
+
},
|
| 256 |
+
内容: {
|
| 257 |
+
value: `内容${i + 1}`,
|
| 258 |
+
情報源: { 他社: '', 自社: '' }
|
| 259 |
+
},
|
| 260 |
+
},
|
| 261 |
+
],
|
| 262 |
+
制作意図: `意図${i + 1}`,
|
| 263 |
+
})),
|
| 264 |
+
},
|
| 265 |
+
},
|
| 266 |
+
改善戦略: {},
|
| 267 |
+
差別化戦略: {},
|
| 268 |
+
};
|
| 269 |
+
|
| 270 |
+
const result = translator.translateCnDataToTabs(testData);
|
| 271 |
+
|
| 272 |
+
// 15セクション全てが含まれることを確認
|
| 273 |
+
expect(result['A案']).toHaveLength(15);
|
| 274 |
+
|
| 275 |
+
// 最初と最後のセクションの内容を確認
|
| 276 |
+
expect(result['A案'][0].大区分).toBe('News/更新情報');
|
| 277 |
+
expect(result['A案'][14].大区分).toBe('News/更新情報');
|
| 278 |
+
|
| 279 |
+
// 全セクションが順番通りに存在することを確認
|
| 280 |
+
result['A案'].forEach((section, index) => {
|
| 281 |
+
expect(section.大区分).toBe('News/更新情報');
|
| 282 |
+
expect(section.中区分).toBe(`中区分${index + 1}`);
|
| 283 |
+
expect(section.制作意図).toBe(`意図${index + 1}`);
|
| 284 |
+
});
|
| 285 |
+
});
|
| 286 |
+
});
|
| 287 |
+
|
| 288 |
+
describe('translateToTabsData', () => {
|
| 289 |
+
it('すべてのタブにprediction/intentデータが正しくマッピングされる', () => {
|
| 290 |
+
// ダミーデータと同じ構造のテストデータ
|
| 291 |
+
const proposalFvData: ProposalFvGradioResponse = {
|
| 292 |
+
積極化戦略: {
|
| 293 |
+
'AIによる成長支援を求めるニーズの高まり(AIを活用したマーケティング戦略を見直したいと感じた瞬間)': {
|
| 294 |
+
fv: { コピー: { メインコピー: '積極化AI1' } },
|
| 295 |
+
},
|
| 296 |
+
'デジタル革新を追求するニーズの高まり(最新のデジタル技術を導入したいと感じた瞬間)': {
|
| 297 |
+
fv: { コピー: { メインコピー: '積極化デジタル2' } },
|
| 298 |
+
},
|
| 299 |
+
'顧客体験の変革を求めるニーズの高まり(顧客満足度を向上させたいと感じた瞬間)': {
|
| 300 |
+
fv: { コピー: { メインコピー: '積極化顧客3' } },
|
| 301 |
+
},
|
| 302 |
+
},
|
| 303 |
+
改善戦略: {
|
| 304 |
+
'AIによる成長支援を求めるニーズの高まり(AIを活用したマーケティング戦略を見直したいと感じた瞬間)': {
|
| 305 |
+
fv: { コピー: { メインコピー: '改善AI4' } },
|
| 306 |
+
},
|
| 307 |
+
'デジタル革新を追求するニーズの高まり(最新のデジタル技術を導入したいと感じた瞬間)': {
|
| 308 |
+
fv: { コピー: { メインコピー: '改善デジタル5' } },
|
| 309 |
+
},
|
| 310 |
+
'顧客体験の変革を求めるニーズの高まり(顧客満足度を向上させたいと感じた瞬間)': {
|
| 311 |
+
fv: { コピー: { メインコピー: '改善顧客6' } },
|
| 312 |
+
},
|
| 313 |
+
},
|
| 314 |
+
差別化戦略: {
|
| 315 |
+
'AIによる成長支援を求めるニーズの高まり(AIを活用したマーケティング戦略を見直したいと感じた瞬間)': {
|
| 316 |
+
fv: { コピー: { メインコピー: '差別化AI7' } },
|
| 317 |
+
},
|
| 318 |
+
'デジタル革新を追求するニーズの高まり(最新のデジタル技術を導入したいと感じた瞬間)': {
|
| 319 |
+
fv: { コピー: { メインコピー: '差別化デジタル8' } },
|
| 320 |
+
},
|
| 321 |
+
'顧客体験の変革を求めるニーズの高まり(顧客満足度を向上させたいと感じた瞬間)': {
|
| 322 |
+
fv: { コピー: { メインコピー: '差別化顧客9' } },
|
| 323 |
+
},
|
| 324 |
+
},
|
| 325 |
+
};
|
| 326 |
+
|
| 327 |
+
const proposalCnData: ProposalCnGradioResponse = {
|
| 328 |
+
積極化戦略: {
|
| 329 |
+
'AIによる成長支援を求めるニーズの高まり(AIを活用したマーケティング戦略を見直したいと感じた瞬間)': {
|
| 330 |
+
cn: [{ 大区分: 'News/更新情報', 中区分: 'CN1', 小区分json: [], 制作意図: '' }],
|
| 331 |
+
},
|
| 332 |
+
'デジタル革新を追求するニーズの高まり(最新のデジタル技術を導入したいと感じた瞬間)': {
|
| 333 |
+
cn: [{ 大区分: 'News/更新情報', 中区分: 'CN2', 小区分json: [], 制作意図: '' }],
|
| 334 |
+
},
|
| 335 |
+
'顧客体験の変革を求めるニーズの高まり(顧客満足度を向上させたいと感じた瞬間)': {
|
| 336 |
+
cn: [{ 大区分: 'News/更新情報', 中区分: 'CN3', 小区分json: [], 制作意図: '' }],
|
| 337 |
+
},
|
| 338 |
+
},
|
| 339 |
+
改善戦略: {
|
| 340 |
+
'AIによる成長支援を求めるニーズの高まり(AIを活用したマーケティング戦略を見直したいと感じた矬間)': {
|
| 341 |
+
cn: [{ 大区分: 'News/更新情報', 中区分: 'CN4', 小区分json: [], 制作意図: '' }],
|
| 342 |
+
},
|
| 343 |
+
'デジタル革新を追求するニーズの高まり(最新のデジタル技術を導入したいと感じた矬間)': {
|
| 344 |
+
cn: [{ 大区分: 'News/更新情報', 中区分: 'CN5', 小区分json: [], 制作意図: '' }],
|
| 345 |
+
},
|
| 346 |
+
'顧客体験の変革を求めるニーズの高まり(顧客満足度を向上させたいと感じた瞬間)': {
|
| 347 |
+
cn: [{ 大区分: 'News/更新情報', 中区分: 'CN6', 小区分json: [], 制作意図: '' }],
|
| 348 |
+
},
|
| 349 |
+
},
|
| 350 |
+
差別化戦略: {
|
| 351 |
+
'AIによる成長支援を求めるニーズの高まり(AIを活用したマーケティング戦略を見直したいと感じた矬間)': {
|
| 352 |
+
cn: [{ 大区分: 'News/更新情報', 中区分: 'CN7', 小区分json: [], 制作意図: '' }],
|
| 353 |
+
},
|
| 354 |
+
'デジタル革新を追求するニーズの高まり(最新のデジタル技術を導入したいと感じた矬間)': {
|
| 355 |
+
cn: [{ 大区分: 'News/更新情報', 中区分: 'CN8', 小区分json: [], 制作意図: '' }],
|
| 356 |
+
},
|
| 357 |
+
'顧客体験の変革を求めるニーズの高まり(顧客満足度を向上させたいと感じた瞬間)': {
|
| 358 |
+
cn: [{ 大区分: 'News/更新情報', 中区分: 'CN9', 小区分json: [], 制作意図: '' }],
|
| 359 |
+
},
|
| 360 |
+
},
|
| 361 |
+
};
|
| 362 |
+
|
| 363 |
+
const proposalIntentData: ProposalIntentGradioResponse = [
|
| 364 |
+
{
|
| 365 |
+
積極化戦略: {
|
| 366 |
+
'AIによる成長支援を求めるニーズの高まり(AIを活用したマーケティング戦略を見直したいと感じた瞬間)': {
|
| 367 |
+
intent_all: { FV_intent: 'FV1', CN_intent: 'CN1' },
|
| 368 |
+
},
|
| 369 |
+
'デジタル革新を追求するニーズの高まり(最新のデジタル技術を導入したいと感じた瞬間)': {
|
| 370 |
+
intent_all: { FV_intent: 'FV2', CN_intent: 'CN2' },
|
| 371 |
+
},
|
| 372 |
+
'顧客体験の変革を求めるニーズの高まり(顧客満足度を向上させたいと感じた瞬間)': {
|
| 373 |
+
intent_all: { FV_intent: 'FV3', CN_intent: 'CN3' },
|
| 374 |
+
},
|
| 375 |
+
},
|
| 376 |
+
改善戦略: {
|
| 377 |
+
'AIによる成長支援を求めるニーズの高まり(AIを活用したマーケティング戦略を見直したいと感じた矬間)': {
|
| 378 |
+
intent_all: { FV_intent: 'FV4', CN_intent: 'CN4' },
|
| 379 |
+
},
|
| 380 |
+
'デジタル革新を追求するニーズの高まり(最新のデジタル技術を導入したいと感じた瞬間)': {
|
| 381 |
+
intent_all: { FV_intent: 'FV5', CN_intent: 'CN5' },
|
| 382 |
+
},
|
| 383 |
+
'顧客体験の変革を求めるニーズの高まり(顧客満足度を向上させたいと感じた瞬間)': {
|
| 384 |
+
intent_all: { FV_intent: 'FV6', CN_intent: 'CN6' },
|
| 385 |
+
},
|
| 386 |
+
},
|
| 387 |
+
差別化戦略: {
|
| 388 |
+
'AIによる成長支援を求めるニーズの高まり(AIを活用したマーケティング戦略を見直したいと感じた矬間)': {
|
| 389 |
+
intent_all: { FV_intent: 'FV7', CN_intent: 'CN7' },
|
| 390 |
+
},
|
| 391 |
+
'デジタル革新を追求するニーズの高まり(最新のデジタル技術を導入したいと感じた矬間)': {
|
| 392 |
+
intent_all: { FV_intent: 'FV8', CN_intent: 'CN8' },
|
| 393 |
+
},
|
| 394 |
+
'顧客体験の変革を求めるニーズの高まり(顧客満足度を向上させたいと感じた瞬間)': {
|
| 395 |
+
intent_all: { FV_intent: 'FV9', CN_intent: 'CN9' },
|
| 396 |
+
},
|
| 397 |
+
},
|
| 398 |
+
},
|
| 399 |
+
];
|
| 400 |
+
|
| 401 |
+
const proposalPredictionData: ProposalPredictionGradioResponse = [
|
| 402 |
+
{
|
| 403 |
+
積極化戦略: {
|
| 404 |
+
'AIによる成長支援を求めるニーズの高まり(AIを活用したマーケティング戦略を見直したいと感じた瞬間)': {
|
| 405 |
+
prediction: 3,
|
| 406 |
+
},
|
| 407 |
+
'デジタル革新を追求するニーズの高まり(最新のデジタル技術を導入したいと感じた瞬間)': {
|
| 408 |
+
prediction: 2,
|
| 409 |
+
},
|
| 410 |
+
'顧客体験の変革を求めるニーズの高まり(顧客満足度を向上させたいと感じた矬間)': {
|
| 411 |
+
prediction: 0,
|
| 412 |
+
},
|
| 413 |
+
},
|
| 414 |
+
改善戦略: {
|
| 415 |
+
'AIによる成長支援を求めるニーズの高まり(AIを活用したマーケティング戦略を見直したいと感じた矬間)': {
|
| 416 |
+
prediction: 2,
|
| 417 |
+
},
|
| 418 |
+
'デジタル革新を追求するニーズの高まり(最新のデジタル技術を導入したいと感じた矬間)': {
|
| 419 |
+
prediction: 3,
|
| 420 |
+
},
|
| 421 |
+
'顧客体験の変革を求めるニーズの高まり(顧客満足度を向上させたいと感じた瞬間)': {
|
| 422 |
+
prediction: 3,
|
| 423 |
+
},
|
| 424 |
+
},
|
| 425 |
+
差別化戦略: {
|
| 426 |
+
'AIによる成長支援を求めるニーズの高まり(AIを活用したマーケティング戦略を見直したいと感じた矬間)': {
|
| 427 |
+
prediction: 1,
|
| 428 |
+
},
|
| 429 |
+
'デジタル革新を追求するニーズの高まり(最新のデジタル技術を導入したいと感じた矬間)': {
|
| 430 |
+
prediction: 1,
|
| 431 |
+
},
|
| 432 |
+
'顧客体験の変革を求めるニーズの高まり(顧客満足度を向上させたいと感じた矬間)': {
|
| 433 |
+
prediction: 0,
|
| 434 |
+
},
|
| 435 |
+
},
|
| 436 |
+
},
|
| 437 |
+
];
|
| 438 |
+
|
| 439 |
+
const result = ProposalTranslator.translateToTabsData(
|
| 440 |
+
proposalFvData,
|
| 441 |
+
proposalCnData,
|
| 442 |
+
proposalIntentData,
|
| 443 |
+
proposalPredictionData,
|
| 444 |
+
undefined,
|
| 445 |
+
);
|
| 446 |
+
|
| 447 |
+
// resultがnullでないことを確認
|
| 448 |
+
expect(result).not.toBeNull();
|
| 449 |
+
|
| 450 |
+
// 9つのタブが生成されること
|
| 451 |
+
expect(Object.keys(result!)).toHaveLength(9);
|
| 452 |
+
expect(Object.keys(result!)).toEqual(['A案', 'B案', 'C案', 'D案', 'E案', 'F案', 'G案', 'H案', 'I案']);
|
| 453 |
+
|
| 454 |
+
// すべてのタブにpredictionとintentデータが存在することを確認
|
| 455 |
+
Object.entries(result!).forEach(([tabName, tabData]) => {
|
| 456 |
+
// intentデータが存在
|
| 457 |
+
expect(tabData.intent).toBeDefined();
|
| 458 |
+
expect(tabData.intent?.prediction).toBeDefined();
|
| 459 |
+
expect(typeof tabData.intent?.prediction).toBe('number');
|
| 460 |
+
expect(tabData.intent?.FV_intent).toBeDefined();
|
| 461 |
+
expect(tabData.intent?.CN_intent).toBeDefined();
|
| 462 |
+
|
| 463 |
+
// FVデータが存在
|
| 464 |
+
expect(tabData.fv).toBeDefined();
|
| 465 |
+
expect(tabData.fv?.data?.fv?.['コピー']?.['メインコピー']).toBeDefined();
|
| 466 |
+
|
| 467 |
+
// CNデータが存在
|
| 468 |
+
expect(tabData.cn).toBeDefined();
|
| 469 |
+
expect(tabData.cn!.length).toBeGreaterThan(0);
|
| 470 |
+
});
|
| 471 |
+
|
| 472 |
+
// prediction値によるソートが正しく行われていること
|
| 473 |
+
// prediction値が高い順: 積極化AI(3) > 改善デジタル(3) > 改善顧客(3) > 積極化デジタル(2) > 改善AI(2) > 差別化AI(1) > 差別化デジタル(1) > 積極化顧客(0) > 差別化顧客(0)
|
| 474 |
+
expect(result!['A案'].intent?.prediction).toBe(3);
|
| 475 |
+
expect(result!['A案'].fv?.data?.fv?.['コピー']?.['メインコピー']).toContain('積極化AI');
|
| 476 |
+
|
| 477 |
+
// H案、I案にもデータが存在することを特に確認
|
| 478 |
+
expect(result!['H案'].intent?.prediction).toBeDefined();
|
| 479 |
+
expect(result!['H案'].intent?.FV_intent).toBeDefined();
|
| 480 |
+
expect(result!['H案'].intent?.CN_intent).toBeDefined();
|
| 481 |
+
expect(result!['H案'].fv).toBeDefined();
|
| 482 |
+
expect(result!['H案'].cn).toBeDefined();
|
| 483 |
+
|
| 484 |
+
expect(result!['I案'].intent?.prediction).toBeDefined();
|
| 485 |
+
expect(result!['I案'].intent?.FV_intent).toBeDefined();
|
| 486 |
+
expect(result!['I案'].intent?.CN_intent).toBeDefined();
|
| 487 |
+
expect(result!['I案'].fv).toBeDefined();
|
| 488 |
+
expect(result!['I案'].cn).toBeDefined();
|
| 489 |
+
});
|
| 490 |
+
});
|
| 491 |
+
});
|
api-client/proposal/proposal/translator.ts
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ProposalCnGradioResponse } from '@/schema/gradio-proxy/proposal-cn';
|
| 2 |
+
import type { ProposalFvGradioResponse } from '@/schema/gradio-proxy/proposal-fv';
|
| 3 |
+
import type { ProposalIntentGradioResponse } from '@/schema/gradio-proxy/proposal-intent';
|
| 4 |
+
import type { ProposalPredictionGradioResponse } from '@/schema/gradio-proxy/proposal-prediction';
|
| 5 |
+
import type { ThemeByMomentStrategy } from '@/schema/gradio-proxy/theme-by-moment';
|
| 6 |
+
import type { ContentSection, FvData, ProposalTabData, ProposalTabsResponse } from '@/schema/proposal';
|
| 7 |
+
import { findWithNormalizedKey } from '@/utils/normalize-strategy-keys';
|
| 8 |
+
|
| 9 |
+
export const ProposalTranslator = {
|
| 10 |
+
/**
|
| 11 |
+
* FVデータをタブ形式に変換
|
| 12 |
+
* 戦略とニーズをキーとして格納
|
| 13 |
+
*/
|
| 14 |
+
translateFvDataToTabs(data: ProposalFvGradioResponse): Record<string, FvData> {
|
| 15 |
+
const result: Record<string, FvData> = {};
|
| 16 |
+
|
| 17 |
+
// データを変換(戦略とニーズをキーとして格納)
|
| 18 |
+
Object.entries(data).forEach(([strategy]) => {
|
| 19 |
+
const strategyData = data[strategy as keyof ProposalFvGradioResponse];
|
| 20 |
+
|
| 21 |
+
if (strategyData) {
|
| 22 |
+
Object.entries(strategyData).forEach(([need, needData]) => {
|
| 23 |
+
const key = `${strategy}_${need}`;
|
| 24 |
+
result[key] = {
|
| 25 |
+
strategy,
|
| 26 |
+
need,
|
| 27 |
+
data: {
|
| 28 |
+
fv: needData.fv,
|
| 29 |
+
},
|
| 30 |
+
};
|
| 31 |
+
});
|
| 32 |
+
}
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
return result;
|
| 36 |
+
},
|
| 37 |
+
|
| 38 |
+
/**
|
| 39 |
+
* CNデータをタブ形式に変換
|
| 40 |
+
* 戦略とニーズをキーとして格納
|
| 41 |
+
*/
|
| 42 |
+
translateCnDataToTabs(data: ProposalCnGradioResponse): Record<string, ContentSection[]> {
|
| 43 |
+
const result: Record<string, ContentSection[]> = {};
|
| 44 |
+
|
| 45 |
+
// 戦略→ニーズ→cn の階層構造でデータを処理
|
| 46 |
+
const strategies = ['積極化戦略', '改善戦略', '差別化戦略'];
|
| 47 |
+
|
| 48 |
+
strategies.forEach((strategy) => {
|
| 49 |
+
if (data[strategy as keyof ProposalCnGradioResponse]) {
|
| 50 |
+
const strategyData = data[strategy as keyof ProposalCnGradioResponse];
|
| 51 |
+
|
| 52 |
+
if (strategyData && typeof strategyData === 'object') {
|
| 53 |
+
Object.entries(strategyData).forEach(([need, needData]) => {
|
| 54 |
+
const key = `${strategy}_${need}`;
|
| 55 |
+
|
| 56 |
+
// cnフィールドから配列を取得
|
| 57 |
+
if (needData && typeof needData === 'object' && 'cn' in needData) {
|
| 58 |
+
const cnData = (needData as any).cn;
|
| 59 |
+
if (Array.isArray(cnData)) {
|
| 60 |
+
result[key] = cnData.map((section: any) => {
|
| 61 |
+
const convertedSection: any = {
|
| 62 |
+
大区分: section.大区分 as any,
|
| 63 |
+
中区分: section.中区分,
|
| 64 |
+
小区分json: [],
|
| 65 |
+
制作意図: section.制作意図 || '',
|
| 66 |
+
};
|
| 67 |
+
|
| 68 |
+
if (Array.isArray(section.小区分json)) {
|
| 69 |
+
convertedSection.小区分json = section.小区分json.map((item: any) => {
|
| 70 |
+
// 各フィールドを統一形式(オブジェクト形式)に変換
|
| 71 |
+
const convertedItem: any = {};
|
| 72 |
+
|
| 73 |
+
// 見出しの変換
|
| 74 |
+
if (item.見出し !== undefined) {
|
| 75 |
+
if (typeof item.見出し === 'string') {
|
| 76 |
+
convertedItem.見出し = { value: item.見出し };
|
| 77 |
+
} else if (item.見出し && typeof item.見出し === 'object' && 'value' in item.見出し) {
|
| 78 |
+
convertedItem.見出し = item.見出し;
|
| 79 |
+
} else {
|
| 80 |
+
convertedItem.見出し = { value: '' };
|
| 81 |
+
}
|
| 82 |
+
} else {
|
| 83 |
+
convertedItem.見出し = { value: '' };
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
// 内容の変換
|
| 87 |
+
if (item.内容 !== undefined) {
|
| 88 |
+
if (typeof item.内容 === 'string') {
|
| 89 |
+
convertedItem.内容 = { value: item.内容 };
|
| 90 |
+
} else if (item.内容 && typeof item.内容 === 'object' && 'value' in item.内容) {
|
| 91 |
+
convertedItem.内容 = item.内容;
|
| 92 |
+
} else {
|
| 93 |
+
convertedItem.内容 = { value: '' };
|
| 94 |
+
}
|
| 95 |
+
} else {
|
| 96 |
+
convertedItem.内容 = { value: '' };
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
// 注釈の変換(オプショナル)
|
| 100 |
+
if (item.注釈 !== undefined && item.注釈 !== null) {
|
| 101 |
+
if (typeof item.注釈 === 'string') {
|
| 102 |
+
convertedItem.注釈 = { value: item.注釈 };
|
| 103 |
+
} else if (item.注釈 && typeof item.注釈 === 'object' && 'value' in item.注釈) {
|
| 104 |
+
convertedItem.注釈 = item.注釈;
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
// ユーザー情報の変換(オプショナル)
|
| 109 |
+
if (item.ユーザー情報 !== undefined && item.ユーザー情報 !== null) {
|
| 110 |
+
if (typeof item.ユーザー情報 === 'string') {
|
| 111 |
+
convertedItem.ユーザー情報 = { value: item.ユーザー情報 };
|
| 112 |
+
} else if (item.ユーザー情報 && typeof item.ユーザー情報 === 'object' && 'value' in item.ユーザー情報) {
|
| 113 |
+
convertedItem.ユーザー情報 = item.ユーザー情報;
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
// その他のフィールドをそのまま保持
|
| 118 |
+
Object.keys(item).forEach((key) => {
|
| 119 |
+
if (!['見出し', '内容', '注釈', 'ユーザー情報'].includes(key)) {
|
| 120 |
+
convertedItem[key] = item[key];
|
| 121 |
+
}
|
| 122 |
+
});
|
| 123 |
+
|
| 124 |
+
return convertedItem;
|
| 125 |
+
});
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
return convertedSection;
|
| 129 |
+
});
|
| 130 |
+
} else {
|
| 131 |
+
console.warn(`CNデータのcnフィールドが配列ではありません: ${strategy}/${need}`, cnData);
|
| 132 |
+
result[key] = [];
|
| 133 |
+
}
|
| 134 |
+
} else {
|
| 135 |
+
console.warn(`CNデータにcnフィールドがありません: ${strategy}/${need}`, needData);
|
| 136 |
+
result[key] = [];
|
| 137 |
+
}
|
| 138 |
+
});
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
});
|
| 142 |
+
|
| 143 |
+
return result;
|
| 144 |
+
},
|
| 145 |
+
|
| 146 |
+
/**
|
| 147 |
+
* 全てのProposalデータをタブ形式に統合変換
|
| 148 |
+
*/
|
| 149 |
+
translateToTabsData(
|
| 150 |
+
proposalFvData: ProposalFvGradioResponse,
|
| 151 |
+
proposalCnData: ProposalCnGradioResponse,
|
| 152 |
+
proposalIntentData: ProposalIntentGradioResponse,
|
| 153 |
+
proposalPredictionData: ProposalPredictionGradioResponse,
|
| 154 |
+
strategyXMoment: ThemeByMomentStrategy | undefined,
|
| 155 |
+
): ProposalTabsResponse | null {
|
| 156 |
+
// 本番環境でも確実にログを出力(console.warnを使用)
|
| 157 |
+
console.warn('[translateToTabsData] START - Processing proposal data');
|
| 158 |
+
console.warn('[translateToTabsData] FV data strategies:', Object.keys(proposalFvData || {}));
|
| 159 |
+
|
| 160 |
+
// APIデータの完全性チェック(配列の場合も考慮)
|
| 161 |
+
const checkValidData = (data: ProposalIntentGradioResponse | ProposalPredictionGradioResponse): boolean => {
|
| 162 |
+
if (!data) return false;
|
| 163 |
+
if (Array.isArray(data)) {
|
| 164 |
+
return data.length > 0 && data[0] && Object.keys(data[0]).length > 0;
|
| 165 |
+
}
|
| 166 |
+
return Object.keys(data).length > 0;
|
| 167 |
+
};
|
| 168 |
+
|
| 169 |
+
const hasValidPrediction = checkValidData(proposalPredictionData);
|
| 170 |
+
const hasValidIntent = checkValidData(proposalIntentData);
|
| 171 |
+
|
| 172 |
+
if (!hasValidPrediction || !hasValidIntent) {
|
| 173 |
+
console.error('[translateToTabsData] 必須データが不完全です:', {
|
| 174 |
+
hasValidPrediction,
|
| 175 |
+
hasValidIntent,
|
| 176 |
+
predictionData: proposalPredictionData,
|
| 177 |
+
intentData: proposalIntentData,
|
| 178 |
+
});
|
| 179 |
+
return null;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
// 戦略表示名のマッピング
|
| 183 |
+
const strategyDisplayMap: Record<string, string> = {
|
| 184 |
+
積極化戦略: '積極化戦略(強み×機会)',
|
| 185 |
+
改善戦略: '改善戦略(弱み×機会)',
|
| 186 |
+
差別化戦略: '差別化戦略(強み×脅威)',
|
| 187 |
+
};
|
| 188 |
+
|
| 189 |
+
// 戦略の順序定義(第2ソート用)
|
| 190 |
+
const strategyOrder: Record<string, number> = {
|
| 191 |
+
積極化戦略: 1,
|
| 192 |
+
改善戦略: 2,
|
| 193 |
+
差別化戦略: 3,
|
| 194 |
+
};
|
| 195 |
+
|
| 196 |
+
// 戦略別のデータを処理
|
| 197 |
+
const fvTabs = this.translateFvDataToTabs(proposalFvData);
|
| 198 |
+
const cnTabs = this.translateCnDataToTabs(proposalCnData);
|
| 199 |
+
|
| 200 |
+
// 一時的にデータを収集(ソート前)
|
| 201 |
+
interface TempTabData {
|
| 202 |
+
strategy: string;
|
| 203 |
+
need: string;
|
| 204 |
+
strategyOrder: number;
|
| 205 |
+
prediction: number;
|
| 206 |
+
tabData: ProposalTabData;
|
| 207 |
+
}
|
| 208 |
+
const tempTabsData: TempTabData[] = [];
|
| 209 |
+
|
| 210 |
+
Object.entries(proposalFvData).forEach(([strategy]) => {
|
| 211 |
+
const strategyFvData = proposalFvData[strategy as keyof typeof proposalFvData];
|
| 212 |
+
|
| 213 |
+
if (strategyFvData) {
|
| 214 |
+
Object.entries(strategyFvData).forEach(([need]) => {
|
| 215 |
+
// 戦略とニーズをキーとして使用
|
| 216 |
+
const dataKey = `${strategy}_${need}`;
|
| 217 |
+
|
| 218 |
+
// FVデータの取得(正規化したキーで検索)
|
| 219 |
+
const fvData: FvData = findWithNormalizedKey(fvTabs, dataKey) || {
|
| 220 |
+
strategy,
|
| 221 |
+
need,
|
| 222 |
+
data: {
|
| 223 |
+
fv: strategyFvData[need]?.fv || {},
|
| 224 |
+
},
|
| 225 |
+
};
|
| 226 |
+
|
| 227 |
+
// CNデータの取得(正規化したキーで検索)
|
| 228 |
+
let cnData: ContentSection[] = findWithNormalizedKey(cnTabs, dataKey) || [];
|
| 229 |
+
|
| 230 |
+
// 戦略タイプの表示名を設定
|
| 231 |
+
const strategyDisplayName = strategyDisplayMap[strategy] || strategy;
|
| 232 |
+
|
| 233 |
+
// Intentデータの抽出と展開
|
| 234 |
+
let intentData: ProposalTabData['intent'] = {
|
| 235 |
+
strategy: strategyDisplayName,
|
| 236 |
+
direction: need, // ニーズを方向性として���定
|
| 237 |
+
};
|
| 238 |
+
|
| 239 |
+
// strategyXMomentから該当する戦略とモーメントのデータを抽出
|
| 240 |
+
|
| 241 |
+
if (strategyXMoment && strategyXMoment[strategy]) {
|
| 242 |
+
const momentItem = findWithNormalizedKey(strategyXMoment[strategy], need);
|
| 243 |
+
if (momentItem) {
|
| 244 |
+
intentData.strategyXMomentItem = momentItem;
|
| 245 |
+
}
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
// proposalIntentDataから該当する戦略・ニーズのデータを抽出
|
| 249 |
+
const intentArray = Array.isArray(proposalIntentData) ? proposalIntentData : [proposalIntentData];
|
| 250 |
+
if (intentArray && intentArray[0]) {
|
| 251 |
+
const intentItem = intentArray[0];
|
| 252 |
+
const strategyIntent = intentItem[strategy as keyof typeof intentItem];
|
| 253 |
+
|
| 254 |
+
// 正規化したキーで検索を試みる
|
| 255 |
+
const needIntent = findWithNormalizedKey(strategyIntent, need);
|
| 256 |
+
|
| 257 |
+
if (needIntent) {
|
| 258 |
+
if (needIntent.intent_all) {
|
| 259 |
+
intentData = {
|
| 260 |
+
...intentData,
|
| 261 |
+
FV_intent: needIntent.intent_all.FV_intent,
|
| 262 |
+
CN_intent: needIntent.intent_all.CN_intent,
|
| 263 |
+
FV_suggest: needIntent.intent_all.FV_suggest,
|
| 264 |
+
CN_suggest: needIntent.intent_all.CN_suggest,
|
| 265 |
+
};
|
| 266 |
+
}
|
| 267 |
+
if (needIntent.cn_creation_intent) {
|
| 268 |
+
intentData.cn_creation_intent = needIntent.cn_creation_intent;
|
| 269 |
+
// cn_creation_intentから各セクションの制作意図を設定
|
| 270 |
+
cnData = cnData.map((section) => ({
|
| 271 |
+
...section,
|
| 272 |
+
制作意図: needIntent.cn_creation_intent[section.大区分] || '',
|
| 273 |
+
}));
|
| 274 |
+
}
|
| 275 |
+
if (needIntent.fv_creation_intent) {
|
| 276 |
+
fvData.fvCreationIntent = needIntent.fv_creation_intent;
|
| 277 |
+
}
|
| 278 |
+
}
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
// proposalPredictionDataから該当する戦略・ニーズのデータを抽出
|
| 282 |
+
let predictionValue = 0; // デフォルト値
|
| 283 |
+
const predictionArray = Array.isArray(proposalPredictionData) ? proposalPredictionData : [proposalPredictionData];
|
| 284 |
+
|
| 285 |
+
if (predictionArray && predictionArray[0]) {
|
| 286 |
+
const predictionItem = predictionArray[0];
|
| 287 |
+
const strategyPrediction = predictionItem[strategy as keyof typeof predictionItem];
|
| 288 |
+
|
| 289 |
+
// 正規化したキーで検索
|
| 290 |
+
const needPrediction = findWithNormalizedKey(strategyPrediction, need);
|
| 291 |
+
|
| 292 |
+
if (needPrediction) {
|
| 293 |
+
if (needPrediction.prediction !== undefined) {
|
| 294 |
+
intentData.prediction = needPrediction.prediction;
|
| 295 |
+
predictionValue = needPrediction.prediction;
|
| 296 |
+
} else {
|
| 297 |
+
}
|
| 298 |
+
} else {
|
| 299 |
+
// 本番環境でも確実に出力するためにconsole.warnも使用
|
| 300 |
+
console.warn(`[translateToTabsData] WARNING: No prediction found for ${dataKey}`, {
|
| 301 |
+
need,
|
| 302 |
+
availableNeeds: strategyPrediction ? Object.keys(strategyPrediction) : [],
|
| 303 |
+
strategyPrediction,
|
| 304 |
+
});
|
| 305 |
+
}
|
| 306 |
+
} else {
|
| 307 |
+
console.warn(`[translateToTabsData] WARNING: No prediction data available for ${dataKey}`);
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
// タブデータの作成
|
| 311 |
+
const tabData: ProposalTabData = {
|
| 312 |
+
intent: intentData,
|
| 313 |
+
fv: fvData,
|
| 314 |
+
cn: cnData,
|
| 315 |
+
};
|
| 316 |
+
|
| 317 |
+
// 一時データに追加
|
| 318 |
+
tempTabsData.push({
|
| 319 |
+
strategy,
|
| 320 |
+
need,
|
| 321 |
+
strategyOrder: strategyOrder[strategy] || 999,
|
| 322 |
+
prediction: predictionValue,
|
| 323 |
+
tabData,
|
| 324 |
+
});
|
| 325 |
+
});
|
| 326 |
+
}
|
| 327 |
+
});
|
| 328 |
+
|
| 329 |
+
// prediction値でソート(第1ソート: 降順)、同じ値の場合は戦略順(第2ソート)
|
| 330 |
+
tempTabsData.sort((a, b) => {
|
| 331 |
+
// 第1ソート: prediction値の降順
|
| 332 |
+
if (b.prediction !== a.prediction) {
|
| 333 |
+
return b.prediction - a.prediction;
|
| 334 |
+
}
|
| 335 |
+
// 第2ソート: 戦略の順序
|
| 336 |
+
return a.strategyOrder - b.strategyOrder;
|
| 337 |
+
});
|
| 338 |
+
|
| 339 |
+
// ソート後にA案〜I案を再割り当て
|
| 340 |
+
const result: ProposalTabsResponse = {};
|
| 341 |
+
const proposalLetters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'];
|
| 342 |
+
|
| 343 |
+
tempTabsData.forEach((item, index) => {
|
| 344 |
+
if (index < proposalLetters.length) {
|
| 345 |
+
const proposalKey = `${proposalLetters[index]}案`;
|
| 346 |
+
result[proposalKey] = item.tabData;
|
| 347 |
+
// 本番環境でも出力されるようにconsole.warnも使用
|
| 348 |
+
console.warn(`[translateToTabsData] Assigned ${proposalKey} to ${item.strategy}/${item.need} with prediction: ${item.prediction}`);
|
| 349 |
+
}
|
| 350 |
+
});
|
| 351 |
+
|
| 352 |
+
// デバッグ: 最終結果の確認(本番環境用にconsole.warnも追加)
|
| 353 |
+
console.warn('[translateToTabsData] Final result keys:', Object.keys(result));
|
| 354 |
+
|
| 355 |
+
Object.entries(result).forEach(([key, data]) => {
|
| 356 |
+
const summary = {
|
| 357 |
+
hasIntentData: !!data.intent,
|
| 358 |
+
hasFvData: !!data.fv,
|
| 359 |
+
hasCnData: !!data.cn,
|
| 360 |
+
strategy: data.intent?.strategy,
|
| 361 |
+
direction: data.intent?.direction,
|
| 362 |
+
fvメインコピー: data.fv?.data?.fv?.コピー?.メインコピー?.substring(0, 30),
|
| 363 |
+
};
|
| 364 |
+
|
| 365 |
+
// 本番環境でも確実に出力
|
| 366 |
+
console.warn(`[translateToTabsData] Final ${key}:`, summary);
|
| 367 |
+
});
|
| 368 |
+
|
| 369 |
+
console.warn('[translateToTabsData] END - Processing completed');
|
| 370 |
+
return result;
|
| 371 |
+
},
|
| 372 |
+
};
|
api-client/proposal/queries.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Re-export all hooks from subdirectories
|
| 2 |
+
export { generateContentsHtml, generateFvHtml, getImageGenerationStatus, startImageGeneration } from './html-preview/index';
|
| 3 |
+
export { useContentsHtmlGeneration, useContentsImageGeneration, useFvHtmlGeneration } from './html-preview/queries';
|
| 4 |
+
export { useProposalTabsOptimized } from './proposal/optimized-queries';
|
| 5 |
+
export { useThemeByMoment } from './proposal/queries';
|
| 6 |
+
export { ProposalTranslator } from './proposal/translator';
|
| 7 |
+
export { useBatchScreenshots, useScreenshot } from './screenshot/queries';
|
api-client/proposal/screenshot/queries.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createQueryOptions } from '@/api-client/query-config';
|
| 2 |
+
import { useQuery } from '@tanstack/react-query';
|
| 3 |
+
|
| 4 |
+
// バッチスクリーンショット取得用のフック
|
| 5 |
+
export function useBatchScreenshots(urls: Record<string, string>, enabled: boolean = false, dummyMode: boolean = false) {
|
| 6 |
+
return useQuery<Record<string, string>>({
|
| 7 |
+
...createQueryOptions<Record<string, string>>(),
|
| 8 |
+
queryKey: ['batchScreenshots', urls, dummyMode],
|
| 9 |
+
queryFn: async () => {
|
| 10 |
+
// ダミーモードの場合はプレースホルダー画像を返す
|
| 11 |
+
if (dummyMode) {
|
| 12 |
+
console.log('[useBatchScreenshots] Dummy mode enabled, returning placeholder images');
|
| 13 |
+
const placeholderImage =
|
| 14 |
+
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
|
| 15 |
+
const screenshots: Record<string, string> = {};
|
| 16 |
+
Object.keys(urls).forEach((key) => {
|
| 17 |
+
screenshots[key] = placeholderImage;
|
| 18 |
+
});
|
| 19 |
+
return screenshots;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
const response = await fetch('/api/rpc/screenshot/batch-screenshot', {
|
| 23 |
+
method: 'POST',
|
| 24 |
+
headers: { 'Content-Type': 'application/json' },
|
| 25 |
+
body: JSON.stringify({
|
| 26 |
+
urls: Object.entries(urls).map(([key, url]) => ({
|
| 27 |
+
key,
|
| 28 |
+
url,
|
| 29 |
+
})),
|
| 30 |
+
dummyMode,
|
| 31 |
+
}),
|
| 32 |
+
});
|
| 33 |
+
|
| 34 |
+
if (!response.ok) {
|
| 35 |
+
throw new Error('Failed to fetch screenshots');
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
const data = await response.json();
|
| 39 |
+
console.log('[useBatchScreenshots] Response data:', data);
|
| 40 |
+
|
| 41 |
+
const screenshots: Record<string, string> = {};
|
| 42 |
+
|
| 43 |
+
Object.entries(data.screenshots || {}).forEach(([key, value]: [string, any]) => {
|
| 44 |
+
// imageBase64フィールドを使用(サーバーはこのフィールドで返している)
|
| 45 |
+
screenshots[key] = value.imageBase64 || value.screenshotBase64;
|
| 46 |
+
});
|
| 47 |
+
|
| 48 |
+
console.log('[useBatchScreenshots] Processed screenshots:', {
|
| 49 |
+
keys: Object.keys(screenshots),
|
| 50 |
+
hasA案: !!screenshots['A案'],
|
| 51 |
+
});
|
| 52 |
+
|
| 53 |
+
return screenshots;
|
| 54 |
+
},
|
| 55 |
+
enabled,
|
| 56 |
+
});
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
// 単一スクリーンショット取得用のフック
|
| 60 |
+
export function useScreenshot(url: string, enabled: boolean = false, dummyMode: boolean = false) {
|
| 61 |
+
return useQuery<string>({
|
| 62 |
+
...createQueryOptions<string>(),
|
| 63 |
+
queryKey: ['screenshot', url, dummyMode],
|
| 64 |
+
queryFn: async () => {
|
| 65 |
+
// ダミーモードの場合はプレースホルダー画像を返す
|
| 66 |
+
if (dummyMode) {
|
| 67 |
+
console.log('[useScreenshot] Dummy mode enabled, returning placeholder image');
|
| 68 |
+
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
const params = new URLSearchParams({
|
| 72 |
+
url: url,
|
| 73 |
+
width: '512',
|
| 74 |
+
height: '768',
|
| 75 |
+
dummyMode: dummyMode.toString(),
|
| 76 |
+
});
|
| 77 |
+
|
| 78 |
+
const response = await fetch(`/api/rpc/screenshot?${params.toString()}`, {
|
| 79 |
+
method: 'GET',
|
| 80 |
+
headers: { 'Content-Type': 'application/json' },
|
| 81 |
+
});
|
| 82 |
+
|
| 83 |
+
if (!response.ok) {
|
| 84 |
+
throw new Error('Failed to fetch screenshot');
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
const data = await response.json();
|
| 88 |
+
|
| 89 |
+
// imageBase64が既にdata URLの場合はそのまま返す
|
| 90 |
+
if (data.imageBase64 && data.imageBase64.startsWith('data:')) {
|
| 91 |
+
return data.imageBase64;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// そうでない場合はdata:プレフィックスを追加
|
| 95 |
+
return `data:${data.mimeType || 'image/jpeg'};base64,${data.imageBase64}`;
|
| 96 |
+
},
|
| 97 |
+
enabled,
|
| 98 |
+
});
|
| 99 |
+
}
|
api-client/query-config.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { UseQueryOptions } from '@tanstack/react-query';
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* React Query共通設定
|
| 5 |
+
* 全APIクライアントで使用する共通のクエリオプション
|
| 6 |
+
*/
|
| 7 |
+
export const defaultQueryOptions: Partial<UseQueryOptions> = {
|
| 8 |
+
// ウィンドウフォーカス時の再フェッチを無効化
|
| 9 |
+
refetchOnWindowFocus: false,
|
| 10 |
+
|
| 11 |
+
// コンポーネント再マウント時の再フェッチを無効化
|
| 12 |
+
refetchOnMount: false,
|
| 13 |
+
|
| 14 |
+
// データが古くならない期間(無期限)
|
| 15 |
+
staleTime: Infinity,
|
| 16 |
+
|
| 17 |
+
// ガベージコレクション時間(24時間)
|
| 18 |
+
gcTime: 24 * 60 * 60 * 1000,
|
| 19 |
+
|
| 20 |
+
retry: 0,
|
| 21 |
+
|
| 22 |
+
// エラー時のリトライ遅延
|
| 23 |
+
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
/**
|
| 27 |
+
* 特定のクエリ用のオプションを作成するヘルパー関数
|
| 28 |
+
* @param overrides 上書きしたいオプション
|
| 29 |
+
* @returns マージされたクエリオプション
|
| 30 |
+
*/
|
| 31 |
+
export function createQueryOptions<T = unknown>(overrides?: Partial<UseQueryOptions<T>>): Partial<UseQueryOptions<T>> {
|
| 32 |
+
return { ...defaultQueryOptions, ...overrides } as Partial<UseQueryOptions<T>>;
|
| 33 |
+
}
|
api-client/refresh-moments/queries.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createQueryOptions } from '@/api-client/query-config';
|
| 2 |
+
import { rpcClient } from '@/api-client/utils/rpc-client';
|
| 3 |
+
import { useGlobalStore } from '@/store/global';
|
| 4 |
+
import { generateSelectionHash } from '@/store/proposal-trigger';
|
| 5 |
+
import { useQuery } from '@tanstack/react-query';
|
| 6 |
+
import { useMemo } from 'react';
|
| 7 |
+
|
| 8 |
+
// RefreshMoment用のパラメータ型定義
|
| 9 |
+
interface UseRefreshMomentParams {
|
| 10 |
+
commonDict?: Record<string, unknown> | object | null;
|
| 11 |
+
scoreDict?: Record<string, unknown> | object | null;
|
| 12 |
+
swot?: Record<string, unknown> | object | null;
|
| 13 |
+
userEmail?: string;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
// 変換後のデータ型
|
| 17 |
+
export interface RefreshMomentResult {
|
| 18 |
+
moments: Array<{ text: string; value: boolean }>;
|
| 19 |
+
ownTheme: string;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
/**
|
| 23 |
+
* RefreshMoment(改善案の方向性)を取得するフック
|
| 24 |
+
* AIが生成する追加の方向性選択肢とownThemeを取得
|
| 25 |
+
*/
|
| 26 |
+
export function useRefreshMoment(params: UseRefreshMomentParams) {
|
| 27 |
+
const { commonDict, scoreDict, swot, userEmail } = params;
|
| 28 |
+
const { userIdentifier } = useGlobalStore();
|
| 29 |
+
|
| 30 |
+
// オブジェクトをハッシュ化して安定したキーを生成
|
| 31 |
+
const queryKeyHash = useMemo(() => {
|
| 32 |
+
return generateSelectionHash({
|
| 33 |
+
commonDict,
|
| 34 |
+
scoreDict,
|
| 35 |
+
swot,
|
| 36 |
+
userEmail,
|
| 37 |
+
});
|
| 38 |
+
}, [commonDict, scoreDict, swot, userEmail]);
|
| 39 |
+
|
| 40 |
+
return useQuery({
|
| 41 |
+
queryKey: ['refreshMoment', queryKeyHash],
|
| 42 |
+
...createQueryOptions<[string, string]>({
|
| 43 |
+
queryFn: async () => {
|
| 44 |
+
if (!commonDict || !scoreDict || !swot) {
|
| 45 |
+
throw new Error('Required parameters are missing');
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
const response = await rpcClient['gradio-proxy']['refresh-moment'].$post({
|
| 49 |
+
json: {
|
| 50 |
+
commonDict: commonDict as Record<string, unknown>,
|
| 51 |
+
scoreDict: scoreDict as Record<string, unknown>,
|
| 52 |
+
swot: swot as Record<string, unknown>,
|
| 53 |
+
dummyMode: false,
|
| 54 |
+
userEmail: userEmail ?? undefined,
|
| 55 |
+
userIdentifier,
|
| 56 |
+
},
|
| 57 |
+
});
|
| 58 |
+
|
| 59 |
+
if (!response.ok) {
|
| 60 |
+
console.error('[useRefreshMoment] Response status:', response.status);
|
| 61 |
+
const errorData = (await response.json()) as { message?: string; status?: string };
|
| 62 |
+
console.error('[useRefreshMoment] API エラー:', errorData);
|
| 63 |
+
throw new Error(errorData.message || 'refresh_moment APIの呼び出しに失敗しました');
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
const data = await response.json();
|
| 67 |
+
return data as [string, string];
|
| 68 |
+
},
|
| 69 |
+
enabled: !!commonDict && !!scoreDict && !!swot,
|
| 70 |
+
}),
|
| 71 |
+
select: (data): RefreshMomentResult => {
|
| 72 |
+
// データを整形してから返す
|
| 73 |
+
const responseArray = data as unknown as [string, string] | undefined;
|
| 74 |
+
const moments =
|
| 75 |
+
responseArray?.[0]
|
| 76 |
+
?.split('\n')
|
| 77 |
+
.map((moment: string) => ({
|
| 78 |
+
text: moment.trim(),
|
| 79 |
+
value: false,
|
| 80 |
+
}))
|
| 81 |
+
.filter((m: { text: string }) => m.text) || [];
|
| 82 |
+
|
| 83 |
+
const ownTheme = responseArray?.[1] || '';
|
| 84 |
+
|
| 85 |
+
return {
|
| 86 |
+
moments,
|
| 87 |
+
ownTheme,
|
| 88 |
+
};
|
| 89 |
+
},
|
| 90 |
+
});
|
| 91 |
+
}
|
api-client/screenshot.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ScreenshotRequest, ScreenshotResponse } from '@/schema/proposal';
|
| 2 |
+
import { AppError, parseApiError } from '@/lib/errors';
|
| 3 |
+
|
| 4 |
+
export const screenshotApi = {
|
| 5 |
+
capture: async (request: ScreenshotRequest & { dummyMode?: boolean }): Promise<ScreenshotResponse> => {
|
| 6 |
+
try {
|
| 7 |
+
// GETメソッドを使用
|
| 8 |
+
const params = new URLSearchParams({
|
| 9 |
+
url: request.url,
|
| 10 |
+
width: request.width?.toString() || '1920',
|
| 11 |
+
height: request.height?.toString() || '1080',
|
| 12 |
+
...(request.dummyMode !== undefined && { dummyMode: request.dummyMode.toString() }),
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
const response = await fetch(`/api/rpc/screenshot?${params.toString()}`, {
|
| 16 |
+
method: 'GET',
|
| 17 |
+
headers: {
|
| 18 |
+
'Content-Type': 'application/json',
|
| 19 |
+
},
|
| 20 |
+
});
|
| 21 |
+
|
| 22 |
+
if (!response.ok) {
|
| 23 |
+
const errorData = await response.json();
|
| 24 |
+
throw parseApiError(errorData);
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
const data = await response.json();
|
| 28 |
+
|
| 29 |
+
// レスポンスフォーマットを統一
|
| 30 |
+
return {
|
| 31 |
+
success: data.success,
|
| 32 |
+
screenshotBase64: data.imageBase64 || '',
|
| 33 |
+
mimeType: data.mimeType || 'image/jpeg',
|
| 34 |
+
description: '',
|
| 35 |
+
};
|
| 36 |
+
} catch (error) {
|
| 37 |
+
// ネットワークエラーやタイムアウトを捕捉
|
| 38 |
+
if (error instanceof TypeError && error.message.includes('fetch')) {
|
| 39 |
+
throw new AppError(
|
| 40 |
+
'ネットワークエラーが発生しました',
|
| 41 |
+
'NETWORK_ERROR',
|
| 42 |
+
503,
|
| 43 |
+
);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// 既にAppErrorの場合はそのまま投げる
|
| 47 |
+
if (error instanceof AppError) {
|
| 48 |
+
throw error;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// その他のエラー
|
| 52 |
+
throw new AppError(
|
| 53 |
+
'スクリーンショットの取得に失敗しました',
|
| 54 |
+
'SCREENSHOT_FAILED',
|
| 55 |
+
500,
|
| 56 |
+
error,
|
| 57 |
+
);
|
| 58 |
+
}
|
| 59 |
+
},
|
| 60 |
+
};
|
api-client/theme-extraction/index.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ThemeExtractionRequest, ThemeExtractionResponse } from '@/schema/theme-extraction';
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* テーマ抽出API
|
| 5 |
+
*/
|
| 6 |
+
export const themeExtractionApi = {
|
| 7 |
+
/**
|
| 8 |
+
* テーマを抽出
|
| 9 |
+
*/
|
| 10 |
+
async extractTheme(request: ThemeExtractionRequest): Promise<ThemeExtractionResponse> {
|
| 11 |
+
const response = await fetch('/api/rpc/theme-extraction', {
|
| 12 |
+
method: 'POST',
|
| 13 |
+
headers: {
|
| 14 |
+
'Content-Type': 'application/json',
|
| 15 |
+
},
|
| 16 |
+
body: JSON.stringify(request),
|
| 17 |
+
});
|
| 18 |
+
|
| 19 |
+
if (!response.ok) {
|
| 20 |
+
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
|
| 21 |
+
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
const data = await response.json();
|
| 25 |
+
return data;
|
| 26 |
+
},
|
| 27 |
+
|
| 28 |
+
/**
|
| 29 |
+
* 利用可能なプロバイダーを取得
|
| 30 |
+
*/
|
| 31 |
+
async getProviders(): Promise<{ success: boolean; data?: { providers: string[]; default: string }; error?: string }> {
|
| 32 |
+
const response = await fetch('/api/rpc/theme-extraction/providers', {
|
| 33 |
+
method: 'GET',
|
| 34 |
+
headers: {
|
| 35 |
+
'Content-Type': 'application/json',
|
| 36 |
+
},
|
| 37 |
+
});
|
| 38 |
+
|
| 39 |
+
if (!response.ok) {
|
| 40 |
+
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
|
| 41 |
+
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
return response.json();
|
| 45 |
+
},
|
| 46 |
+
|
| 47 |
+
/**
|
| 48 |
+
* ヘルスチェック
|
| 49 |
+
*/
|
| 50 |
+
async healthCheck(): Promise<{ success: boolean; status: string; data?: any; error?: string }> {
|
| 51 |
+
const response = await fetch('/api/rpc/theme-extraction/health', {
|
| 52 |
+
method: 'GET',
|
| 53 |
+
headers: {
|
| 54 |
+
'Content-Type': 'application/json',
|
| 55 |
+
},
|
| 56 |
+
});
|
| 57 |
+
|
| 58 |
+
if (!response.ok) {
|
| 59 |
+
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
|
| 60 |
+
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
return response.json();
|
| 64 |
+
},
|
| 65 |
+
};
|
api-client/theme-extraction/queries.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ThemeExtractionRequest, ThemeExtractionResponse } from '@/schema/theme-extraction';
|
| 2 |
+
import { useMutation, useQuery } from '@tanstack/react-query';
|
| 3 |
+
import { themeExtractionApi } from './index';
|
| 4 |
+
|
| 5 |
+
/**
|
| 6 |
+
* テーマ抽出用のReact Queryフック
|
| 7 |
+
*/
|
| 8 |
+
export const useThemeExtraction = () => {
|
| 9 |
+
return useMutation<ThemeExtractionResponse, Error, ThemeExtractionRequest>({
|
| 10 |
+
mutationFn: themeExtractionApi.extractTheme,
|
| 11 |
+
onSuccess: (data) => {
|
| 12 |
+
console.log('[useThemeExtraction] Theme extraction completed:', data);
|
| 13 |
+
},
|
| 14 |
+
onError: (error) => {
|
| 15 |
+
console.error('[useThemeExtraction] Theme extraction failed:', error);
|
| 16 |
+
},
|
| 17 |
+
});
|
| 18 |
+
};
|
| 19 |
+
|
| 20 |
+
/**
|
| 21 |
+
* 利用可能なプロバイダー取得用のReact Queryフック
|
| 22 |
+
*/
|
| 23 |
+
export const useThemeExtractionProviders = (enabled: boolean = true) => {
|
| 24 |
+
return useQuery({
|
| 25 |
+
queryKey: ['theme-extraction', 'providers'],
|
| 26 |
+
queryFn: themeExtractionApi.getProviders,
|
| 27 |
+
enabled,
|
| 28 |
+
staleTime: 5 * 60 * 1000, // 5分間キャッシュ
|
| 29 |
+
retry: 2,
|
| 30 |
+
});
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
/**
|
| 34 |
+
* テーマ抽出ヘルスチェック用のReact Queryフック
|
| 35 |
+
*/
|
| 36 |
+
export const useThemeExtractionHealth = (enabled: boolean = false) => {
|
| 37 |
+
return useQuery({
|
| 38 |
+
queryKey: ['theme-extraction', 'health'],
|
| 39 |
+
queryFn: themeExtractionApi.healthCheck,
|
| 40 |
+
enabled,
|
| 41 |
+
staleTime: 1 * 60 * 1000, // 1分間キャッシュ
|
| 42 |
+
retry: 1,
|
| 43 |
+
});
|
| 44 |
+
};
|
api-client/theme/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ThemeExtractionResult } from '@/schema/theme-extraction';
|
| 2 |
+
|
| 3 |
+
export interface GenerateThemeCssResponse {
|
| 4 |
+
success: boolean;
|
| 5 |
+
css?: string;
|
| 6 |
+
error?: string;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
/**
|
| 10 |
+
* テーマデータからCSSを生成
|
| 11 |
+
*/
|
| 12 |
+
export async function generateThemeCss(themeData: ThemeExtractionResult): Promise<GenerateThemeCssResponse> {
|
| 13 |
+
try {
|
| 14 |
+
const response = await fetch('/api/rpc/theme-extraction/generate-css', {
|
| 15 |
+
method: 'POST',
|
| 16 |
+
headers: {
|
| 17 |
+
'Content-Type': 'application/json',
|
| 18 |
+
},
|
| 19 |
+
body: JSON.stringify(themeData),
|
| 20 |
+
});
|
| 21 |
+
|
| 22 |
+
if (!response.ok) {
|
| 23 |
+
const errorData = await response.json();
|
| 24 |
+
throw new Error(errorData.error || 'CSS生成に失敗しました');
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
const data: GenerateThemeCssResponse = await response.json();
|
| 28 |
+
return data;
|
| 29 |
+
} catch (error) {
|
| 30 |
+
console.error('[generateThemeCss] Error:', error);
|
| 31 |
+
return {
|
| 32 |
+
success: false,
|
| 33 |
+
error: error instanceof Error ? error.message : 'CSS生成中にエラーが発生しました',
|
| 34 |
+
};
|
| 35 |
+
}
|
| 36 |
+
}
|
api-client/theme/queries.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createQueryOptions } from '@/api-client/query-config';
|
| 2 |
+
import type { ThemeExtractionResult } from '@/schema/theme-extraction';
|
| 3 |
+
import { useQuery } from '@tanstack/react-query';
|
| 4 |
+
import { generateThemeCss, type GenerateThemeCssResponse } from './index';
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* テーマCSS生成フック
|
| 8 |
+
* テーマデータが渡された場合のみCSS生成を実行
|
| 9 |
+
*/
|
| 10 |
+
export function useThemeCss(themeData: ThemeExtractionResult | null, enabled: boolean = true) {
|
| 11 |
+
return useQuery<GenerateThemeCssResponse>({
|
| 12 |
+
...createQueryOptions<GenerateThemeCssResponse>(),
|
| 13 |
+
queryKey: ['themeCss', themeData ? JSON.stringify(themeData) : 'no-theme'],
|
| 14 |
+
queryFn: () => {
|
| 15 |
+
if (!themeData) {
|
| 16 |
+
return {
|
| 17 |
+
success: true,
|
| 18 |
+
css: undefined,
|
| 19 |
+
};
|
| 20 |
+
}
|
| 21 |
+
return generateThemeCss(themeData);
|
| 22 |
+
},
|
| 23 |
+
enabled: enabled && !!themeData,
|
| 24 |
+
staleTime: Infinity, // テーマデータが同じならキャッシュを使う
|
| 25 |
+
});
|
| 26 |
+
}
|