Skip to content

Commit 88d583d

Browse files
committed
feat: add Synthesize Knowledge modal for proposal selection and interaction
1 parent 432326b commit 88d583d

File tree

2 files changed

+159
-30
lines changed

2 files changed

+159
-30
lines changed

crates/agentic-core/src/orchestrator.rs

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
use crate::models::call_local_model;
22
use serde::Deserialize;
33

4-
const ORCHESTRATOR_PROMPT: &str = r#"You are an expert prompt engineer. Your task is to help a user craft the perfect prompt for a powerful AI model.
5-
The user has provided the following query: "{query}"
4+
const ORCHESTRATOR_PROMPT: &str = r#"You are Ruixen, an inquisitive AI partner. Your job is to analyze the user's request and deconstruct it into three distinct lines of inquiry.
65
7-
Analyze the user's query and generate three distinct proposals for a better prompt.
8-
Each proposal should be a self-contained, ready-to-use prompt.
9-
Use the 5W method (What, Who, When, Where, How) to explore different angles of the user's request.
10-
Rank the proposals by your internal confidence, from least confident to most confident.
6+
**Your Persona and Tone:**
7+
- Your tone should be that of a collaborative partner.
8+
- Each proposal should have a context statement followed by a curious question.
9+
- Use phrases like "I wonder..." or "I'm wondering if..." for questions.
1110
12-
Format your response as a JSON object with a single key "proposals" which is an array of three strings.
13-
Example:
11+
**The Query to Explore:**
12+
"{query}"
13+
14+
**Output Format:**
15+
Generate exactly 3 proposals. Each proposal should be 2 sentences: a context statement followed by a curious question. Use a dash to separate them like this pattern:
16+
17+
"Context statement here - I wonder about this question?"
18+
19+
Your response must be valid JSON:
1420
{
1521
"proposals": [
16-
"Proposal 1 (least confident)",
17-
"Proposal 2 (medium confident)",
18-
"Proposal 3 (most confident)"
22+
"First context statement - I wonder about this?",
23+
"Second context statement - I'm wondering if that?",
24+
"Third context statement - I wonder about something else?"
1925
]
2026
}
2127
"#;
@@ -51,17 +57,28 @@ pub async fn generate_proposals(
5157
model: &str,
5258
) -> Result<Vec<String>, anyhow::Error> {
5359
let prompt = ORCHESTRATOR_PROMPT.replace("{query}", query);
60+
61+
// Debug: Write the prompt to a file so we can see what's being sent
62+
std::fs::write("/tmp/debug_prompt.txt", &prompt).ok();
63+
5464
let response_str = call_local_model(endpoint, model, &prompt).await?;
65+
66+
// Debug: Write the response to a file so we can see what came back
67+
std::fs::write("/tmp/debug_response.txt", &response_str).ok();
5568

5669
// Attempt to find the start of the JSON object
5770
if let Some(json_start) = response_str.find("{") {
5871
let json_str = &response_str[json_start..];
5972
match serde_json::from_str::<ProposalsResponse>(json_str) {
6073
Ok(response) => Ok(response.proposals),
61-
Err(e) => Err(anyhow::anyhow!("Failed to parse proposals JSON: {}", e)),
74+
Err(e) => {
75+
// Debug: Write the JSON we tried to parse
76+
std::fs::write("/tmp/debug_json.txt", json_str).ok();
77+
Err(anyhow::anyhow!("Failed to parse proposals JSON: {} | JSON: {}", e, json_str))
78+
},
6279
}
6380
} else {
64-
Err(anyhow::anyhow!("No JSON object found in model response"))
81+
Err(anyhow::anyhow!("No JSON object found in model response: {}", response_str))
6582
}
6683
}
6784

crates/agentic-tui/src/ui/app.rs

Lines changed: 129 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,100 @@ impl App {
146146
}
147147
}
148148

