# 🚀 Phase 2: Implementation Plan

**Status**: Ready to Start  
**Last Updated**: 2026-01-21  
**Estimated Total Time**: 26 hours  
**Target Completion**: Before release (1月31日)

---

## 📊 Overview

Phase 2 focuses on **enhancing user experience** with three major features:

1. **Help Button Placement** (Pattern C: Hybrid) - 6 hours
2. **AI-Powered Recommendations** ("あわせて飲みたい") - 12 hours
3. **AI Analysis Info Editing** - 8 hours

**Total**: 26 hours (~3-4 days of focused work)

---

## 🎯 Feature 1: Help Button Placement (Pattern C: Hybrid)

### Problem
Current state:
- Single "ガイド・ヘルプ" button in soul_screen.dart
- User must navigate away from context to find help
- No contextual help for specific features

### Solution: Pattern C (Hybrid Approach)

**Keep**: Global help button in AppBar  
**Add**: Contextual "?" icons next to relevant sections

### User Research
**Best Practice Examples**:
- Duolingo: ? icons next to complex features
- Notion: Contextual help in modals
- GitHub: ? icons in settings pages
- Figma: Help tooltips on hover

**User Feedback**:
> "少なくとも説明対象の該当タブ内（現在の称号、バッジケースはマイページ、酒向タイプはソムリエタブ）に配置した方がわかりやすいよね？"

### Implementation

#### Step 1: Create Reusable Help Icon Widget (1 hour)

Create `lib/widgets/contextual_help_icon.dart`:

```dart
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';

/// Contextual help icon that shows a bottom sheet with help content
class ContextualHelpIcon extends StatelessWidget {
  final String title;
  final String content;
  final Widget? customContent; // For complex help (tables, images)

  const ContextualHelpIcon({
    super.key,
    required this.title,
    required this.content,
    this.customContent,
  });

  @override
  Widget build(BuildContext context) {
    return IconButton(
      icon: Icon(
        LucideIcons.helpCircle,
        size: 18,
        color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
      ),
      tooltip: 'ヘルプを表示',
      onPressed: () => _showHelpSheet(context),
    );
  }

  void _showHelpSheet(BuildContext context) {
    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
      ),
      builder: (context) => DraggableScrollableSheet(
        initialChildSize: 0.6,
        minChildSize: 0.4,
        maxChildSize: 0.9,
        expand: false,
        builder: (context, scrollController) => _HelpSheetContent(
          title: title,
          content: content,
          customContent: customContent,
          scrollController: scrollController,
        ),
      ),
    );
  }
}

class _HelpSheetContent extends StatelessWidget {
  final String title;
  final String content;
  final Widget? customContent;
  final ScrollController scrollController;

  const _HelpSheetContent({
    required this.title,
    required this.content,
    this.customContent,
    required this.scrollController,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(24),
      child: ListView(
        controller: scrollController,
        children: [
          // Drag handle
          Center(
            child: Container(
              width: 40,
              height: 4,
              margin: const EdgeInsets.only(bottom: 16),
              decoration: BoxDecoration(
                color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.2),
                borderRadius: BorderRadius.circular(2),
              ),
            ),
          ),
          
          // Title
          Row(
            children: [
              Icon(
                LucideIcons.helpCircle,
                color: Theme.of(context).colorScheme.primary,
              ),
              const SizedBox(width: 12),
              Expanded(
                child: Text(
                  title,
                  style: Theme.of(context).textTheme.titleLarge?.copyWith(
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ),
            ],
          ),
          
          const SizedBox(height: 16),
          const Divider(),
          const SizedBox(height: 16),
          
          // Content
          if (customContent != null)
            customContent!
          else
            Text(
              content,
              style: Theme.of(context).textTheme.bodyLarge?.copyWith(
                height: 1.6,
              ),
            ),
          
          const SizedBox(height: 24),
          
          // Close button
          FilledButton.tonal(
            onPressed: () => Navigator.pop(context),
            child: const Text('閉じる'),
          ),
        ],
      ),
    );
  }
}
```

#### Step 2: Add to soul_screen.dart (2 hours)

**Location 1: Level & Title Card**

```dart
// In soul_screen.dart, modify the LevelTitleCard header
Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: [
    Text(
      '現在の称号',
      style: Theme.of(context).textTheme.titleMedium?.copyWith(
        fontWeight: FontWeight.bold,
      ),
    ),
    ContextualHelpIcon(
      title: 'レベルと称号について',
      customContent: _buildLevelHelpContent(context),
    ),
  ],
)

Widget _buildLevelHelpContent(BuildContext context) {
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text(
        'レベルの上げ方',
        style: Theme.of(context).textTheme.titleSmall?.copyWith(
          fontWeight: FontWeight.bold,
        ),
      ),
      const SizedBox(height: 8),
      Text(
        '日本酒を1本登録するごとに 10 EXP 獲得できます。\nメニューを作成するとボーナスが入ることも！',
        style: Theme.of(context).textTheme.bodyMedium,
      ),
      const SizedBox(height: 16),
      Text(
        '称号一覧',
        style: Theme.of(context).textTheme.titleSmall?.copyWith(
          fontWeight: FontWeight.bold,
        ),
      ),
      const SizedBox(height: 8),
      _buildLevelTable(context),
    ],
  );
}
```

**Location 2: Badge Case**

```dart
// In soul_screen.dart, modify the BadgeCase header
Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: [
    Text(
      'バッジコレクション',
      style: Theme.of(context).textTheme.titleMedium?.copyWith(
        fontWeight: FontWeight.bold,
      ),
    ),
    ContextualHelpIcon(
      title: 'バッジについて',
      content: '''バッジは特定の条件を達成すると獲得できます。

例：
🍶 初めての一歩: 最初の日本酒を登録
👹 東北制覇: 東北6県すべての日本酒を登録
🌶️ 辛口党: 日本酒度+5以上の辛口酒を10本登録

バッジを集めて、日本酒マスターを目指しましょう！''',
    ),
  ],
)
```

#### Step 3: Add to sommelier_screen.dart (2 hours)

**Location: Sake Type Chart**

```dart
// In sommelier_screen.dart, add help icon next to chart title
Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: [
    Text(
      'あなたの酒向タイプ',
      style: Theme.of(context).textTheme.titleLarge?.copyWith(
        fontWeight: FontWeight.bold,
      ),
    ),
    ContextualHelpIcon(
      title: 'チャートの見方',
      customContent: _buildChartHelpContent(context),
    ),
  ],
)

Widget _buildChartHelpContent(BuildContext context) {
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text(
        'AIがあなたの登録した日本酒の味覚データを分析し、好みの傾向をチャート化します。',
        style: Theme.of(context).textTheme.bodyMedium,
      ),
      const SizedBox(height: 16),
      _buildChartAxisExplanation(context, '華やか', '香りが高い、フルーティー'),
      _buildChartAxisExplanation(context, '芳醇', 'コク・旨味が強い、濃厚'),
      _buildChartAxisExplanation(context, '重厚', '苦味やボディ感、飲みごたえ'),
      _buildChartAxisExplanation(context, '穏やか', 'アルコール感が控えめ、優しい'),
      _buildChartAxisExplanation(context, '軽快', 'さっぱり、爽やか'),
    ],
  );
}

Widget _buildChartAxisExplanation(BuildContext context, String axis, String description) {
  return Padding(
    padding: const EdgeInsets.only(bottom: 12),
    child: Row(
      children: [
        Container(
          width: 8,
          height: 8,
          decoration: BoxDecoration(
            color: Theme.of(context).colorScheme.primary,
            shape: BoxShape.circle,
          ),
        ),
        const SizedBox(width: 12),
        Expanded(
          child: RichText(
            text: TextSpan(
              style: Theme.of(context).textTheme.bodyMedium,
              children: [
                TextSpan(
                  text: '$axis: ',
                  style: const TextStyle(fontWeight: FontWeight.bold),
                ),
                TextSpan(text: description),
              ],
            ),
          ),
        ),
      ],
    ),
  );
}
```

#### Step 4: Testing (1 hour)

**Test Cases**:
- [ ] Help icon visible in all 3 locations
- [ ] Bottom sheet opens smoothly
- [ ] Content displays correctly in Light mode
- [ ] Content displays correctly in Dark mode
- [ ] Draggable sheet works (resize)
- [ ] Close button works
- [ ] Tap outside to dismiss works

---

## 🤖 Feature 2: AI-Powered "あわせて飲みたい" Recommendations

### Current State (Problem)
```dart
// lib/services/sake_recommendation_service.dart:25
List<SakeItem> getRecommendations(SakeItem currentSake) {
  // 現在: ユーザーの登録済みデータから類似を探すだけ
  return allSakes.where((sake) => 
    sake.key != currentSake.key &&
    _isSimilar(sake, currentSake)
  ).take(3).toList();
}
```

**Limitations**:
- Only recommends from user's existing collection
- No "discovery" of new sake
- Simple similarity matching (not AI-powered)

### User Feedback
> "「あわせて飲みたい」の実用的な実装も優先度が高いです"  
> "未知の銘柄のおすすめの方がニーズがあると思います"

### Solution: Gemini API-Powered Recommendations

**Two-Tier Approach**:
1. **Tier 1**: Similar from user's collection (fast, offline)
2. **Tier 2**: AI-generated recommendations from Gemini API (slow, online)

### Implementation

#### Step 1: Enhance SakeRecommendationService (4 hours)

Modify `lib/services/sake_recommendation_service.dart`:

```dart
import 'package:google_generative_ai/google_generative_ai.dart';
import '../secrets.dart';
import 'analysis_cache_service.dart'; // Reuse cache

class SakeRecommendationService {
  static final _gemini = GenerativeModel(
    model: 'gemini-2.0-flash-exp', // Fast, cheap model for recommendations
    apiKey: geminiApiKey,
  );

  /// Get recommendations with two-tier approach
  static Future<RecommendationResult> getRecommendations({
    required SakeItem currentSake,
    required List<SakeItem> allUserSakes,
    bool includeAiRecommendations = true,
  }) async {
    // Tier 1: Similar from user's collection (always fast)
    final similarFromCollection = _getSimilarFromCollection(
      currentSake: currentSake,
      allSakes: allUserSakes,
    );

    // Tier 2: AI recommendations (optional, cached)
    List<AiRecommendation>? aiRecommendations;
    if (includeAiRecommendations) {
      aiRecommendations = await _getAiRecommendations(
        currentSake: currentSake,
        userTasteProfile: _buildTasteProfile(allUserSakes),
      );
    }

    return RecommendationResult(
      similarFromCollection: similarFromCollection,
      aiRecommendations: aiRecommendations ?? [],
    );
  }

  /// Tier 1: Fast similarity matching
  static List<SakeItem> _getSimilarFromCollection({
    required SakeItem currentSake,
    required List<SakeItem> allSakes,
  }) {
    // Existing logic (kept as fallback)
    return allSakes
        .where((sake) => sake.key != currentSake.key)
        .map((sake) => (sake: sake, score: _calculateSimilarity(sake, currentSake)))
        .where((record) => record.score > 0.6)
        .toList()
      ..sort((a, b) => b.score.compareTo(a.score));
    return sorted.take(3).map((r) => r.sake).toList();
  }

  /// Tier 2: AI-powered recommendations
  static Future<List<AiRecommendation>> _getAiRecommendations({
    required SakeItem currentSake,
    required TasteProfile userTasteProfile,
  }) async {
    // Cache key
    final cacheKey = 'rec_${currentSake.key}_${userTasteProfile.hashCode}';
    
    // Check cache first
    final cached = await AnalysisCacheService.getCachedAnalysis(cacheKey);
    if (cached != null) {
      debugPrint('💰 AI recommendations cache hit');
      return _parseAiRecommendations(cached);
    }

    // Call Gemini API
    final prompt = _buildRecommendationPrompt(currentSake, userTasteProfile);
    
    try {
      final response = await _gemini.generateContent([Content.text(prompt)]);
      final text = response.text ?? '';
      
      // Cache result
      await AnalysisCacheService.cacheAnalysis(cacheKey, text);
      
      return _parseAiRecommendations(text);
    } catch (e) {
      debugPrint('⚠️ AI recommendation failed: $e');
      return []; // Return empty, Tier 1 will still work
    }
  }

  static String _buildRecommendationPrompt(
    SakeItem currentSake,
    TasteProfile userTasteProfile,
  ) {
    return '''あなたは日本酒ソムリエです。以下の情報に基づいて、おすすめの日本酒を3つ提案してください。

【現在見ている日本酒】
- 名前: ${currentSake.displayData.name}
- 蔵元: ${currentSake.displayData.brewery ?? '不明'}
- 地域: ${currentSake.displayData.prefecture ?? '不明'}
- タイプ: ${currentSake.hiddenSpecs.type ?? '不明'}
- 甘辛度: ${currentSake.hiddenSpecs.sweetnessScore?.toStringAsFixed(1) ?? '不明'}
- 濃淡度: ${currentSake.hiddenSpecs.bodyScore?.toStringAsFixed(1) ?? '不明'}

【ユーザーの好みの傾向】
- 平均甘辛度: ${userTasteProfile.avgSweetness.toStringAsFixed(1)}
- 平均濃淡度: ${userTasteProfile.avgBody.toStringAsFixed(1)}
- 好きな地域: ${userTasteProfile.favoritePrefectures.join(', ')}
- 好きなタイプ: ${userTasteProfile.favoriteTypes.join(', ')}

【指示】
1. この日本酒の特徴を踏まえて、「一緒に飲むと良い」日本酒を3つ提案してください
2. 提案は「対比」と「相乗効果」の両方を考慮してください
   - 対比: 全く違う味わいで新しい発見
   - 相乗効果: 似た系統で深掘り
3. 実在する銘柄を提案してください（創作は不可）
4. 各提案には以下を含めてください：
   - 銘柄名
   - 蔵元名
   - 地域（都道府県）
   - おすすめ理由（30文字以内）

【出力形式】
以下のJSON形式で出力してください：
```json
[
  {
    "name": "銘柄名",
    "brewery": "蔵元名",
    "prefecture": "都道府県",
    "reason": "おすすめ理由"
  },
  ...
]
```''';
  }

  static List<AiRecommendation> _parseAiRecommendations(String jsonText) {
    try {
      // Extract JSON from markdown code block if present
      final jsonMatch = RegExp(r'```json\s*(\[.*?\])\s*```', dotAll: true).firstMatch(jsonText);
      final jsonString = jsonMatch?.group(1) ?? jsonText;
      
      final List<dynamic> parsed = jsonDecode(jsonString);
      return parsed.map((item) => AiRecommendation.fromJson(item)).toList();
    } catch (e) {
      debugPrint('⚠️ Failed to parse AI recommendations: $e');
      return [];
    }
  }

  static TasteProfile _buildTasteProfile(List<SakeItem> allSakes) {
    if (allSakes.isEmpty) {
      return TasteProfile.empty();
    }

    final validSweetness = allSakes
        .where((s) => s.hiddenSpecs.sweetnessScore != null)
        .map((s) => s.hiddenSpecs.sweetnessScore!)
        .toList();
    
    final validBody = allSakes
        .where((s) => s.hiddenSpecs.bodyScore != null)
        .map((s) => s.hiddenSpecs.bodyScore!)
        .toList();

    // Most common prefectures
    final prefectureCounts = <String, int>{};
    for (final sake in allSakes) {
      final pref = sake.displayData.prefecture;
      if (pref != null && pref.isNotEmpty) {
        prefectureCounts[pref] = (prefectureCounts[pref] ?? 0) + 1;
      }
    }
    final favoritePrefectures = (prefectureCounts.entries.toList()
      ..sort((a, b) => b.value.compareTo(a.value)))
        .take(3)
        .map((e) => e.key)
        .toList();

    // Most common types
    final typeCounts = <String, int>{};
    for (final sake in allSakes) {
      final type = sake.hiddenSpecs.type;
      if (type != null && type.isNotEmpty) {
        typeCounts[type] = (typeCounts[type] ?? 0) + 1;
      }
    }
    final favoriteTypes = (typeCounts.entries.toList()
      ..sort((a, b) => b.value.compareTo(a.value)))
        .take(3)
        .map((e) => e.key)
        .toList();

    return TasteProfile(
      avgSweetness: validSweetness.isEmpty ? 0.0 : validSweetness.reduce((a, b) => a + b) / validSweetness.length,
      avgBody: validBody.isEmpty ? 0.0 : validBody.reduce((a, b) => a + b) / validBody.length,
      favoritePrefectures: favoritePrefectures,
      favoriteTypes: favoriteTypes,
    );
  }
}

// Models
class RecommendationResult {
  final List<SakeItem> similarFromCollection;
  final List<AiRecommendation> aiRecommendations;

  RecommendationResult({
    required this.similarFromCollection,
    required this.aiRecommendations,
  });
}

class AiRecommendation {
  final String name;
  final String brewery;
  final String prefecture;
  final String reason;

  AiRecommendation({
    required this.name,
    required this.brewery,
    required this.prefecture,
    required this.reason,
  });

  factory AiRecommendation.fromJson(Map<String, dynamic> json) {
    return AiRecommendation(
      name: json['name'] as String,
      brewery: json['brewery'] as String,
      prefecture: json['prefecture'] as String,
      reason: json['reason'] as String,
    );
  }
}

class TasteProfile {
  final double avgSweetness;
  final double avgBody;
  final List<String> favoritePrefectures;
  final List<String> favoriteTypes;

  TasteProfile({
    required this.avgSweetness,
    required this.avgBody,
    required this.favoritePrefectures,
    required this.favoriteTypes,
  });

  factory TasteProfile.empty() {
    return TasteProfile(
      avgSweetness: 0.0,
      avgBody: 0.0,
      favoritePrefectures: [],
      favoriteTypes: [],
    );
  }

  @override
  int get hashCode => Object.hash(
    avgSweetness.toStringAsFixed(1),
    avgBody.toStringAsFixed(1),
    favoritePrefectures.join(','),
    favoriteTypes.join(','),
  );
}
```

