import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/constants.dart'; import '../../core/time.dart'; import '../../data/ai/model_lifecycle.dart'; import '../../data/db/daos/habit_dao.dart'; import '../../domain/ai/frame_candidate.dart'; import '../../domain/models/habit.dart'; import '../../domain/rules/active_habit_quota.dart'; import '../../state/ai_providers.dart'; import '../../state/providers.dart'; import '../widgets/frame_suggestion_dialog.dart'; class HabitCreateScreen extends ConsumerStatefulWidget { const HabitCreateScreen({super.key}); @override ConsumerState createState() => _HabitCreateScreenState(); } class _HabitCreateScreenState extends ConsumerState { final _formKey = GlobalKey(); final _titleCtrl = TextEditingController(); final _framedCtrl = TextEditingController(); HabitType _type = HabitType.build; FrameLevel _level = FrameLevel.l2; bool _saving = false; @override void dispose() { _titleCtrl.dispose(); _framedCtrl.dispose(); super.dispose(); } Future _save() async { if (!_formKey.currentState!.validate()) return; setState(() => _saving = true); try { final dao = ref.read(habitDaoProvider); final count = await dao.countActive( userId: kLocalDefaultUserId, type: _type, ); final quota = judgeActiveHabitQuota(type: _type, currentActiveCount: count); if (!quota.allowed) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(quota.reason)), ); return; } await dao.insertWithVariants(HabitDraft( userId: kLocalDefaultUserId, type: _type, title: _titleCtrl.text.trim(), // Placeholder: vertical-slice uses the first seeded protocol. protocolId: _type == HabitType.build ? 'morning_sunlight' : null, breakProtocolId: _type == HabitType.breakHabit ? 'alcohol' : null, frameLevel: _level, frameFramedText: _framedCtrl.text.trim(), startedAt: _ymd(nowKst()), )); if (!mounted) return; Navigator.of(context).pop(); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('저장 실패: $e')), ); } finally { if (mounted) setState(() => _saving = false); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('새 습관')), body: Padding( padding: const EdgeInsets.all(16), child: Form( key: _formKey, child: ListView( children: [ TextFormField( controller: _titleCtrl, decoration: const InputDecoration(labelText: '제목'), validator: (v) => (v == null || v.trim().isEmpty) ? '제목을 입력하세요' : null, ), const SizedBox(height: 16), DropdownButtonFormField( initialValue: _type, items: const [ DropdownMenuItem(value: HabitType.build, child: Text('만들기')), DropdownMenuItem(value: HabitType.breakHabit, child: Text('없애기')), ], onChanged: (v) => setState(() => _type = v ?? HabitType.build), decoration: const InputDecoration(labelText: '타입'), ), const SizedBox(height: 16), DropdownButtonFormField( initialValue: _level, items: const [ DropdownMenuItem(value: FrameLevel.l2, child: Text('L2 · 조건부 긍정')), DropdownMenuItem(value: FrameLevel.l3, child: Text('L3 · 정체성')), ], onChanged: (v) => setState(() => _level = v ?? FrameLevel.l2), decoration: const InputDecoration(labelText: '프레임 레벨'), ), const SizedBox(height: 16), TextFormField( controller: _framedCtrl, decoration: const InputDecoration( labelText: '프레임 문구', hintText: '예: 아침 햇빛을 10분 받는다', ), maxLines: 2, validator: (v) => (v == null || v.trim().isEmpty) ? '프레임 문구를 입력하세요' : null, ), const SizedBox(height: 8), _AiSuggestButton( titleCtrl: _titleCtrl, framedCtrl: _framedCtrl, habitType: _type, onSelectLevel: (level) => setState(() => _level = level), ), const SizedBox(height: 16), FilledButton( onPressed: _saving ? null : _save, child: Text(_saving ? '저장 중...' : '저장'), ), ], ), ), ), ); } } String _ymd(DateTime d) => '${d.year.toString().padLeft(4, '0')}-' '${d.month.toString().padLeft(2, '0')}-' '${d.day.toString().padLeft(2, '0')}'; /// "AI 제안" button — three states (AC3, AC6, AC7): /// - opt-in OFF → hidden (discoverability gated by Settings) /// - opt-in ON && model not ready → visible but DISABLED + tooltip /// - opt-in ON && model ready → enabled class _AiSuggestButton extends ConsumerWidget { final TextEditingController titleCtrl; final TextEditingController framedCtrl; final HabitType habitType; final ValueChanged onSelectLevel; const _AiSuggestButton({ required this.titleCtrl, required this.framedCtrl, required this.habitType, required this.onSelectLevel, }); @override Widget build(BuildContext context, WidgetRef ref) { final optIn = ref.watch(aiSettingsProvider).maybeWhen( data: (v) => v, orElse: () => false, ); if (!optIn) return const SizedBox.shrink(); final ready = ref.watch(modelAvailabilityProvider).maybeWhen( data: (a) => a == ModelAvailability.ready, orElse: () => false, ); final button = TextButton.icon( icon: const Icon(Icons.auto_awesome), label: const Text('AI 제안'), onPressed: ready ? () async { final raw = titleCtrl.text.trim(); if (raw.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('먼저 제목을 입력해주세요')), ); return; } final picked = await FrameSuggestionDialog.show( context, input: SuggestFrameInput( rawText: raw, habitType: habitType, ), ); if (picked != null) { framedCtrl.text = picked.framedText; // AC5: use the candidate's explicit level, not a heuristic. onSelectLevel(picked.level); } } : null, ); return Align( alignment: Alignment.centerRight, child: ready ? button : Tooltip( message: 'AI 도움을 먼저 켜주세요', child: button, ), ); } }