[Designer] #342 UX round 2 — chat 빈 상태 + 한국식 날짜 + 표현 방식

남은 P1/P2 3건.

- ChatScreen 빈 상태: 아이콘 + 한 줄 설명 + 예시 prompt 4개
  (tap → _textCtrl 자동 채움, 자동 send X).
- CheckIn 날짜: '2026-06-15' raw → '6월 15일 (월)' 한국식.
  DB 저장은 _ymd 유지.
- HabitCreate '프레임 레벨' → '표현 방식' + helperText.
  아이템: '조건부 행동 (예: 아침에 햇빛 받기)' / '정체성 (예: 나는 일찍 자는 사람)'.
- 설계서 #342 README — D 섹션 + AC-D1/D2/D3 추가.
- CHANGELOG v0.4.2 UX round 2 블록.

167 tests passed, analyze clean.

Refs #342
This commit is contained in:
2026-06-15 15:28:03 +09:00
parent e81f3e44a4
commit c18dca1def
5 changed files with 116 additions and 24 deletions

View File

@@ -132,24 +132,31 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
),
),
Expanded(
child: ListView.builder(
controller: _scrollCtrl,
padding: const EdgeInsets.all(12),
itemCount: state.messages.length +
(state.streamingText != null &&
state.streamingText!.isNotEmpty
? 1
: 0),
itemBuilder: (context, i) {
if (i < state.messages.length) {
return _MessageBubble(message: state.messages[i]);
}
return _MessageBubble(
message: ModelChatMessage(state.streamingText ?? ''),
streaming: true,
);
},
),
child: state.messages.isEmpty && state.streamingText == null
? _EmptyChatHint(onPickPrompt: (p) {
_textCtrl.text = p;
_textCtrl.selection = TextSelection.fromPosition(
TextPosition(offset: p.length),
);
})
: ListView.builder(
controller: _scrollCtrl,
padding: const EdgeInsets.all(12),
itemCount: state.messages.length +
(state.streamingText != null &&
state.streamingText!.isNotEmpty
? 1
: 0),
itemBuilder: (context, i) {
if (i < state.messages.length) {
return _MessageBubble(message: state.messages[i]);
}
return _MessageBubble(
message: ModelChatMessage(state.streamingText ?? ''),
streaming: true,
);
},
),
),
const Divider(height: 1),
Padding(
@@ -248,6 +255,73 @@ class _WarmupErrorBanner extends ConsumerWidget {
}
}
/// 첫 진입 시 빈 대화창 안내. 사용자가 무엇을 물어볼 수 있는지
/// 예시 prompt 4개 + tool 카테고리 짧은 안내. tap 하면 입력창에 채워지고
/// 사용자가 보내기만 누르면 됨 (자동 send X — 사용자가 문구 수정 여지).
class _EmptyChatHint extends StatelessWidget {
final ValueChanged<String> onPickPrompt;
const _EmptyChatHint({required this.onPickPrompt});
static const _examples = [
'아침 햇빛 받기 습관 추가해줘',
'오늘 운동 했어',
'내 스트릭 보여줘',
'수면 프로토콜 알려줘',
];
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Icon(
Icons.smart_toy_outlined,
size: 48,
color: theme.colorScheme.primary,
),
const SizedBox(height: 16),
Text(
'AI 코치',
style: theme.textTheme.titleLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'습관 추가·기록 · 카탈로그 검색 · 스트릭 조회를 도와드려요.',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
Text(
'예시',
style: theme.textTheme.labelLarge?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
..._examples.map((p) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: OutlinedButton(
onPressed: () => onPickPrompt(p),
style: OutlinedButton.styleFrom(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 12),
),
child: Text(p),
),
)),
],
),
);
}
}
/// Human-friendly Korean labels for the 6 tools registered in
/// `ToolRegistry.defaults()`. Falls back to the raw tool name for any
/// future tool that hasn't been mapped yet — better to show the raw id

View File

@@ -50,7 +50,7 @@ class _CheckInScreenState extends ConsumerState<CheckInScreen> {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('오늘 (${_ymd(nowKst())})',
Text('오늘 · ${_koreanDate(nowKst())}',
style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center),
const SizedBox(height: 32),
@@ -80,3 +80,9 @@ String _ymd(DateTime d) =>
'${d.year.toString().padLeft(4, '0')}-'
'${d.month.toString().padLeft(2, '0')}-'
'${d.day.toString().padLeft(2, '0')}';
/// 사용자 노출용 한국식 날짜 — '6월 15일 (월)'. DB 저장은 _ymd 가 담당.
String _koreanDate(DateTime d) {
const weekdays = ['', '', '', '', '', '', ''];
return '${d.month}${d.day}일 (${weekdays[d.weekday - 1]})';
}

View File

@@ -104,11 +104,16 @@ class _HabitCreateScreenState extends ConsumerState<HabitCreateScreen> {
DropdownButtonFormField<FrameLevel>(
initialValue: _level,
items: const [
DropdownMenuItem(value: FrameLevel.l2, child: Text('L2 · 조건부 긍정')),
DropdownMenuItem(value: FrameLevel.l3, child: Text('L3 · 정체성')),
DropdownMenuItem(
value: FrameLevel.l2, child: Text('조건부 행동 (예: 아침에 햇빛 받기)')),
DropdownMenuItem(
value: FrameLevel.l3, child: Text('정체성 (예: 나는 일찍 자는 사람)')),
],
onChanged: (v) => setState(() => _level = v ?? FrameLevel.l2),
decoration: const InputDecoration(labelText: '프레임 레벨'),
decoration: const InputDecoration(
labelText: '표현 방식',
helperText: '문구를 어떻게 적을지 — 행동 위주 vs 정체성 위주',
),
),
const SizedBox(height: 16),
TextFormField(