#### Step 2: Update UI in sake_detail_screen.dart (4 hours)

Modify the "あわせて飲みたい" section:

```dart
// In sake_detail_screen.dart, replace existing recommendation section
Widget _buildRecommendationsSection() {
  return FutureBuilder<RecommendationResult>(
    future: SakeRecommendationService.getRecommendations(
      currentSake: _sake,
      allUserSakes: ref.watch(rawSakeListItemsProvider).valueOrNull ?? [],
      includeAiRecommendations: true,
    ),
    builder: (context, snapshot) {
      if (snapshot.connectionState == ConnectionState.waiting) {
        return _buildRecommendationLoadingState();
      }

      final result = snapshot.data;
      if (result == null) {
        return _buildRecommendationErrorState();
      }

      final hasSimilar = result.similarFromCollection.isNotEmpty;
      final hasAi = result.aiRecommendations.isNotEmpty;

      if (!hasSimilar && !hasAi) {
        return _buildRecommendationEmptyState();
      }

      return Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // Header
          Row(
            children: [
              Icon(LucideIcons.sparkles, color: Theme.of(context).colorScheme.primary),
              const SizedBox(width: 8),
              Expanded(
                child: Text(
                  'あわせて飲みたい',
                  style: Theme.of(context).textTheme.titleMedium?.copyWith(
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ),
              ContextualHelpIcon(
                title: '「あわせて飲みたい」について',
                content: '''この日本酒と一緒に楽しむのにおすすめの銘柄を提案します。

🍶 あなたのコレクションから
登録済みの日本酒から、似た味わいの銘柄を表示します。

✨ AIのおすすめ
味の傾向を分析して、新しい銘柄を提案します。
実際に購入する際の参考にしてください。''',
              ),
            ],
          ),
          
          const SizedBox(height: 16),
          
          // Tier 1: Similar from collection
          if (hasSimilar) ...[
            Text(
              '🍶 あなたのコレクションから',
              style: Theme.of(context).textTheme.titleSmall?.copyWith(
                color: Theme.of(context).colorScheme.secondary,
              ),
            ),
            const SizedBox(height: 8),
            ...result.similarFromCollection.map((sake) => _buildSimilarCard(sake)),
            const SizedBox(height: 16),
          ],
          
          // Tier 2: AI recommendations
          if (hasAi) ...[
            Text(
              '✨ AIのおすすめ（未登録の銘柄）',
              style: Theme.of(context).textTheme.titleSmall?.copyWith(
                color: Theme.of(context).colorScheme.primary,
              ),
            ),
            const SizedBox(height: 8),
            ...result.aiRecommendations.map((rec) => _buildAiRecommendationCard(rec)),
          ],
        ],
      );
    },
  );
}

Widget _buildAiRecommendationCard(AiRecommendation rec) {
  return Card(
    margin: const EdgeInsets.only(bottom: 8),
    child: ListTile(
      leading: Container(
        width: 40,
        height: 40,
        decoration: BoxDecoration(
          color: Theme.of(context).colorScheme.primaryContainer,
          shape: BoxShape.circle,
        ),
        child: Icon(
          LucideIcons.sparkles,
          size: 20,
          color: Theme.of(context).colorScheme.onPrimaryContainer,
        ),
      ),
      title: Text(
        rec.name,
        style: const TextStyle(fontWeight: FontWeight.bold),
      ),
      subtitle: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const SizedBox(height: 4),
          Text('${rec.brewery} / ${rec.prefecture}'),
          const SizedBox(height: 4),
          Text(
            rec.reason,
            style: TextStyle(
              color: Theme.of(context).colorScheme.secondary,
              fontSize: 12,
            ),
          ),
        ],
      ),
      trailing: IconButton(
        icon: const Icon(LucideIcons.search),
        tooltip: 'ネットで検索',
        onPressed: () => _searchOnline(rec.name),
      ),
    ),
  );
}

void _searchOnline(String sakeName) async {
  final query = Uri.encodeComponent('$sakeName 日本酒 購入');
  final url = 'https://www.google.com/search?q=$query';
  // Open in browser (use url_launcher package)
  // await launchUrl(Uri.parse(url));
  
  // For now, just show snackbar
  if (mounted) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('「$sakeName」を検索...')),
    );
  }
}

Widget _buildRecommendationLoadingState() {
  return Center(
    child: Column(
      children: [
        const CircularProgressIndicator(),
        const SizedBox(height: 16),
        Text(
          'AIがおすすめを分析中...',
          style: Theme.of(context).textTheme.bodySmall,
        ),
      ],
    ),
  );
}

Widget _buildRecommendationEmptyState() {
  return Center(
    child: Column(
      children: [
        Icon(
          LucideIcons.info,
          color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.3),
          size: 48,
        ),
        const SizedBox(height: 16),
        Text(
          '関連する日本酒を追加すると\nおすすめが表示されます',
          textAlign: TextAlign.center,
          style: Theme.of(context).textTheme.bodyMedium?.copyWith(
            color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
          ),
        ),
      ],
    ),
  );
}
```

