feat: pres artifact 2 (#13344)
This commit is contained in:
parent
4874b9291a
commit
72dc444b2c
5 changed files with 1109 additions and 7 deletions
1
codex-rs/Cargo.lock
generated
1
codex-rs/Cargo.lock
generated
|
|
@ -1555,6 +1555,7 @@ dependencies = [
|
|||
"thiserror 2.0.18",
|
||||
"tiny_http",
|
||||
"uuid",
|
||||
"zip 2.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ serde = { workspace = true, features = ["derive"] }
|
|||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
uuid = { workspace = true, features = ["v4"] }
|
||||
zip = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,18 @@
|
|||
use super::presentation_artifact::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::io::Read;
|
||||
|
||||
fn zip_entry_text(
|
||||
path: &std::path::Path,
|
||||
entry_name: &str,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let file = std::fs::File::open(path)?;
|
||||
let mut archive = zip::ZipArchive::new(file)?;
|
||||
let mut entry = archive.by_name(entry_name)?;
|
||||
let mut text = String::new();
|
||||
entry.read_to_string(&mut text)?;
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manager_can_create_add_text_and_export() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
|
@ -275,6 +288,398 @@ fn image_uris_can_add_and_replace_images() -> Result<(), Box<dyn std::error::Err
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_slide_can_be_set_and_tracks_reorders() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
let mut manager = PresentationArtifactManager::default();
|
||||
let created = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: None,
|
||||
action: "create".to_string(),
|
||||
args: serde_json::json!({ "name": "Active Slide" }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let artifact_id = created.artifact_id;
|
||||
for _ in 0..3 {
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "add_slide".to_string(),
|
||||
args: serde_json::json!({}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
}
|
||||
let set_active = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "set_active_slide".to_string(),
|
||||
args: serde_json::json!({ "slide_index": 2 }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
assert_eq!(set_active.active_slide_index, Some(2));
|
||||
assert_eq!(
|
||||
set_active.slide_list.as_ref().map(|slides| slides
|
||||
.iter()
|
||||
.map(|slide| slide.is_active)
|
||||
.collect::<Vec<_>>()),
|
||||
Some(vec![false, false, true])
|
||||
);
|
||||
|
||||
let moved = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "move_slide".to_string(),
|
||||
args: serde_json::json!({ "from_index": 2, "to_index": 0 }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let summary = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "get_summary".to_string(),
|
||||
args: serde_json::json!({}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
assert_eq!(summary.active_slide_index, Some(0));
|
||||
assert_eq!(
|
||||
summary.slide_list.as_ref().map(|slides| slides
|
||||
.iter()
|
||||
.map(|slide| slide.is_active)
|
||||
.collect::<Vec<_>>()),
|
||||
Some(vec![true, false, false])
|
||||
);
|
||||
|
||||
let inspect = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "inspect".to_string(),
|
||||
args: serde_json::json!({ "kind": "deck,slide" }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let inspect_ndjson = inspect.inspect_ndjson.expect("inspect");
|
||||
assert!(inspect_ndjson.contains("\"activeSlideIndex\":0"));
|
||||
assert!(inspect_ndjson.contains("\"isActive\":true"));
|
||||
|
||||
let active_slide_id = moved
|
||||
.artifact_snapshot
|
||||
.as_ref()
|
||||
.and_then(|snapshot| snapshot.slides.first())
|
||||
.map(|slide| slide.slide_id.clone())
|
||||
.expect("active slide id");
|
||||
let resolved = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "resolve".to_string(),
|
||||
args: serde_json::json!({ "id": format!("sl/{active_slide_id}") }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
assert_eq!(
|
||||
resolved
|
||||
.resolved_record
|
||||
.as_ref()
|
||||
.and_then(|record| record.get("isActive"))
|
||||
.and_then(serde_json::Value::as_bool),
|
||||
Some(true)
|
||||
);
|
||||
|
||||
let deleted = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id),
|
||||
action: "delete_slide".to_string(),
|
||||
args: serde_json::json!({ "slide_index": 0 }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
assert_eq!(
|
||||
deleted
|
||||
.artifact_snapshot
|
||||
.as_ref()
|
||||
.map(|snapshot| snapshot.slide_count),
|
||||
Some(2)
|
||||
);
|
||||
let after_delete = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(deleted.artifact_id),
|
||||
action: "list_slides".to_string(),
|
||||
args: serde_json::json!({}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
assert_eq!(after_delete.active_slide_index, Some(0));
|
||||
assert_eq!(
|
||||
after_delete.slide_list.as_ref().map(|slides| slides
|
||||
.iter()
|
||||
.map(|slide| slide.is_active)
|
||||
.collect::<Vec<_>>()),
|
||||
Some(vec![true, false])
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_replace_and_insert_helpers_update_text_elements() -> Result<(), Box<dyn std::error::Error>>
|
||||
{
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
let mut manager = PresentationArtifactManager::default();
|
||||
let created = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: None,
|
||||
action: "create".to_string(),
|
||||
args: serde_json::json!({ "name": "Text Helpers" }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let artifact_id = created.artifact_id;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "add_slide".to_string(),
|
||||
args: serde_json::json!({}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let added = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "add_text_shape".to_string(),
|
||||
args: serde_json::json!({
|
||||
"slide_index": 0,
|
||||
"text": "Revenue up 24%",
|
||||
"position": { "left": 24, "top": 24, "width": 240, "height": 80 }
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let element_id = added
|
||||
.artifact_snapshot
|
||||
.as_ref()
|
||||
.and_then(|snapshot| snapshot.slides.first())
|
||||
.and_then(|slide| slide.element_ids.first())
|
||||
.cloned()
|
||||
.expect("text id");
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "replace_text".to_string(),
|
||||
args: serde_json::json!({
|
||||
"element_id": format!("sh/{element_id}"),
|
||||
"search": "24%",
|
||||
"replace": "31%"
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "insert_text_after".to_string(),
|
||||
args: serde_json::json!({
|
||||
"element_id": format!("sh/{element_id}"),
|
||||
"after": "Revenue",
|
||||
"insert": " QoQ"
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let resolved = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id),
|
||||
action: "resolve".to_string(),
|
||||
args: serde_json::json!({ "id": format!("sh/{element_id}") }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
assert_eq!(
|
||||
resolved
|
||||
.resolved_record
|
||||
.as_ref()
|
||||
.and_then(|record| record.get("text"))
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("Revenue QoQ up 31%")
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hyperlinks_are_inspectable_and_exported() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
let mut manager = PresentationArtifactManager::default();
|
||||
let created = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: None,
|
||||
action: "create".to_string(),
|
||||
args: serde_json::json!({ "name": "Hyperlinks" }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let artifact_id = created.artifact_id;
|
||||
for _ in 0..2 {
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "add_slide".to_string(),
|
||||
args: serde_json::json!({}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
}
|
||||
|
||||
let text = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "add_text_shape".to_string(),
|
||||
args: serde_json::json!({
|
||||
"slide_index": 0,
|
||||
"text": "Open roadmap",
|
||||
"position": { "left": 24, "top": 24, "width": 220, "height": 60 }
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let text_id = text
|
||||
.artifact_snapshot
|
||||
.as_ref()
|
||||
.and_then(|snapshot| snapshot.slides.first())
|
||||
.and_then(|slide| slide.element_ids.first())
|
||||
.cloned()
|
||||
.expect("text id");
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "set_hyperlink".to_string(),
|
||||
args: serde_json::json!({
|
||||
"element_id": format!("sh/{text_id}"),
|
||||
"link_type": "url",
|
||||
"url": "https://example.com/roadmap",
|
||||
"tooltip": "Roadmap",
|
||||
"highlight_click": false
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
|
||||
let shape = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "add_shape".to_string(),
|
||||
args: serde_json::json!({
|
||||
"slide_index": 0,
|
||||
"geometry": "rounded_rectangle",
|
||||
"position": { "left": 24, "top": 120, "width": 220, "height": 72 },
|
||||
"text": "Jump to appendix"
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let shape_id = shape
|
||||
.artifact_snapshot
|
||||
.as_ref()
|
||||
.and_then(|snapshot| snapshot.slides.first())
|
||||
.and_then(|slide| slide.element_ids.last())
|
||||
.cloned()
|
||||
.expect("shape id");
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "set_hyperlink".to_string(),
|
||||
args: serde_json::json!({
|
||||
"element_id": format!("sh/{shape_id}"),
|
||||
"link_type": "slide",
|
||||
"slide_index": 1,
|
||||
"tooltip": "Appendix"
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
|
||||
let inspect = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "inspect".to_string(),
|
||||
args: serde_json::json!({ "kind": "textbox,shape" }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let inspect_ndjson = inspect.inspect_ndjson.expect("inspect");
|
||||
assert!(inspect_ndjson.contains("\"type\":\"url\""));
|
||||
assert!(inspect_ndjson.contains("\"url\":\"https://example.com/roadmap\""));
|
||||
assert!(inspect_ndjson.contains("\"type\":\"slide\""));
|
||||
assert!(inspect_ndjson.contains("\"slideIndex\":1"));
|
||||
|
||||
let resolved = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "resolve".to_string(),
|
||||
args: serde_json::json!({ "id": format!("sh/{text_id}") }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
assert_eq!(
|
||||
resolved
|
||||
.resolved_record
|
||||
.as_ref()
|
||||
.and_then(|record| record.get("hyperlink"))
|
||||
.and_then(|hyperlink| hyperlink.get("url"))
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("https://example.com/roadmap")
|
||||
);
|
||||
|
||||
let export_path = temp_dir.path().join("hyperlinks.pptx");
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "export_pptx".to_string(),
|
||||
args: serde_json::json!({ "path": export_path }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let slide_xml = zip_entry_text(
|
||||
&temp_dir.path().join("hyperlinks.pptx"),
|
||||
"ppt/slides/slide1.xml",
|
||||
)?;
|
||||
let rels_xml = zip_entry_text(
|
||||
&temp_dir.path().join("hyperlinks.pptx"),
|
||||
"ppt/slides/_rels/slide1.xml.rels",
|
||||
)?;
|
||||
assert!(slide_xml.contains("hlinkClick"));
|
||||
assert!(rels_xml.contains("https://example.com/roadmap"));
|
||||
assert!(rels_xml.contains("slide2.xml"));
|
||||
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "set_hyperlink".to_string(),
|
||||
args: serde_json::json!({
|
||||
"element_id": format!("sh/{text_id}"),
|
||||
"clear": true
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let cleared = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id),
|
||||
action: "resolve".to_string(),
|
||||
args: serde_json::json!({ "id": format!("sh/{text_id}") }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
assert_eq!(
|
||||
cleared
|
||||
.resolved_record
|
||||
.as_ref()
|
||||
.and_then(|record| record.get("hyperlink")),
|
||||
None
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manager_supports_layout_theme_notes_and_inspect() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ Supported actions:
|
|||
- `append_notes`
|
||||
- `clear_notes`
|
||||
- `set_notes_visibility`
|
||||
- `set_active_slide`
|
||||
- `add_slide`
|
||||
- `insert_slide`
|
||||
- `duplicate_slide`
|
||||
|
|
@ -42,6 +43,9 @@ Supported actions:
|
|||
- `merge_table_cells`
|
||||
- `add_chart`
|
||||
- `update_text`
|
||||
- `replace_text`
|
||||
- `insert_text_after`
|
||||
- `set_hyperlink`
|
||||
- `update_shape_style`
|
||||
- `bring_to_front`
|
||||
- `send_to_back`
|
||||
|
|
@ -74,6 +78,12 @@ Example inspect:
|
|||
Example resolve:
|
||||
`{"artifact_id":"presentation_x","action":"resolve","args":{"id":"sh/element_3"}}`
|
||||
|
||||
Deck summaries, slide listings, `inspect`, and `resolve` now include active-slide metadata. Use `set_active_slide` to change it explicitly.
|
||||
|
||||
Text-bearing elements also support literal `replace_text` and `insert_text_after` helpers for in-place edits without resending the full string.
|
||||
|
||||
Text boxes and shapes support whole-element hyperlinks via `set_hyperlink`. Supported `link_type` values are `url`, `slide`, `first_slide`, `last_slide`, `next_slide`, `previous_slide`, `end_show`, `email`, and `file`. Use `clear: true` to remove an existing hyperlink.
|
||||
|
||||
Notes visibility is honored on export: `set_notes_visibility` controls whether speaker notes are emitted into exported PPTX output.
|
||||
|
||||
Image placeholders can be prompt-only. `add_image` accepts `prompt` without `path`/`data_url`, and unresolved placeholders export as a visible placeholder box instead of failing.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue