-
Notifications
You must be signed in to change notification settings - Fork 12
Add enhanced Word, Excel and PowerPoint tools #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| import type { Tool } from "@github/copilot-sdk"; | ||
|
|
||
| export const applyStyleToSelection: Tool = { | ||
| name: "apply_style_to_selection", | ||
| description: `Apply formatting styles to the currently selected text in Word. | ||
|
|
||
| All parameters are optional - only specified styles will be applied. | ||
|
|
||
| Parameters: | ||
| - bold: Set text to bold (true) or remove bold (false) | ||
| - italic: Set text to italic (true) or remove italic (false) | ||
| - underline: Set text to underline (true) or remove underline (false) | ||
| - strikethrough: Set strikethrough (true) or remove it (false) | ||
| - fontSize: Font size in points (e.g., 12, 14, 24) | ||
| - fontName: Font family name (e.g., "Arial", "Times New Roman", "Calibri") | ||
| - fontColor: Text color as hex string (e.g., "FF0000" for red, "0000FF" for blue) | ||
| - highlightColor: Highlight/background color. Use Word highlight colors: "yellow", "green", "cyan", "magenta", "blue", "red", "darkBlue", "darkCyan", "darkGreen", "darkMagenta", "darkRed", "darkYellow", "gray25", "gray50", "black", or "noHighlight" to remove | ||
|
|
||
| Examples: | ||
| - Make text bold and red: bold=true, fontColor="FF0000" | ||
| - Increase font size: fontSize=16 | ||
| - Highlight important text: highlightColor="yellow" | ||
| - Apply multiple styles: bold=true, italic=true, fontSize=14, fontName="Arial"`, | ||
| parameters: { | ||
| type: "object", | ||
| properties: { | ||
| bold: { | ||
| type: "boolean", | ||
| description: "Set to true for bold, false to remove bold.", | ||
| }, | ||
| italic: { | ||
| type: "boolean", | ||
| description: "Set to true for italic, false to remove italic.", | ||
| }, | ||
| underline: { | ||
| type: "boolean", | ||
| description: "Set to true for underline, false to remove underline.", | ||
| }, | ||
| strikethrough: { | ||
| type: "boolean", | ||
| description: "Set to true for strikethrough, false to remove it.", | ||
| }, | ||
| fontSize: { | ||
| type: "number", | ||
| description: "Font size in points.", | ||
| }, | ||
| fontName: { | ||
| type: "string", | ||
| description: "Font family name (e.g., 'Arial', 'Calibri').", | ||
| }, | ||
| fontColor: { | ||
| type: "string", | ||
| description: "Text color as hex string without # (e.g., 'FF0000' for red).", | ||
| }, | ||
| highlightColor: { | ||
| type: "string", | ||
| description: "Highlight color name (e.g., 'yellow', 'green', 'noHighlight').", | ||
| }, | ||
| }, | ||
| required: [], | ||
| }, | ||
| handler: async ({ arguments: args }) => { | ||
| const { | ||
| bold, | ||
| italic, | ||
| underline, | ||
| strikethrough, | ||
| fontSize, | ||
| fontName, | ||
| fontColor, | ||
| highlightColor, | ||
| } = args as { | ||
| bold?: boolean; | ||
| italic?: boolean; | ||
| underline?: boolean; | ||
| strikethrough?: boolean; | ||
| fontSize?: number; | ||
| fontName?: string; | ||
| fontColor?: string; | ||
| highlightColor?: string; | ||
| }; | ||
|
|
||
| // Check if any style was specified | ||
| const hasStyles = bold !== undefined || italic !== undefined || underline !== undefined || | ||
| strikethrough !== undefined || fontSize !== undefined || fontName !== undefined || | ||
| fontColor !== undefined || highlightColor !== undefined; | ||
|
|
||
| if (!hasStyles) { | ||
| return { textResultForLlm: "No styles specified. Provide at least one style parameter.", resultType: "failure", error: "No styles", toolTelemetry: {} }; | ||
| } | ||
|
|
||
| try { | ||
| return await Word.run(async (context) => { | ||
| const selection = context.document.getSelection(); | ||
| selection.load("text"); | ||
| await context.sync(); | ||
|
|
||
| if (!selection.text || selection.text.trim().length === 0) { | ||
| return "No text selected. Please select some text first."; | ||
| } | ||
|
|
||
| const font = selection.font; | ||
|
|
||
| // Apply each specified style | ||
| if (bold !== undefined) { | ||
| font.bold = bold; | ||
| } | ||
| if (italic !== undefined) { | ||
| font.italic = italic; | ||
| } | ||
| if (underline !== undefined) { | ||
| font.underline = underline ? Word.UnderlineType.single : Word.UnderlineType.none; | ||
| } | ||
| if (strikethrough !== undefined) { | ||
| font.strikeThrough = strikethrough; | ||
| } | ||
| if (fontSize !== undefined) { | ||
| font.size = fontSize; | ||
| } | ||
| if (fontName !== undefined) { | ||
| font.name = fontName; | ||
| } | ||
| if (fontColor !== undefined) { | ||
| font.color = fontColor.startsWith("#") ? fontColor : `#${fontColor}`; | ||
| } | ||
| if (highlightColor !== undefined) { | ||
| // Map string to Word.HighlightColor | ||
| const colorMap: { [key: string]: Word.HighlightColor } = { | ||
| "yellow": Word.HighlightColor.yellow, | ||
| "green": Word.HighlightColor.green, | ||
| "cyan": Word.HighlightColor.turquoise, | ||
| "turquoise": Word.HighlightColor.turquoise, | ||
| "magenta": Word.HighlightColor.pink, | ||
| "pink": Word.HighlightColor.pink, | ||
| "blue": Word.HighlightColor.blue, | ||
| "red": Word.HighlightColor.red, | ||
| "darkblue": Word.HighlightColor.darkBlue, | ||
| "darkcyan": Word.HighlightColor.darkCyan, | ||
| "darkgreen": Word.HighlightColor.darkGreen, | ||
| "darkmagenta": Word.HighlightColor.darkMagenta, | ||
| "darkred": Word.HighlightColor.darkRed, | ||
| "darkyellow": Word.HighlightColor.darkYellow, | ||
| "gray25": Word.HighlightColor.lightGray, | ||
| "lightgray": Word.HighlightColor.lightGray, | ||
| "gray50": Word.HighlightColor.darkGray, | ||
| "darkgray": Word.HighlightColor.darkGray, | ||
| "black": Word.HighlightColor.black, | ||
| "nohighlight": Word.HighlightColor.noHighlight, | ||
| "none": Word.HighlightColor.noHighlight, | ||
| }; | ||
| const color = colorMap[highlightColor.toLowerCase()]; | ||
| if (color !== undefined) { | ||
| font.highlightColor = color; | ||
| } | ||
| } | ||
|
|
||
| await context.sync(); | ||
|
|
||
| // Build confirmation message | ||
| const applied: string[] = []; | ||
| if (bold !== undefined) applied.push(bold ? "bold" : "not bold"); | ||
| if (italic !== undefined) applied.push(italic ? "italic" : "not italic"); | ||
| if (underline !== undefined) applied.push(underline ? "underlined" : "not underlined"); | ||
| if (strikethrough !== undefined) applied.push(strikethrough ? "strikethrough" : "no strikethrough"); | ||
| if (fontSize !== undefined) applied.push(`${fontSize}pt`); | ||
| if (fontName !== undefined) applied.push(fontName); | ||
| if (fontColor !== undefined) applied.push(`color #${fontColor.replace("#", "")}`); | ||
| if (highlightColor !== undefined) applied.push(`${highlightColor} highlight`); | ||
|
|
||
| return `Applied formatting: ${applied.join(", ")}.`; | ||
| }); | ||
| } catch (e: any) { | ||
| return { textResultForLlm: e.message, resultType: "failure", error: e.message, toolTelemetry: {} }; | ||
| } | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| import type { Tool } from "@github/copilot-sdk"; | ||
|
|
||
| export const duplicateSlide: Tool = { | ||
| name: "duplicate_slide", | ||
| description: `Duplicate an existing slide in the PowerPoint presentation. | ||
|
|
||
| Parameters: | ||
| - sourceIndex: 0-based index of the slide to duplicate | ||
| - targetIndex: Optional 0-based index where the duplicate should be inserted. | ||
| If omitted, the duplicate is placed immediately after the source slide. | ||
|
|
||
| The duplicated slide preserves all content, formatting, and layout from the original. | ||
|
|
||
| Examples: | ||
| - Duplicate slide 1 (index 0) and place after it: sourceIndex=0 | ||
| - Duplicate slide 3 and place at the end: sourceIndex=2, targetIndex=<slideCount> | ||
| - Duplicate slide 5 and place at position 2: sourceIndex=4, targetIndex=1`, | ||
| parameters: { | ||
| type: "object", | ||
| properties: { | ||
| sourceIndex: { | ||
| type: "number", | ||
| description: "0-based index of the slide to duplicate.", | ||
| }, | ||
| targetIndex: { | ||
| type: "number", | ||
| description: "0-based index where the duplicate should be inserted. Default is after the source slide.", | ||
| }, | ||
| }, | ||
| required: ["sourceIndex"], | ||
| }, | ||
| handler: async ({ arguments: args }) => { | ||
| const { sourceIndex, targetIndex } = args as { sourceIndex: number; targetIndex?: number }; | ||
|
|
||
| try { | ||
| return await PowerPoint.run(async (context) => { | ||
| const slides = context.presentation.slides; | ||
| slides.load("items"); | ||
| await context.sync(); | ||
|
|
||
| const slideCount = slides.items.length; | ||
| if (slideCount === 0) { | ||
| return "Presentation has no slides."; | ||
| } | ||
|
|
||
| if (sourceIndex < 0 || sourceIndex >= slideCount) { | ||
| return `Invalid sourceIndex ${sourceIndex}. Must be 0-${slideCount - 1}.`; | ||
| } | ||
|
|
||
| // Determine insert position | ||
| const insertAfterIndex = targetIndex !== undefined ? targetIndex - 1 : sourceIndex; | ||
|
|
||
| // Get the source slide | ||
| const sourceSlide = slides.items[sourceIndex]; | ||
| sourceSlide.load("id"); | ||
| await context.sync(); | ||
|
|
||
| // Get the slide to insert after (if not inserting at the beginning) | ||
| let targetSlideId: string | undefined; | ||
| if (insertAfterIndex >= 0 && insertAfterIndex < slideCount) { | ||
| const targetSlide = slides.items[insertAfterIndex]; | ||
| targetSlide.load("id"); | ||
| await context.sync(); | ||
| targetSlideId = targetSlide.id; | ||
| } | ||
|
|
||
| // Use the setSelectedSlides and copy approach | ||
| // First, we need to export the slide and re-import it | ||
| // PowerPoint JS API approach: use insertSlidesFromBase64 with slide selection | ||
|
|
||
| // Get the presentation as base64 (we'll extract just our slide) | ||
| // Note: This is a workaround since direct slide duplication isn't in the API | ||
|
|
||
| // Alternative approach: Use the slides collection's getItemAt and copy | ||
| // The PowerPoint JS API v1.5+ supports slide manipulation | ||
|
|
||
| // Since direct duplication isn't available, we'll use shape copying | ||
| const newSlide = slides.add(); | ||
| await context.sync(); | ||
|
|
||
| // Load the new slide and source slide shapes | ||
| newSlide.load("id"); | ||
| sourceSlide.shapes.load("items"); | ||
| await context.sync(); | ||
|
|
||
| // Get shapes from source | ||
| for (const shape of sourceSlide.shapes.items) { | ||
| shape.load(["type", "left", "top", "width", "height"]); | ||
| try { | ||
| shape.textFrame.textRange.load("text"); | ||
| } catch {} | ||
| } | ||
| await context.sync(); | ||
|
|
||
| // Copy text shapes (basic duplication - full OOXML copy would be more complete) | ||
| for (const shape of sourceSlide.shapes.items) { | ||
| try { | ||
| const text = shape.textFrame?.textRange?.text; | ||
| if (text) { | ||
| newSlide.shapes.addTextBox(text, { | ||
| left: shape.left, | ||
| top: shape.top, | ||
| width: shape.width, | ||
| height: shape.height, | ||
| }); | ||
| } | ||
| } catch { | ||
| // Shape might not have text, skip | ||
| } | ||
| } | ||
|
|
||
| await context.sync(); | ||
|
|
||
| // Move the slide to the target position if needed | ||
| if (targetIndex !== undefined && targetIndex !== slideCount) { | ||
| // Reload slides to get updated order | ||
| slides.load("items"); | ||
| await context.sync(); | ||
|
|
||
| // Find and move the new slide | ||
| // Note: Moving slides requires specific API support | ||
|
Comment on lines
+115
to
+121
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| const newPosition = targetIndex !== undefined ? targetIndex + 1 : sourceIndex + 2; | ||
| return `Duplicated slide ${sourceIndex + 1}. New slide created at position ${newPosition} (note: complex shapes/images may need manual adjustment).`; | ||
| }); | ||
| } catch (e: any) { | ||
| return { textResultForLlm: e.message, resultType: "failure", error: e.message, toolTelemetry: {} }; | ||
| } | ||
| }, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| import type { Tool } from "@github/copilot-sdk"; | ||
|
|
||
| export const findAndReplace: Tool = { | ||
| name: "find_and_replace", | ||
| description: `Find and replace text in the Word document. | ||
|
|
||
| Searches the entire document for occurrences of the search text and replaces them. | ||
|
|
||
| Parameters: | ||
| - find: The text to search for | ||
| - replace: The text to replace it with | ||
| - matchCase: If true, search is case-sensitive (default: false) | ||
| - matchWholeWord: If true, only match whole words, not partial matches (default: false) | ||
|
|
||
| Returns the number of replacements made. | ||
|
|
||
| Examples: | ||
| - Replace all "colour" with "color": find="colour", replace="color" | ||
| - Replace exact case "JavaScript": find="JavaScript", replace="TypeScript", matchCase=true | ||
| - Replace whole word "cat" (not "category"): find="cat", replace="dog", matchWholeWord=true`, | ||
| parameters: { | ||
| type: "object", | ||
| properties: { | ||
| find: { | ||
| type: "string", | ||
| description: "The text to search for.", | ||
| }, | ||
| replace: { | ||
| type: "string", | ||
| description: "The text to replace matches with.", | ||
| }, | ||
| matchCase: { | ||
| type: "boolean", | ||
| description: "If true, the search is case-sensitive. Default is false.", | ||
| }, | ||
| matchWholeWord: { | ||
| type: "boolean", | ||
| description: "If true, only matches whole words. Default is false.", | ||
| }, | ||
| }, | ||
| required: ["find", "replace"], | ||
| }, | ||
| handler: async ({ arguments: args }) => { | ||
| const { find, replace, matchCase = false, matchWholeWord = false } = args as { | ||
| find: string; | ||
| replace: string; | ||
| matchCase?: boolean; | ||
| matchWholeWord?: boolean; | ||
| }; | ||
|
|
||
| if (!find || find.length === 0) { | ||
| return { textResultForLlm: "Search text cannot be empty.", resultType: "failure", error: "Empty search", toolTelemetry: {} }; | ||
| } | ||
|
|
||
| try { | ||
| return await Word.run(async (context) => { | ||
| const body = context.document.body; | ||
|
|
||
| // Create search options | ||
| const searchResults = body.search(find, { | ||
| ignorePunct: false, | ||
| ignoreSpace: false, | ||
| matchCase: matchCase, | ||
| matchWholeWord: matchWholeWord, | ||
| }); | ||
|
|
||
| searchResults.load("items"); | ||
| await context.sync(); | ||
|
|
||
| const count = searchResults.items.length; | ||
|
|
||
| if (count === 0) { | ||
| return `No matches found for "${find}".`; | ||
| } | ||
|
|
||
| // Replace all matches | ||
| for (const result of searchResults.items) { | ||
| result.insertText(replace, Word.InsertLocation.replace); | ||
| } | ||
|
|
||
| await context.sync(); | ||
|
|
||
| return `Replaced ${count} occurrence${count === 1 ? "" : "s"} of "${find}" with "${replace}".`; | ||
| }); | ||
| } catch (e: any) { | ||
| return { textResultForLlm: e.message, resultType: "failure", error: e.message, toolTelemetry: {} }; | ||
| } | ||
| }, | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The duplication logic recreates only text boxes (
addTextBox) from shapes that expose text and drops everything else (images, charts, tables, lines, formatting/layout metadata). For typical slides this produces a materially incomplete duplicate while the tool advertises full duplication, so users can lose most slide content.Useful? React with 👍 / 👎.