#### Step 3: Testing & Caching (2 hours)

**Test Scenarios**:
1. **No collection**: Show AI recommendations only
2. **Small collection (1-3 items)**: Show similar + AI
3. **Large collection (50+ items)**: Show similar (Tier 1) first, AI loads async
4. **Offline mode**: Tier 1 works, Tier 2 fails gracefully
5. **Cache hit**: Fast loading (< 100ms)
6. **API limit reached**: Fallback to Tier 1 only

**Cache Strategy**:
- Cache key: `rec_{sake_key}_{taste_profile_hash}`
- TTL: 7 days (taste profile changes slowly)
- Invalidation: When user adds/removes sake

#### Step 4: pubspec.yaml Update (1 hour)

Add required packages:

```yaml
dependencies:
  url_launcher: ^6.2.2 # For "ネットで検索" button
```

#### Step 5: Cost Analysis & Monitoring (1 hour)

**Gemini API Cost**:
- Model: `gemini-2.0-flash-exp` (cheapest)
- Tokens per request: ~500 input + 300 output = 800 total
- Cost: ~$0.0001 per request
- With caching: ~90% cache hit rate
- Daily cost for 100 users: ~$0.01-0.02

**Monitoring**:
- Add analytics event: `recommendation_api_call`
- Track: Cache hit rate, response time, error rate