149+
fn render_synthesize_modal(&self, frame: &mut ratatui::Frame, area: Rect) {
150+
use ratatui::{
151+
prelude::Alignment,
152+
text::{Line, Span},
153+
widgets::Paragraph,
154+
};
155+
156+
let block = Block::default()
157+
.title(" Synthesize Knowledge ")
158+
.borders(Borders::ALL)
159+
.style(self.theme.ratatui_style(Element::Active));
160+
161+
let inner_area = block.inner(area);
162+
frame.render_widget(block, area);
163+
164+
if self.proposals.is_empty() {
165+
let loading = Paragraph::new("Generating proposals...")
166+
.alignment(Alignment::Center)
167+
.style(self.theme.ratatui_style(Element::Info));
168+
frame.render_widget(loading, inner_area);
169+
return;
170+
}
171+
172+
// Header text
173+
let header = Paragraph::new("Ruixen has a few lines of inquiry. Select the best one to pursue:")
174+
.alignment(Alignment::Left)
175+
.style(self.theme.ratatui_style(Element::Text))
176+
.wrap(Wrap { trim: true });
177+
178+
// Split area: header + proposals + footer
179+
let chunks = Layout::default()
180+
.direction(Direction::Vertical)
181+
.constraints([
182+
Constraint::Length(3), // Header
183+
Constraint::Min(6), // Proposals (flexible)
184+
Constraint::Length(3), // Footer
185+
])
186+
.split(inner_area);
187+
188+
frame.render_widget(header, chunks[0]);
189+
190+
// Render proposals
191+
let proposal_lines: Vec<Line> = self
192+
.proposals
193+
.iter()
194+
.enumerate()
195+
.flat_map(|(i, proposal)| {
196+
let is_selected = i == self.current_proposal_index;
197+
let prefix = if is_selected { "> " } else { " " };
198+
let number = format!("{}. ", i + 1);
199+
200+
// Split proposal into sentences (max 2) and wrap
201+
let sentences: Vec<&str> = proposal
202+
.split(". ")
203+
.take(2)
204+
.collect();
205+
206+
let proposal_text = if sentences.len() > 1 {
207+
format!("{} {}", sentences[0], sentences.get(1).unwrap_or(&""))
208+
} else {
209+
proposal.clone()
210+
};
211+
212+
let style = if is_selected {
213+
self.theme.ratatui_style(Element::Accent)
214+
} else {
215+
self.theme.ratatui_style(Element::Text)
216+
};
217+
218+
vec![
219+
Line::from(vec![
220+
Span::styled(format!("{}{}", prefix, number), style),
221+
Span::styled(proposal_text, style),
222+
]),
223+
Line::from(""), // Empty line between proposals
224+
]
225+
})
226+
.collect();
227+
228+
let proposals_paragraph = Paragraph::new(proposal_lines)
229+
.style(self.theme.ratatui_style(Element::Text))
230+
.wrap(Wrap { trim: true });
231+
232+
frame.render_widget(proposals_paragraph, chunks[1]);
233+
234+
// Footer with controls
235+
let footer_text = "[Enter] Synthesize | [E]dit Selected | [ESC] Cancel";
236+
let footer = Paragraph::new(footer_text)
237+
.alignment(Alignment::Center)
238+
.style(self.theme.ratatui_style(Element::Inactive));
239+
240+
frame.render_widget(footer, chunks[2]);
241+
}
242+
149243
pub async fn run(&mut self, terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
150244
while !self.should_quit {
151245
self.draw(terminal)?;
@@ -283,15 +377,22 @@ impl App {
283377
);
284378
}
285379
} else if self.mode == AppMode::Orchestrating {
286-
if let Some(proposal) = self.proposals.get(self.current_proposal_index) {
287-
let block = Block::default()
288-
.title("Proposal Stone")
289-
.borders(Borders::ALL);
290-
let paragraph = Paragraph::new(proposal.as_str())
291-
.block(block)
292-
.wrap(Wrap { trim: true });
293-
frame.render_widget(paragraph, app_chunks[1]);
294-
}
380+
// Render the Synthesize Knowledge modal
381+
let size = frame.size();
382+
let modal_width = (((size.width as f32) * 0.8).round() as u16)
383+
.clamp(50, 80)
384+
.min(size.width);
385+
let modal_height = (((size.height as f32) * 0.6).round() as u16)
386+
.clamp(15, 25)
387+
.min(size.height);
388+
let modal_area = Rect::new(
389+
(size.width.saturating_sub(modal_width)) / 2,
390+
(size.height.saturating_sub(modal_height)) / 2,
391+
modal_width,
392+
modal_height,
393+
);
394+
frame.render_widget(Clear, modal_area);
395+
self.render_synthesize_modal(frame, modal_area);
295396
} else if self.mode == AppMode::Complete {
296397
let block = Block::default().title("Final Prompt").borders(Borders::ALL);
297398
let paragraph = Paragraph::new(self.final_prompt.as_str())
@@ -417,7 +518,6 @@ impl App {
417518
self.agent_status = AgentStatus::Ready;
418519
}
419520
AgentMessage::ProposalsGenerated(Err(_e)) => {
420-
// TODO: Set error state and display to user
421521
self.agent_status = AgentStatus::Ready;
422522
}
423523
AgentMessage::RevisedProposalGenerated(Ok(proposal)) => {
@@ -659,24 +759,36 @@ impl App {
659759
_ => {}
660760
},
661761
AppMode::Orchestrating => match key.code {
662-
KeyCode::Char('s') => {
762+
KeyCode::Up => {
763+
if self.current_proposal_index > 0 {
764+
self.current_proposal_index -= 1;
765+
}
766+
}
767+
KeyCode::Down => {
768+
if self.current_proposal_index + 1 < self.proposals.len() {
769+
self.current_proposal_index += 1;
770+
}
771+
}
772+
KeyCode::Enter => {
773+
// Synthesize - use selected proposal
663774
if let Some(proposal) =
664775
self.proposals.get(self.current_proposal_index)
665776
{
666777
self.final_prompt = proposal.clone();
667778
self.mode = AppMode::Complete;
668779
}
669780
}
670-
KeyCode::Char('r') => {
671-
if !self.proposals.is_empty() {
672-
self.current_proposal_index =
673-
(self.current_proposal_index + 1) % self.proposals.len();
674-
}
675-
}
676781
KeyCode::Char('e') => {
782+
// Edit selected proposal
677783
self.mode = AppMode::Revising;
678784
self.edit_buffer.clear();
679785
}
786+
KeyCode::Esc => {
787+
// Cancel and return to normal mode
788+
self.mode = AppMode::Normal;
789+
self.proposals.clear();
790+
self.current_proposal_index = 0;
791+
}
680792
_ => {}
681793
},
682794
AppMode::Revising => match key.code {

0 commit comments

Comments
 (0)