@@ -32,7 +32,6 @@ pub enum AppMode {
3232 SelectingLocalModel ,
3333 SelectingCloudModel ,
3434 Orchestrating ,
35- Revising ,
3635 Complete ,
3736 CoachingTip ,
3837 // TODO: Add About mode
@@ -65,7 +64,6 @@ pub enum ValidationMessage {
6564#[ derive( Debug ) ]
6665pub enum AgentMessage {
6766 ProposalsGenerated ( Result < Vec < String > , anyhow:: Error > ) ,
68- RevisedProposalGenerated ( Result < String , anyhow:: Error > ) ,
6967 CloudSynthesisComplete ( Result < AtomicNote , CloudError > ) ,
7068}
7169
@@ -122,6 +120,7 @@ pub struct App {
122120 models_per_page : usize ,
123121 proposals : Vec < String > ,
124122 current_proposal_index : usize ,
123+ original_user_query : String , // Store the user's original query for metadata
125124 final_prompt : String ,
126125 cloud_response : Option < AtomicNote > ,
127126 synthesis_scroll : u16 ,
@@ -150,6 +149,7 @@ impl App {
150149 models_per_page : 10 , // Show 10 models per page
151150 proposals : Vec :: new ( ) ,
152151 current_proposal_index : 0 ,
152+ original_user_query : String :: new ( ) ,
153153 final_prompt : String :: new ( ) ,
154154 cloud_response : None ,
155155 synthesis_scroll : 0 ,
@@ -238,8 +238,11 @@ impl App {
238238
239239 frame. render_widget ( proposals_paragraph, chunks[ 1 ] ) ;
240240
241- // Footer with controls
242- let footer_text = "[Enter] Synthesize | [E]dit Selected | [ESC] Cancel" ;
241+ // Footer with controls - dynamic based on synthesis status
242+ let footer_text = match self . agent_status {
243+ AgentStatus :: Searching => "⏳ Synthesizing... | [ESC] Cancel" ,
244+ _ => "[Enter] Synthesize | [ESC] Cancel" ,
245+ } ;
243246 let footer = Paragraph :: new ( footer_text)
244247 . alignment ( Alignment :: Center )
245248 . style ( self . theme . ratatui_style ( Element :: Inactive ) ) ;
@@ -457,16 +460,31 @@ impl App {
457460 frame. render_widget ( Clear , modal_area) ;
458461 self . render_coaching_tip_modal ( frame, modal_area) ;
459462 } else if self . mode == AppMode :: Complete {
463+ // Center the synthesis content for better visual balance
460464 let content = if let Some ( note) = & self . cloud_response {
461465 // Clean display - only show the synthesis content, hide system metadata
462466 Paragraph :: new ( note. body_text . as_str ( ) )
463467 . style ( self . theme . ratatui_style ( Element :: Text ) )
468+ . alignment ( ratatui:: prelude:: Alignment :: Center )
464469 } else {
465470 // This case should ideally not be reached if mode is Complete
466471 Paragraph :: new ( "Waiting for synthesis..." )
467472 . style ( self . theme . ratatui_style ( Element :: Text ) )
473+ . alignment ( ratatui:: prelude:: Alignment :: Center )
468474 } ;
469475
476+ // Create a centered area for the synthesis (70% width, centered vertically)
477+ let main_area = app_chunks[ 1 ] ;
478+ let synthesis_width = ( main_area. width * 70 / 100 ) . max ( 40 ) . min ( main_area. width ) ;
479+ let synthesis_height = 10 ; // Fixed height for the synthesis box
480+
481+ let synthesis_area = Rect :: new (
482+ main_area. x + ( main_area. width . saturating_sub ( synthesis_width) ) / 2 ,
483+ main_area. y + ( main_area. height . saturating_sub ( synthesis_height) ) / 2 ,
484+ synthesis_width,
485+ synthesis_height,
486+ ) ;
487+
470488 let block = Block :: default ( )
471489 . title ( " Synthesis Complete " )
472490 . borders ( Borders :: ALL )
@@ -477,7 +495,7 @@ impl App {
477495 . wrap ( Wrap { trim : true } )
478496 . scroll ( ( self . synthesis_scroll , 0 ) ) ;
479497
480- frame. render_widget ( paragraph, app_chunks [ 1 ] ) ;
498+ frame. render_widget ( paragraph, synthesis_area ) ;
481499 } else {
482500 render_chat (
483501 frame,
@@ -604,19 +622,6 @@ impl App {
604622 self . mode = AppMode :: CoachingTip ;
605623 self . agent_status = AgentStatus :: Ready ;
606624 }
607- AgentMessage :: RevisedProposalGenerated ( Ok ( proposal) ) => {
608- self . proposals [ self . current_proposal_index ] = proposal;
609- self . mode = AppMode :: Orchestrating ;
610- self . agent_status = AgentStatus :: Ready ;
611- }
612- AgentMessage :: RevisedProposalGenerated ( Err ( _e) ) => {
613- self . coaching_tip = (
614- "Local Model Error" . to_string ( ) ,
615- "The local model failed to revise the proposal. Check if it is running and configured correctly." . to_string ( ) ,
616- ) ;
617- self . mode = AppMode :: CoachingTip ;
618- self . agent_status = AgentStatus :: Ready ;
619- }
620625 AgentMessage :: CloudSynthesisComplete ( Ok ( response) ) => {
621626 self . cloud_response = Some ( response) ;
622627 self . mode = AppMode :: Complete ;
@@ -883,39 +888,22 @@ impl App {
883888 }
884889 KeyCode :: Enter => {
885890 // Synthesize - send proposal to cloud for synthesis
886- if let Some ( proposal) =
887- self . proposals . get ( self . current_proposal_index )
888- {
889- self . final_prompt = proposal. clone ( ) ;
890- self . handle_cloud_synthesis ( ) ;
891+ // Rate limiting: only allow if not already processing
892+ if self . agent_status != AgentStatus :: Searching {
893+ if let Some ( proposal) =
894+ self . proposals . get ( self . current_proposal_index )
895+ {
896+ self . final_prompt = proposal. clone ( ) ;
897+ self . handle_cloud_synthesis ( ) ;
898+ }
891899 }
892900 }
893- KeyCode :: Char ( 'e' ) => {
894- // Edit selected proposal
895- self . mode = AppMode :: Revising ;
896- self . edit_buffer . clear ( ) ;
897- }
898901 KeyCode :: Esc => {
899902 // Cancel and return to normal mode
900903 self . mode = AppMode :: Normal ;
901904 self . proposals . clear ( ) ;
902905 self . current_proposal_index = 0 ;
903- }
904- _ => { }
905- } ,
906- AppMode :: Revising => match key. code {
907- KeyCode :: Enter => {
908- self . handle_revision ( ) ;
909- }
910- KeyCode :: Esc => {
911- self . mode = AppMode :: Orchestrating ;
912- self . edit_buffer . clear ( ) ;
913- }
914- KeyCode :: Backspace => {
915- self . edit_buffer . pop ( ) ;
916- }
917- KeyCode :: Char ( c) => {
918- self . edit_buffer . push ( c) ;
906+ self . original_user_query . clear ( ) ;
919907 }
920908 _ => { }
921909 } ,
@@ -927,6 +915,7 @@ impl App {
927915 self . final_prompt . clear ( ) ;
928916 self . proposals . clear ( ) ;
929917 self . current_proposal_index = 0 ;
918+ self . original_user_query . clear ( ) ;
930919 self . cloud_response = None ;
931920 self . synthesis_scroll = 0 ;
932921 self . agent_status = AgentStatus :: Ready ;
@@ -937,6 +926,7 @@ impl App {
937926 self . final_prompt . clear ( ) ;
938927 self . proposals . clear ( ) ;
939928 self . current_proposal_index = 0 ;
929+ self . original_user_query . clear ( ) ;
940930 self . cloud_response = None ;
941931 self . synthesis_scroll = 0 ;
942932 self . agent_status = AgentStatus :: Ready ;
@@ -980,6 +970,9 @@ impl App {
980970 self . handle_slash_command ( & message) ;
981971 } else {
982972 // Handle regular chat message
973+ // Store the original user query for metadata
974+ self . original_user_query = message. clone ( ) ;
975+
983976 self . agent_status = AgentStatus :: Orchestrating ;
984977 let settings = self . settings . clone ( ) ;
985978 let tx = self . agent_tx . clone ( ) ;
@@ -998,31 +991,18 @@ impl App {
998991 self . edit_buffer . clear ( ) ;
999992 }
1000993
1001- fn handle_revision ( & mut self ) {
1002- let revision = self . edit_buffer . trim ( ) . to_string ( ) ;
1003- if let Some ( current_proposal) = self . proposals . get ( self . current_proposal_index ) . cloned ( ) {
1004- self . agent_status = AgentStatus :: Orchestrating ;
1005- let settings = self . settings . clone ( ) ;
1006- let tx = self . agent_tx . clone ( ) ;
1007- tokio:: spawn ( async move {
1008- let result = orchestrator:: revise_proposal (
1009- & current_proposal,
1010- & revision,
1011- & settings. endpoint ,
1012- & settings. local_model ,
1013- )
1014- . await ;
1015- let _ = tx. send ( AgentMessage :: RevisedProposalGenerated ( result) ) ;
1016- } ) ;
1017- }
1018- self . edit_buffer . clear ( ) ;
1019- }
1020994
1021995 fn save_synthesis ( & self ) {
1022996 if let Some ( note) = & self . cloud_response {
1023- // Generate markdown content with v0.1.0 metadata structure
1024- let timestamp = chrono:: Utc :: now ( ) . format ( "%Y-%m-%d_%H-%M-%S" ) . to_string ( ) ;
1025- let filename = format ! ( "synthesis_{}.md" , timestamp) ;
997+ // Generate meaningful filename from query and metadata
998+ let timestamp = chrono:: Utc :: now ( ) ;
999+ let date_part = timestamp. format ( "%Y-%m-%d" ) . to_string ( ) ;
1000+
1001+ // Extract keywords from original user query for filename
1002+ let keywords = self . extract_filename_keywords ( & self . original_user_query , & note. header_tags ) ;
1003+ let time_suffix = timestamp. format ( "-%H%M" ) . to_string ( ) ; // Add time for uniqueness
1004+
1005+ let filename = format ! ( "{}-{}{}.md" , date_part, keywords, time_suffix) ;
10261006
10271007 // Get the selected proposal text
10281008 let proposal_text = if !self . proposals . is_empty ( )
@@ -1044,7 +1024,7 @@ impl App {
10441024 } else {
10451025 & self . settings. cloud_model
10461026 } ,
1047- self . final_prompt . replace( "\" " , "\\ \" " ) ,
1027+ self . original_user_query . replace( "\" " , "\\ \" " ) ,
10481028 clean_proposal. replace( "\" " , "\\ \" " ) ,
10491029 note. header_tags. iter( ) . map( |tag| format!( "\" {}\" " , tag) ) . collect:: <Vec <_>>( ) . join( ", " ) ,
10501030 note. header_tags. join( " • " ) ,
@@ -1229,4 +1209,55 @@ impl App {
12291209 let target_page = self . selected_model_index / self . models_per_page ;
12301210 self . current_page = target_page;
12311211 }
1212+
1213+ fn extract_filename_keywords ( & self , query : & str , meta_tags : & [ String ] ) -> String {
1214+ // Common words to filter out
1215+ let stop_words = [
1216+ "the" , "a" , "an" , "and" , "or" , "but" , "in" , "on" , "at" , "to" , "for" , "of" , "with" ,
1217+ "by" , "is" , "are" , "was" , "were" , "be" , "been" , "have" , "has" , "had" , "do" , "does" ,
1218+ "did" , "will" , "would" , "could" , "should" , "can" , "what" , "where" , "when" , "why" ,
1219+ "how" , "who" , "which" , "that" , "this" , "these" , "those" , "i" , "you" , "he" , "she" ,
1220+ "it" , "we" , "they" , "me" , "him" , "her" , "us" , "them" , "my" , "your" , "his" , "her" ,
1221+ "its" , "our" , "their"
1222+ ] ;
1223+
1224+ // Extract meaningful words from query
1225+ let query_words: Vec < String > = query
1226+ . to_lowercase ( )
1227+ . split_whitespace ( )
1228+ . filter_map ( |word| {
1229+ // Clean up punctuation
1230+ let clean_word = word. trim_matches ( |c : char | !c. is_alphanumeric ( ) ) ;
1231+
1232+ // Filter out stop words and short words
1233+ if clean_word. len ( ) >= 3 && !stop_words. contains ( & clean_word) {
1234+ Some ( clean_word. to_string ( ) )
1235+ } else {
1236+ None
1237+ }
1238+ } )
1239+ . take ( 3 ) // Limit to 3 keywords from query
1240+ . collect ( ) ;
1241+
1242+ // If we got good keywords from query, use them
1243+ if query_words. len ( ) >= 2 {
1244+ query_words. join ( "-" )
1245+ } else {
1246+ // Fallback to first meta tag if query didn't provide enough keywords
1247+ if let Some ( first_tag) = meta_tags. first ( ) {
1248+ first_tag
1249+ . to_lowercase ( )
1250+ . replace ( ' ' , "-" )
1251+ . chars ( )
1252+ . filter ( |c| c. is_alphanumeric ( ) || * c == '-' )
1253+ . collect :: < String > ( )
1254+ . trim_matches ( '-' )
1255+ . to_string ( )
1256+ } else {
1257+ // Final fallback
1258+ "synthesis" . to_string ( )
1259+ }
1260+ }
1261+ }
1262+
12321263}
0 commit comments