---

## ✏️ Feature 3: AI Analysis Info Editing

### Problem
Currently, users cannot correct AI mistakes:
- Wrong sake type ("純米大吟醸" vs "純米吟醸")
- Incorrect sweetness/body scores
- Missing information

### Solution: Inline Editing with Save

### Implementation

#### Step 1: Make Fields Editable (4 hours)

Modify `lib/screens/sake_detail_screen.dart`:

```dart
// Add state for editing mode
bool _isEditingAiInfo = false;

// Controllers for each field
late final TextEditingController _typeController;
late final TextEditingController _sweetnessController;
late final TextEditingController _bodyController;
late final TextEditingController _polishingController;
late final TextEditingController _alcoholController;
late final TextEditingController _sakeMeterController;
late final TextEditingController _riceController;
late final TextEditingController _yeastController;
late final TextEditingController _manufacturingController;

@override
void initState() {
  super.initState();
  // Initialize controllers
  _typeController = TextEditingController(text: _sake.hiddenSpecs.type);
  _sweetnessController = TextEditingController(
    text: _sake.hiddenSpecs.sweetnessScore?.toStringAsFixed(1) ?? '',
  );
  // ... (initialize all other controllers)
}

@override
void dispose() {
  // Dispose all controllers
  _typeController.dispose();
  _sweetnessController.dispose();
  // ... (dispose all controllers)
  super.dispose();
}

// Modify ExpansionTile to include edit button
ExpansionTile(
  leading: Icon(LucideIcons.sparkles, color: Theme.of(context).colorScheme.primary),
  title: Row(
    children: [
      Expanded(
        child: Text(
          'AIで分析された情報',
          style: Theme.of(context).textTheme.titleMedium?.copyWith(
            fontWeight: FontWeight.bold,
          ),
        ),
      ),
      if (!_isEditingAiInfo)
        IconButton(
          icon: const Icon(LucideIcons.edit2, size: 18),
          tooltip: '編集',
          onPressed: () => setState(() => _isEditingAiInfo = true),
        )
      else
        Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            TextButton(
              onPressed: _cancelAiInfoEdit,
              child: const Text('キャンセル'),
            ),
            FilledButton.icon(
              onPressed: _saveAiInfo,
              icon: const Icon(LucideIcons.save, size: 16),
              label: const Text('保存'),
            ),
          ],
        ),
    ],
  ),
  children: [
    Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      child: Column(
        children: [
          _buildEditableSpecRow('特定名称', _typeController, _isEditingAiInfo),
          _buildEditableSpecRow('甘辛度', _sweetnessController, _isEditingAiInfo, keyboardType: TextInputType.number),
          _buildEditableSpecRow('濃淡度', _bodyController, _isEditingAiInfo, keyboardType: TextInputType.number),
          const Divider(height: 24),
          _buildEditableSpecRow('精米歩合', _polishingController, _isEditingAiInfo, suffix: '%'),
          _buildEditableSpecRow('アルコール分', _alcoholController, _isEditingAiInfo, suffix: '度'),
          _buildEditableSpecRow('日本酒度', _sakeMeterController, _isEditingAiInfo),
          _buildEditableSpecRow('酒米', _riceController, _isEditingAiInfo),
          _buildEditableSpecRow('酵母', _yeastController, _isEditingAiInfo),
          _buildEditableSpecRow('製造年月', _manufacturingController, _isEditingAiInfo),
        ],
      ),
    ),
  ],
)

Widget _buildEditableSpecRow(
  String label,
  TextEditingController controller,
  bool isEditing, {
  TextInputType? keyboardType,
  String? suffix,
}) {
  return Padding(
    padding: const EdgeInsets.symmetric(vertical: 8),
    child: Row(
      children: [
        SizedBox(
          width: 100,
          child: Text(
            label,
            style: Theme.of(context).textTheme.bodyMedium?.copyWith(
              color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
            ),
          ),
        ),
        Expanded(
          child: isEditing
              ? TextField(
                  controller: controller,
                  keyboardType: keyboardType,
                  decoration: InputDecoration(
                    isDense: true,
                    suffixText: suffix,
                    border: const OutlineInputBorder(),
                  ),
                  style: Theme.of(context).textTheme.bodyMedium,
                )
              : Text(
                  controller.text.isEmpty ? '-' : '${controller.text}${suffix ?? ''}',
                  style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                    fontWeight: FontWeight.w500,
                  ),
                ),
        ),
      ],
    ),
  );
}

void _cancelAiInfoEdit() {
  setState(() {
    _isEditingAiInfo = false;
    // Reset controllers to original values
    _typeController.text = _sake.hiddenSpecs.type ?? '';
    _sweetnessController.text = _sake.hiddenSpecs.sweetnessScore?.toStringAsFixed(1) ?? '';
    // ... (reset all controllers)
  });
}

Future<void> _saveAiInfo() async {
  // Validate inputs
  final sweetness = double.tryParse(_sweetnessController.text);
  final body = double.tryParse(_bodyController.text);
  final polishing = double.tryParse(_polishingController.text);
  final alcohol = double.tryParse(_alcoholController.text);
  final sakeMeter = double.tryParse(_sakeMeterController.text);

  // Create updated sake item
  final updatedSpecs = _sake.hiddenSpecs.copyWith(
    type: _typeController.text.isEmpty ? null : _typeController.text,
    sweetnessScore: sweetness,
    bodyScore: body,
    polishingRatio: polishing,
    alcoholContent: alcohol,
    sakeMeterValue: sakeMeter,
    riceVariety: _riceController.text.isEmpty ? null : _riceController.text,
    yeast: _yeastController.text.isEmpty ? null : _yeastController.text,
    manufacturingYearMonth: _manufacturingController.text.isEmpty ? null : _manufacturingController.text,
  );

  final updatedSake = _sake.copyWith(hiddenSpecs: updatedSpecs);

  // Save to Hive
  try {
    final box = Hive.box<SakeItem>('sake_items');
    await box.put(_sake.key, updatedSake);

    setState(() {
      _sake = updatedSake;
      _isEditingAiInfo = false;
    });

    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('✅ 保存しました')),
      );
    }
  } catch (e) {
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('⚠️ 保存に失敗しました: $e')),
      );
    }
  }
}
```

