Skip to content

Commit 24b671c

Browse files
author
Gustaf Eden
committed
feat: Add Haiku-powered dynamic placeholder text
Generates witty placeholder text using Claude Haiku at startup. Includes typing animation effect for visual polish.
1 parent abbdb95 commit 24b671c

2 files changed

Lines changed: 141 additions & 2 deletions

File tree

lib/modules/agent_network/pages/networks_overview_page.dart

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'dart:async';
12
import 'dart:io';
23
import 'package:nocterm/nocterm.dart';
34
import 'package:nocterm_riverpod/nocterm_riverpod.dart';
@@ -7,6 +8,10 @@ import 'package:vide_cli/modules/agent_network/pages/networks_list_page.dart';
78
import 'package:vide_cli/modules/agent_network/service/agent_network_manager.dart';
89
import 'package:vide_cli/modules/agent_network/state/agent_networks_state_notifier.dart';
910
import 'package:vide_cli/components/attachment_text_field.dart';
11+
import 'package:vide_cli/modules/haiku/haiku_service.dart';
12+
import 'package:vide_cli/modules/haiku/haiku_providers.dart';
13+
import 'package:vide_cli/modules/haiku/prompts/loading_words_prompt.dart';
14+
import 'package:vide_cli/modules/haiku/prompts/placeholder_prompt.dart';
1015
import 'package:vide_cli/utils/project_detector.dart';
1116
import 'package:path/path.dart' as path;
1217

