mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-18 14:44:21 +01:00
fix: remove computer controller presentation (#2956)
This commit is contained in:
@@ -24,7 +24,6 @@ use mcp_server::Router;
|
|||||||
|
|
||||||
mod docx_tool;
|
mod docx_tool;
|
||||||
mod pdf_tool;
|
mod pdf_tool;
|
||||||
mod presentation_tool;
|
|
||||||
mod xlsx_tool;
|
mod xlsx_tool;
|
||||||
|
|
||||||
mod platform;
|
mod platform;
|
||||||
@@ -363,47 +362,6 @@ impl ComputerControllerRouter {
|
|||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
|
||||||
let make_presentation_tool = Tool::new(
|
|
||||||
"make_presentation",
|
|
||||||
indoc! {r#"
|
|
||||||
Create and manage HTML presentations with a simple, modern design.
|
|
||||||
Operations:
|
|
||||||
- create: Create new presentation with template
|
|
||||||
- add_slide: Add a new slide with content
|
|
||||||
|
|
||||||
Open in a browser (using a command) to show the user: open <path>
|
|
||||||
|
|
||||||
For advanced edits, use developer tools to modify the HTML directly.
|
|
||||||
A template slide is included in comments for reference.
|
|
||||||
"#},
|
|
||||||
json!({
|
|
||||||
"type": "object",
|
|
||||||
"required": ["path", "operation"],
|
|
||||||
"properties": {
|
|
||||||
"path": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Path to the presentation file"
|
|
||||||
},
|
|
||||||
"operation": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["create", "add_slide"],
|
|
||||||
"description": "Operation to perform"
|
|
||||||
},
|
|
||||||
"params": {
|
|
||||||
"type": "object",
|
|
||||||
"description": "Parameters for add_slide operation",
|
|
||||||
"properties": {
|
|
||||||
"content": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Content for the new slide"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
|
|
||||||
let xlsx_tool = Tool::new(
|
let xlsx_tool = Tool::new(
|
||||||
"xlsx_tool",
|
"xlsx_tool",
|
||||||
indoc! {r#"
|
indoc! {r#"
|
||||||
@@ -593,7 +551,6 @@ impl ComputerControllerRouter {
|
|||||||
pdf_tool,
|
pdf_tool,
|
||||||
docx_tool,
|
docx_tool,
|
||||||
xlsx_tool,
|
xlsx_tool,
|
||||||
make_presentation_tool,
|
|
||||||
],
|
],
|
||||||
cache_dir,
|
cache_dir,
|
||||||
active_resources: Arc::new(Mutex::new(HashMap::new())),
|
active_resources: Arc::new(Mutex::new(HashMap::new())),
|
||||||
@@ -1168,24 +1125,6 @@ impl Router for ComputerControllerRouter {
|
|||||||
"pdf_tool" => this.pdf_tool(arguments).await,
|
"pdf_tool" => this.pdf_tool(arguments).await,
|
||||||
"docx_tool" => this.docx_tool(arguments).await,
|
"docx_tool" => this.docx_tool(arguments).await,
|
||||||
"xlsx_tool" => this.xlsx_tool(arguments).await,
|
"xlsx_tool" => this.xlsx_tool(arguments).await,
|
||||||
"make_presentation" => {
|
|
||||||
let path = arguments
|
|
||||||
.get("path")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.ok_or_else(|| {
|
|
||||||
ToolError::InvalidParameters("Missing 'path' parameter".into())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let operation = arguments
|
|
||||||
.get("operation")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.ok_or_else(|| {
|
|
||||||
ToolError::InvalidParameters("Missing 'operation' parameter".into())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
presentation_tool::make_presentation(path, operation, arguments.get("params"))
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
_ => Err(ToolError::NotFound(format!("Tool {} not found", tool_name))),
|
_ => Err(ToolError::NotFound(format!("Tool {} not found", tool_name))),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,398 +0,0 @@
|
|||||||
use mcp_core::{Content, ToolError};
|
|
||||||
use serde_json::Value;
|
|
||||||
use std::fs;
|
|
||||||
|
|
||||||
const TEMPLATE: &str = r#"<html>
|
|
||||||
<head>
|
|
||||||
<title>HTML and CSS Slideshow</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: Helvetica, sans-serif;
|
|
||||||
padding: 5%;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Styling the area of the slides */
|
|
||||||
#slideshow {
|
|
||||||
overflow: hidden;
|
|
||||||
height: 510px;
|
|
||||||
width: 728px;
|
|
||||||
margin: 0 auto;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Style each of the sides with a fixed width and height */
|
|
||||||
.slide {
|
|
||||||
float: left;
|
|
||||||
height: 510px;
|
|
||||||
width: 728px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 20px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add animation to the slides */
|
|
||||||
.slide-wrapper {
|
|
||||||
/* Calculate the total width on the basis of number of slides */
|
|
||||||
width: calc(728px * var(--num-slides));
|
|
||||||
transition: margin-left 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Set the background color of each of the slides */
|
|
||||||
.slide:nth-child(1) { background: #4CAF50; } /* Material Green */
|
|
||||||
.slide:nth-child(2) { background: #2196F3; } /* Material Blue */
|
|
||||||
.slide:nth-child(3) { background: #FFC107; } /* Material Amber */
|
|
||||||
|
|
||||||
/* Style slide content */
|
|
||||||
.slide h1 {
|
|
||||||
color: white;
|
|
||||||
font-size: 2.5em;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide p {
|
|
||||||
color: white;
|
|
||||||
font-size: 1.5em;
|
|
||||||
line-height: 1.4;
|
|
||||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
|
|
||||||
max-width: 90%;
|
|
||||||
margin: 0.5em auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide ul, .slide ol {
|
|
||||||
color: white;
|
|
||||||
font-size: 1.2em;
|
|
||||||
text-align: left;
|
|
||||||
margin: 1em auto;
|
|
||||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
|
|
||||||
max-width: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide li {
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide ul ul, .slide ol ol {
|
|
||||||
font-size: 0.9em;
|
|
||||||
margin: 0.5em 0 0.5em 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide pre {
|
|
||||||
font-size: 1.1em;
|
|
||||||
text-align: left;
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
padding: 1em;
|
|
||||||
border-radius: 5px;
|
|
||||||
max-width: 90%;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-hint {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 20px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
background: rgba(0, 0, 0, 0.7);
|
|
||||||
color: white;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: 16px;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-hint.fade {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- Define the slideshow container -->
|
|
||||||
<div id="slideshow">
|
|
||||||
<div class="slide-wrapper" style="--num-slides: 2">
|
|
||||||
<!-- First slide -->
|
|
||||||
<div class="slide">
|
|
||||||
<h1>Your Presentation</h1>
|
|
||||||
<p>Use arrow keys to navigate</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- SLIDE_TEMPLATE (do not remove this comment)
|
|
||||||
|
|
||||||
<div class="slide">
|
|
||||||
<h1>New Slide Title</h1>
|
|
||||||
<p>Slide content goes here, can use rich like below:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Use make_presentation to:
|
|
||||||
<ul>
|
|
||||||
<li>create - Create new presentation</li>
|
|
||||||
<li>add_slide - Add a new slide with content</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>For manual edits:
|
|
||||||
<ul>
|
|
||||||
<li>Use developer tools to edit the HTML</li>
|
|
||||||
<li>Update --num-slides in slide-wrapper</li>
|
|
||||||
<li>Copy template below for new slides</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
END_SLIDE_TEMPLATE -->
|
|
||||||
|
|
||||||
<!-- ADD_SLIDES_HERE (do not remove this comment) -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="nav-hint">
|
|
||||||
Use ← and → arrow keys to navigate
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const slideWrapper = document.querySelector('.slide-wrapper');
|
|
||||||
const slideWidth = 728;
|
|
||||||
let currentSlide = 0;
|
|
||||||
const hint = document.querySelector('.nav-hint');
|
|
||||||
const totalSlides = document.querySelectorAll('.slide').length;
|
|
||||||
|
|
||||||
// Hide hint after 5 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
hint.classList.add('fade');
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'ArrowLeft') {
|
|
||||||
if (currentSlide > 0) {
|
|
||||||
currentSlide--;
|
|
||||||
updateSlide();
|
|
||||||
}
|
|
||||||
} else if (e.key === 'ArrowRight') {
|
|
||||||
if (currentSlide < totalSlides - 1) {
|
|
||||||
currentSlide++;
|
|
||||||
updateSlide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function updateSlide() {
|
|
||||||
slideWrapper.style.marginLeft = `-${currentSlide * slideWidth}px`;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>"#;
|
|
||||||
|
|
||||||
pub async fn make_presentation(
|
|
||||||
path: &str,
|
|
||||||
operation: &str,
|
|
||||||
params: Option<&Value>,
|
|
||||||
) -> Result<Vec<Content>, ToolError> {
|
|
||||||
match operation {
|
|
||||||
"create" => {
|
|
||||||
// Get title from params or use default
|
|
||||||
let title = params
|
|
||||||
.and_then(|p| p.get("title"))
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or("Your Presentation");
|
|
||||||
|
|
||||||
// Replace title in template
|
|
||||||
let content = TEMPLATE.replace("Your Presentation", title);
|
|
||||||
|
|
||||||
// Create a new presentation with the template
|
|
||||||
fs::write(path, content).map_err(|e| {
|
|
||||||
ToolError::ExecutionError(format!("Failed to create presentation file: {}", e))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(vec![Content::text(format!(
|
|
||||||
"Created new presentation with title '{}' at: {}\nYou can open it with the command: `open {}` to show user. You should look at the html and consider if you want to ask user if they need to adjust it, colours, typeface and so on.",
|
|
||||||
title, path, path
|
|
||||||
))])
|
|
||||||
}
|
|
||||||
"add_slide" => {
|
|
||||||
let content = params
|
|
||||||
.and_then(|p| p.get("content"))
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.ok_or_else(|| {
|
|
||||||
ToolError::InvalidParameters("Missing 'content' parameter for slide".into())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Read existing file
|
|
||||||
let mut html = fs::read_to_string(path).map_err(|e| {
|
|
||||||
ToolError::ExecutionError(format!("Failed to read presentation file: {}", e))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Find the marker comment
|
|
||||||
let marker = "<!-- ADD_SLIDES_HERE";
|
|
||||||
let insert_pos = html.find(marker).ok_or_else(|| {
|
|
||||||
ToolError::ExecutionError("Invalid presentation file format".into())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Count actual slides (excluding template)
|
|
||||||
let current_slides = html.matches("class=\"slide\"").count() - 1; // -1 for template
|
|
||||||
let new_count = current_slides + 1;
|
|
||||||
|
|
||||||
// Update the num-slides value
|
|
||||||
html = html.replace(
|
|
||||||
&format!("--num-slides: {}", current_slides),
|
|
||||||
&format!("--num-slides: {}", new_count),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create new slide HTML
|
|
||||||
let slide_html = format!(
|
|
||||||
r#" <div class="slide">
|
|
||||||
<h1>{}</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}"#,
|
|
||||||
content, marker
|
|
||||||
);
|
|
||||||
|
|
||||||
// Insert the new slide
|
|
||||||
html.replace_range(insert_pos..insert_pos + marker.len(), &slide_html);
|
|
||||||
|
|
||||||
// Save the file
|
|
||||||
fs::write(path, html).map_err(|e| {
|
|
||||||
ToolError::ExecutionError(format!("Failed to update presentation file: {}", e))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(vec![Content::text(format!(
|
|
||||||
"Added new slide to presentation. You can view it with: open {}\nNote: when creating, or adding a slide, if the content for a slide is long, edit it so that it uses appropriate size, formatting, lists etc (and can even split it to other slides if needed).",
|
|
||||||
path
|
|
||||||
))])
|
|
||||||
}
|
|
||||||
_ => Err(ToolError::InvalidParameters(format!(
|
|
||||||
"Invalid operation: {}. Valid operations are: create, add_slide",
|
|
||||||
operation
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_create_presentation() {
|
|
||||||
let test_dir = tempfile::tempdir().unwrap();
|
|
||||||
let test_path = test_dir.path().join("test_presentation.html");
|
|
||||||
let path_str = test_path.to_str().unwrap();
|
|
||||||
|
|
||||||
// Test default title
|
|
||||||
let result = make_presentation(path_str, "create", None).await;
|
|
||||||
assert!(result.is_ok(), "Should successfully create presentation");
|
|
||||||
|
|
||||||
// Verify the file exists and contains the default title
|
|
||||||
assert!(test_path.exists(), "Presentation file should exist");
|
|
||||||
let content = fs::read_to_string(&test_path).unwrap();
|
|
||||||
assert!(
|
|
||||||
content.contains("Your Presentation"),
|
|
||||||
"Should contain default title"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test custom title
|
|
||||||
let test_path2 = test_dir.path().join("test_presentation2.html");
|
|
||||||
let path_str2 = test_path2.to_str().unwrap();
|
|
||||||
let params = serde_json::json!({
|
|
||||||
"title": "Custom Title Test"
|
|
||||||
});
|
|
||||||
let result = make_presentation(path_str2, "create", Some(¶ms)).await;
|
|
||||||
assert!(
|
|
||||||
result.is_ok(),
|
|
||||||
"Should successfully create presentation with custom title"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify custom title
|
|
||||||
let content = fs::read_to_string(&test_path2).unwrap();
|
|
||||||
assert!(
|
|
||||||
content.contains("Custom Title Test"),
|
|
||||||
"Should contain custom title"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
content.contains("SLIDE_TEMPLATE"),
|
|
||||||
"Should contain slide template"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
content.contains("ADD_SLIDES_HERE"),
|
|
||||||
"Should contain slides marker"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
test_dir.close().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_add_slide() {
|
|
||||||
let test_dir = tempfile::tempdir().unwrap();
|
|
||||||
let test_path = test_dir.path().join("test_presentation.html");
|
|
||||||
let path_str = test_path.to_str().unwrap();
|
|
||||||
|
|
||||||
// First create the presentation
|
|
||||||
let result = make_presentation(path_str, "create", None).await;
|
|
||||||
assert!(result.is_ok(), "Should successfully create presentation");
|
|
||||||
|
|
||||||
// Add a new slide
|
|
||||||
let params = serde_json::json!({
|
|
||||||
"content": "New Test Slide"
|
|
||||||
});
|
|
||||||
let result = make_presentation(path_str, "add_slide", Some(¶ms)).await;
|
|
||||||
assert!(result.is_ok(), "Should successfully add slide");
|
|
||||||
|
|
||||||
// Verify the content
|
|
||||||
let content = fs::read_to_string(&test_path).unwrap();
|
|
||||||
assert!(
|
|
||||||
content.contains("New Test Slide"),
|
|
||||||
"Should contain new slide content"
|
|
||||||
);
|
|
||||||
// Initial template has 1 slide + new slide = 2
|
|
||||||
assert!(
|
|
||||||
content.contains("--num-slides: 2"),
|
|
||||||
"Should have correct slide count"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
content.contains("ADD_SLIDES_HERE"),
|
|
||||||
"Should preserve marker"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
test_dir.close().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_add_slide_without_content() {
|
|
||||||
let test_dir = tempfile::tempdir().unwrap();
|
|
||||||
let test_path = test_dir.path().join("test_presentation.html");
|
|
||||||
let path_str = test_path.to_str().unwrap();
|
|
||||||
|
|
||||||
// Create the presentation first
|
|
||||||
let _ = make_presentation(path_str, "create", None).await;
|
|
||||||
|
|
||||||
// Try to add slide without content
|
|
||||||
let result = make_presentation(path_str, "add_slide", None).await;
|
|
||||||
assert!(result.is_err(), "Should fail without content");
|
|
||||||
match result {
|
|
||||||
Err(ToolError::InvalidParameters(msg)) => {
|
|
||||||
assert!(msg.contains("Missing 'content' parameter"));
|
|
||||||
}
|
|
||||||
_ => panic!("Expected InvalidParameters error"),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
test_dir.close().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_invalid_operation() {
|
|
||||||
let result = make_presentation("test.html", "invalid", None).await;
|
|
||||||
assert!(result.is_err(), "Should fail with invalid operation");
|
|
||||||
match result {
|
|
||||||
Err(ToolError::InvalidParameters(msg)) => {
|
|
||||||
assert!(msg.contains("Valid operations are: create, add_slide"));
|
|
||||||
}
|
|
||||||
_ => panic!("Expected InvalidParameters error"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user