#### Step 2: Add Validation (2 hours)

```dart
String? _validateNumber(String value, {double? min, double? max}) {
  if (value.isEmpty) return null; // Allow empty
  
  final number = double.tryParse(value);
  if (number == null) return '数値を入力してください';
  
  if (min != null && number < min) return '$min以上で入力してください';
  if (max != null && number > max) return '$max以下で入力してください';
  
  return null;
}

// Apply validation to fields
TextField(
  controller: _sweetnessController,
  keyboardType: TextInputType.number,
  decoration: InputDecoration(
    isDense: true,
    border: const OutlineInputBorder(),
    errorText: _validateNumber(_sweetnessController.text, min: -10, max: 10),
    helperText: '-10 〜 +10',
  ),
)
```

#### Step 3: Testing (2 hours)

**Test Cases**:
- [ ] Edit button appears when not in edit mode
- [ ] Cancel button resets all fields
- [ ] Save button validates inputs
- [ ] Save button writes to Hive correctly
- [ ] UI updates after save
- [ ] Empty fields saved as null
- [ ] Number validation works (min/max)
- [ ] Type dropdown works (if implemented)

---

## 📅 Implementation Schedule

### Week 1 (Mon-Wed)
- **Day 1**: Feature 1 - Help Button Placement (6 hours)
- **Day 2**: Feature 2 - AI Recommendations (Part 1: Service layer, 6 hours)
- **Day 3**: Feature 2 - AI Recommendations (Part 2: UI layer, 6 hours)

