1+ import 'dart:async' ;
12import 'dart:io' ;
23import 'package:nocterm/nocterm.dart' ;
34import 'package:nocterm_riverpod/nocterm_riverpod.dart' ;
@@ -7,6 +8,10 @@ import 'package:vide_cli/modules/agent_network/pages/networks_list_page.dart';
78import 'package:vide_cli/modules/agent_network/service/agent_network_manager.dart' ;
89import 'package:vide_cli/modules/agent_network/state/agent_networks_state_notifier.dart' ;
910import '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' ;
1015import 'package:vide_cli/utils/project_detector.dart' ;
1116import 'package:path/path.dart' as path;
1217
@@ -20,10 +25,48 @@ class NetworksOverviewPage extends StatefulComponent {
2025class _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-
0 commit comments