@@ -20,10 +25,48 @@ class NetworksOverviewPage extends StatefulComponent {
2025
class _NetworksOverviewPageState extends State<NetworksOverviewPage> {
2126
ProjectType? projectType;
2227

28+
// Placeholder animation state
29+
Timer? _placeholderTimer;
30+
bool _isLoadingPlaceholder = true;
31+
bool _isTypingPlaceholder = false;
32+
String _fullPlaceholder = '';
33+
String _displayedPlaceholder = '';
34+
int _typingIndex = 0;
35+
2336
@override
2437
void initState() {
2538
super.initState();
2639
_loadProjectInfo();
40+
_generateStartupContent();
41+
}
42+
43+
void _startTypingAnimation(String text) {
44+
_placeholderTimer?.cancel();
45+
_fullPlaceholder = text;
46+
_typingIndex = 0;
47+
_displayedPlaceholder = '';
48+
_isTypingPlaceholder = true;
49+
50+
_placeholderTimer = Timer.periodic(const Duration(milliseconds: 30), (_) {
51+
if (mounted && _typingIndex < _fullPlaceholder.length) {
52+
setState(() {
53+
_typingIndex++;
54+
_displayedPlaceholder = _fullPlaceholder.substring(0, _typingIndex);
55+
});
56+
} else {
57+
_placeholderTimer?.cancel();
58+
setState(() {
59+
_isTypingPlaceholder = false;
60+
_displayedPlaceholder = _fullPlaceholder;
61+
});
62+
}
63+
});
64+
}
65+
66+
@override
67+
void dispose() {
68+
_placeholderTimer?.cancel();
69+
super.dispose();
2770
}
2871

2972
Future<void> _loadProjectInfo() async {
@@ -37,6 +80,68 @@ class _NetworksOverviewPageState extends State<NetworksOverviewPage> {
3780
}
3881
}
3982

83+
/// Generate startup content using HaikuService
84+
void _generateStartupContent() {
85+
final now = DateTime.now();
86+
87+
// Pre-generate loading words for first message
88+
HaikuService.invokeForList(
89+
systemPrompt: LoadingWordsPrompt.build(now),
90+
userMessage: 'Generate loading words for: "Starting a new coding session"',
91+
delay: Duration.zero, // No delay on startup
92+
).then((words) {
93+
if (mounted && words != null) {
94+
context.read(loadingWordsProvider.notifier).state = words;
95+
}
96+
});
97+
98+
// Generate dynamic placeholder text
99+
HaikuService.invoke(
100+
systemPrompt: PlaceholderPrompt.build(now),
101+
userMessage: 'Generate placeholder text',
102+
delay: Duration.zero,
103+
).then((placeholder) {
104+
if (mounted) {
105+
setState(() {
106+
_isLoadingPlaceholder = false;
107+
});
108+
String text = placeholder?.trim() ?? 'Describe your goal (you can attach images)';
109+
110+
// Validate: handle verbose multi-line responses
111+
if (text.contains('\n') || text.length > 50) {
112+
// Multi-line or too long - try to extract just the placeholder
113+
final lines = text.split('\n').map((l) => l.trim()).where((l) => l.isNotEmpty).toList();
114+
// Look for a short line that looks like a placeholder (not explanation text)
115+
String? shortLine;
116+
for (final line in lines) {
117+
// Skip lines that look like explanations
118+
if (line.startsWith('Here') ||
119+
line.startsWith('Alright') ||
120+
line.contains(':') ||
121+
line.startsWith('Pick') ||
122+
line.startsWith('I')) continue;
123+
// Clean up markdown and list markers
124+
final cleaned =
125+
line.replaceAll(RegExp(r'^[\*\-\d\.\)]+\s*'), '').replaceAll('**', '').trim();
126+
if (cleaned.length >= 3 && cleaned.length <= 45) {
127+
shortLine = cleaned;
128+
break;
129+
}
130+
}
131+
text = shortLine ?? 'Describe your goal (you can attach images)';
132+
}
133+
134+
// Final safety: if still too long, use fallback
135+
if (text.length > 50) {
136+
text = 'Describe your goal (you can attach images)';
137+
}
138+
139+
context.read(placeholderTextProvider.notifier).state = text;
140+
_startTypingAnimation(text);
141+
}
142+
});
143+
}
144+
40145
void _handleSubmit(Message message) async {
41146
// Start a new agent network with the full message (preserves attachments)
42147
final network = await context.read(agentNetworkManagerProvider.notifier).startNew(message);
@@ -54,6 +159,18 @@ class _NetworksOverviewPageState extends State<NetworksOverviewPage> {
54159
final currentDir = Directory.current.path;
55160
final dirName = path.basename(currentDir);
56161

162+
// Get placeholder - empty while loading, then type in the text when ready
163+
final String placeholder;
164+
if (_isLoadingPlaceholder) {
165+
placeholder = '';
166+
} else if (_isTypingPlaceholder) {
167+
placeholder = _displayedPlaceholder;
168+
} else {
169+
placeholder = _displayedPlaceholder.isNotEmpty
170+
? _displayedPlaceholder
171+
: (context.watch(placeholderTextProvider) ?? 'Describe your goal (you can attach images)');
172+
}
173+
57174
return Focusable(
58175
focused: true,
59176
onKeyEvent: (event) {
@@ -84,7 +201,7 @@ class _NetworksOverviewPageState extends State<NetworksOverviewPage> {
84201
Container(
85202
child: AttachmentTextField(
86203
focused: true,
87-
placeholder: 'Describe your goal (you can attach images)',
204+
placeholder: placeholder,
88205
onSubmit: _handleSubmit,
89206
),
90207
padding: EdgeInsets.all(1),
@@ -139,4 +256,3 @@ class _ProjectTypeBadge extends StatelessComponent {
139256
);
140257
}
141258
}
142-
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/// Prompt builder for dynamic input field placeholder text
2+
class PlaceholderPrompt {
3+
static String build(DateTime now) {
4+
final randomSeed = now.millisecondsSinceEpoch % 100;
5+
6+
return '''
7+
Generate witty placeholder text for an AI coding assistant input field.
8+
9+
Seed: $randomSeed
10+
11+
RULES:
12+
- 3-5 words only
13+
- Playful, slightly cheeky tone
14+
- Like a friend asking what you want to work on
15+
- BANNED: "What's on your mind", "Describe your", "Enter your", "Type here"
16+
- Be creative! Think: "What are we breaking today?", "Got bugs?", "Your wish, my code..."
17+
18+
CRITICAL: Output ONLY the placeholder text itself.
19+
NO explanations, NO options, NO numbering, NO markdown.
20+
Just the 3-5 word phrase, nothing else.
21+
''';
22+
}
23+
}

0 commit comments

Comments
 (0)