### Week 2 (Thu-Fri)
- **Day 4**: Feature 3 - AI Info Editing (Part 1: Editable fields, 4 hours)
- **Day 5**: Feature 3 - AI Info Editing (Part 2: Validation & testing, 4 hours)

**Total**: 26 hours over 5 days

---

## ✅ Definition of Done

Each feature is considered complete when:

### Feature 1: Help Buttons
- [ ] Help icons visible in all 3 locations
- [ ] Bottom sheets open/close smoothly
- [ ] Content accurate and helpful
- [ ] Works in Light and Dark modes
- [ ] Tested on real device

### Feature 2: AI Recommendations
- [ ] Tier 1 (similar) works offline
- [ ] Tier 2 (AI) works online with caching
- [ ] Graceful degradation when offline/API fails
- [ ] "ネットで検索" button works
- [ ] Cache hit rate > 80%
- [ ] Tested with 1, 10, 50, 100 sake items

### Feature 3: AI Info Editing
- [ ] All fields editable
- [ ] Validation prevents invalid input
- [ ] Save writes to Hive correctly
- [ ] Cancel resets fields
- [ ] Works in both Light and Dark modes
- [ ] Tested with various input scenarios

---

## 🚨 Risks & Mitigation

### Risk 1: Gemini API Rate Limiting
**Mitigation**: 
- Aggressive caching (90%+ hit rate)
- Tier 1 fallback always works
- User sees Tier 1 immediately, Tier 2 loads async

### Risk 2: AI Recommendations Quality
**Mitigation**:
- Prompt engineering for accurate results
- JSON parsing with error handling
- Manual testing with real sake data
- User feedback mechanism (future)

### Risk 3: Editing Breaks Hive Data
**Mitigation**:
- Thorough input validation
- copyWith() to preserve other fields
- Automatic backup before edit (future)
- Extensive testing

---

## 📊 Success Metrics

### Feature 1: Help Buttons
- **Adoption Rate**: % of users who tap help icons
- **Target**: > 30% within first week

### Feature 2: AI Recommendations
- **API Call Rate**: Calls per day
- **Cache Hit Rate**: > 80%
- **User Engagement**: % who tap "ネットで検索"
- **Target**: > 50% engagement

### Feature 3: AI Info Editing
- **Edit Rate**: % of sake items edited by users
- **Target**: > 10% (users correct AI mistakes)

---

## 🔗 Related Documents

- [PROJECT_TODO.md](PROJECT_TODO.md) - Active tasks
- [DARK_MODE_COLOR_GUIDELINES.md](DARK_MODE_COLOR_GUIDELINES.md) - UI guidelines
- [gamification_specification.md](gamification_specification.md) - Badge system
- [RECOMMENDATION_EXPANSION_PLAN.md](RECOMMENDATION_EXPANSION_PLAN.md) - Phase 3 expansion

---

**Status**: Ready to implement  
**Next Action**: Choose Feature 1, 2, or 3 to start  
**Recommended Order**: Feature 1 → Feature 3 → Feature 2 (Quick wins first)
