From 8b17a175c3e7e27b789812eba4e3cd760beadb10 Mon Sep 17 00:00:00 2001 From: soryu Date: Tue, 6 Jan 2026 04:08:11 +0000 Subject: Initial Control system --- makima/src/server/handlers/chat.rs | 115 +++++++++++++++++++++++++++++++++---- 1 file changed, 103 insertions(+), 12 deletions(-) (limited to 'makima/src/server/handlers/chat.rs') diff --git a/makima/src/server/handlers/chat.rs b/makima/src/server/handlers/chat.rs index 51f17c1..dfdb64e 100644 --- a/makima/src/server/handlers/chat.rs +++ b/makima/src/server/handlers/chat.rs @@ -53,6 +53,9 @@ pub struct ChatRequest { /// Optional conversation history for context continuity #[serde(default)] pub history: Option>, + /// Optional focused element index (for targeted editing) + #[serde(default)] + pub focused_element_index: Option, } #[derive(Debug, Serialize, ToSchema)] @@ -232,6 +235,9 @@ pub async fn chat_handler( // Build context about the file let file_context = build_file_context(&file); + // Build focused element context if specified + let focused_context = build_focused_element_context(&file.body, request.focused_element_index); + // Build agentic system prompt let system_prompt = format!( r#"You are an intelligent document editing agent. You help users view, analyze, and modify document files. @@ -274,13 +280,14 @@ You have access to tools for: ## Current Document Context {file_context} - +{focused_context} ## Important Notes - Body element indices are 0-based - When updating elements, provide ALL required fields for that element type - The transcript is read-only (you cannot modify it, only read it) - Changes are saved automatically after tool execution"#, - file_context = file_context + file_context = file_context, + focused_context = focused_context ); // Build initial messages (Groq/OpenAI format - will be converted for Claude) @@ -690,12 +697,25 @@ fn build_file_context(file: &crate::db::models::File) -> String { let desc = match element { BodyElement::Heading { level, text } => format!("H{}: {}", level, text), BodyElement::Paragraph { text } => { - let preview = if text.len() > 50 { - format!("{}...", &text[..50]) + let preview: String = text.chars().take(50).collect(); + if text.chars().count() > 50 { + format!("Paragraph: {}...", preview) } else { - text.clone() - }; - format!("Paragraph: {}", preview) + format!("Paragraph: {}", preview) + } + } + BodyElement::Code { language, content } => { + let lang = language.as_deref().unwrap_or("plain"); + let preview: String = content.chars().take(50).collect(); + if content.chars().count() > 50 { + format!("Code ({}): {}...", lang, preview) + } else { + format!("Code ({}): {}", lang, preview) + } + } + BodyElement::List { ordered, items } => { + let list_type = if *ordered { "ordered" } else { "unordered" }; + format!("List ({}): {} items", list_type, items.len()) } BodyElement::Chart { chart_type, title, .. } => { format!( @@ -726,6 +746,64 @@ fn build_file_context(file: &crate::db::models::File) -> String { context } +/// Build context for a focused element +fn build_focused_element_context(body: &[BodyElement], focused_index: Option) -> String { + let Some(index) = focused_index else { + return String::new(); + }; + + let Some(element) = body.get(index) else { + return format!( + "\n## Focused Element\nNote: User focused on element [{}] but it doesn't exist (document has {} elements).\n", + index, + body.len() + ); + }; + + let (element_type, full_content) = match element { + BodyElement::Heading { level, text } => { + (format!("Heading (level {})", level), text.clone()) + } + BodyElement::Paragraph { text } => { + ("Paragraph".to_string(), text.clone()) + } + BodyElement::Code { language, content } => { + let lang = language.as_deref().unwrap_or("plain"); + (format!("Code ({})", lang), content.clone()) + } + BodyElement::List { ordered, items } => { + let list_type = if *ordered { "Ordered list" } else { "Unordered list" }; + let content = items.iter() + .enumerate() + .map(|(i, item)| format!("{}. {}", i + 1, item)) + .collect::>() + .join("\n"); + (list_type.to_string(), content) + } + BodyElement::Chart { chart_type, title, .. } => { + let title_str = title.as_deref().unwrap_or("untitled"); + (format!("Chart ({:?})", chart_type), title_str.to_string()) + } + BodyElement::Image { alt, caption, .. } => { + let desc = alt.as_deref().or(caption.as_deref()).unwrap_or("no description"); + ("Image".to_string(), desc.to_string()) + } + }; + + format!( + r#" +## Focused Element +The user is focusing on element [{}]: {} +Full content of focused element: +--- +{} +--- +When the user's request is ambiguous about which element to modify, prioritize this focused element. +"#, + index, element_type, full_content + ) +} + /// Result of handling a version tool request struct VersionRequestResult { result: ToolResult, @@ -795,12 +873,25 @@ async fn handle_version_request( let desc = match element { BodyElement::Heading { level, text } => format!("H{}: {}", level, text), BodyElement::Paragraph { text } => { - let preview = if text.len() > 100 { - format!("{}...", &text[..100]) + let preview: String = text.chars().take(100).collect(); + if text.chars().count() > 100 { + format!("Paragraph: {}...", preview) } else { - text.clone() - }; - format!("Paragraph: {}", preview) + format!("Paragraph: {}", preview) + } + } + BodyElement::Code { language, content } => { + let lang = language.as_deref().unwrap_or("plain"); + let preview: String = content.chars().take(100).collect(); + if content.chars().count() > 100 { + format!("Code ({}): {}...", lang, preview) + } else { + format!("Code ({}): {}", lang, preview) + } + } + BodyElement::List { ordered, items } => { + let list_type = if *ordered { "ordered" } else { "unordered" }; + format!("List ({}): {} items", list_type, items.len()) } BodyElement::Chart { chart_type, title, .. } => { format!( -- cgit v1.2.3