Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion GETTING_STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Install the following software:
### 1. Clone and Install Dependencies

```bash
git clone https://github.com/patniko/github-copilot-office.git
git clone https://github.com/<your-account>/github-copilot-office.git
cd github-copilot-office
npm install
```
Expand Down Expand Up @@ -62,6 +62,11 @@ You should see the GitHub Copilot icon appear in your system tray (Windows) or m

6. Have fun!

### Local files
- Use **Attach files** in the chat box to add local documents from your computer.
- Pasted images still work as before.
- For best results, use files that Copilot can read directly, such as text, PDF, Word, PowerPoint, spreadsheet, or code files.

https://github.com/user-attachments/assets/5bb771d3-0bf6-4b7b-8e6c-757a085b3131

## Troubleshooting
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@

A Microsoft Office add-in that integrates GitHub Copilot into Word, Excel, and PowerPoint.

## Fork / Reference

This project is based on the open-source repository:

- **Reference project:** `patniko/github-copilot-office`

This fork keeps the original Office add-in idea and extends it for local use and publishing.

## What changed in this fork

- Added a safer local file upload flow for chat attachments
- Kept image paste/upload support and made it work through a shared upload helper
- Improved the PowerPoint/Office setup notes so the deployment flow is clearer
- Added documentation that explains the reference project and the fork-specific changes
- Tuned the local development flow so the add-in is easier to run and verify on Windows

## Getting Started

**👉 See [GETTING_STARTED.md](GETTING_STARTED.md) for setup instructions.**
Expand Down
33 changes: 16 additions & 17 deletions src/server-prod.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const path = require('path');
const fs = require('fs');
const os = require('os');
const { setupCopilotProxy } = require('./copilotProxy');
const { saveUploadedFile } = require('./uploadFile');

// Determine if we're running from pkg bundle
const isPkg = typeof process.pkg !== 'undefined';
Expand Down Expand Up @@ -47,31 +48,29 @@ async function createServer() {
return res.status(400).json({ error: 'Invalid image data' });
}

const matches = dataUrl.match(/^data:image\/([a-zA-Z+]+);base64,(.+)$/);
if (!matches || matches.length !== 3) {
return res.status(400).json({ error: 'Invalid data URL format' });
}

const extension = matches[1] === 'svg+xml' ? 'svg' : matches[1];
const base64Data = matches[2];
const buffer = Buffer.from(base64Data, 'base64');

const tempDir = path.join(os.tmpdir(), 'copilot-office-images');
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
if (!dataUrl.startsWith('data:image/')) {
return res.status(400).json({ error: 'Invalid image data' });
}

const filename = name || `image-${Date.now()}.${extension}`;
const filepath = path.join(tempDir, filename);
fs.writeFileSync(filepath, buffer);

res.json({ path: filepath, name: filename });
const saved = saveUploadedFile({ dataUrl, name, defaultExtension: '.png', tempSubdir: 'copilot-office-images' });
res.json({ path: saved.path, name: saved.name });
} catch (error) {
console.error('Upload error:', error);
res.status(500).json({ error: error.message });
}
});

apiRouter.post('/upload-file', async (req, res) => {
try {
const { dataUrl, name } = req.body;
const saved = saveUploadedFile({ dataUrl, name });
res.json({ path: saved.path, name: saved.name, mimeType: saved.mimeType });
} catch (error) {
console.error('Upload error:', error);
res.status(400).json({ error: error.message });
}
});

apiRouter.get('/fetch', async (req, res) => {
const url = req.query.url;
if (!url) {
Expand Down
39 changes: 16 additions & 23 deletions src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const path = require('path');
const fs = require('fs');
const os = require('os');
const { setupCopilotProxy } = require('./copilotProxy');
const { saveUploadedFile } = require('./uploadFile');

async function createServer() {
const app = express();
Expand All @@ -27,36 +28,29 @@ async function createServer() {
return res.status(400).json({ error: 'Invalid image data' });
}

// Extract base64 data
const matches = dataUrl.match(/^data:image\/([a-zA-Z+]+);base64,(.+)$/);
if (!matches || matches.length !== 3) {
return res.status(400).json({ error: 'Invalid data URL format' });
}

const extension = matches[1] === 'svg+xml' ? 'svg' : matches[1];
const base64Data = matches[2];
const buffer = Buffer.from(base64Data, 'base64');

// Create temp directory if it doesn't exist
const tempDir = path.join(os.tmpdir(), 'copilot-office-images');
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
if (!dataUrl.startsWith('data:image/')) {
return res.status(400).json({ error: 'Invalid image data' });
}

// Generate unique filename
const filename = name || `image-${Date.now()}.${extension}`;
const filepath = path.join(tempDir, filename);

// Write file
fs.writeFileSync(filepath, buffer);

res.json({ path: filepath, name: filename });
const saved = saveUploadedFile({ dataUrl, name, defaultExtension: '.png', tempSubdir: 'copilot-office-images' });
res.json({ path: saved.path, name: saved.name });
} catch (error) {
console.error('Upload error:', error);
res.status(500).json({ error: error.message });
}
});

apiRouter.post('/upload-file', async (req, res) => {
try {
const { dataUrl, name } = req.body;
const saved = saveUploadedFile({ dataUrl, name });
res.json({ path: saved.path, name: saved.name, mimeType: saved.mimeType });
} catch (error) {
console.error('Upload error:', error);
res.status(400).json({ error: error.message });
}
});

// Proxy for web fetch (GET only, avoids CORS)
apiRouter.get('/fetch', async (req, res) => {
const url = req.query.url;
Expand Down Expand Up @@ -186,4 +180,3 @@ async function createServer() {
createServer().catch(console.error);



63 changes: 44 additions & 19 deletions src/ui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
webDarkTheme,
makeStyles,
} from "@fluentui/react-components";
import { ChatInput, ImageAttachment } from "./components/ChatInput";
import { ChatInput, ImageAttachment, FileAttachment } from "./components/ChatInput";
import { Message, MessageList, DebugEvent } from "./components/MessageList";
import { HeaderBar, ModelType } from "./components/HeaderBar";
import { SessionHistory } from "./components/SessionHistory";
Expand Down Expand Up @@ -58,6 +58,7 @@ export const App: React.FC = () => {
const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState("");
const [images, setImages] = useState<ImageAttachment[]>([]);
const [files, setFiles] = useState<FileAttachment[]>([]);
const [isTyping, setIsTyping] = useState(false);
const [currentActivity, setCurrentActivity] = useState<string>("");
const [streamingText, setStreamingText] = useState<string>("");
Expand Down Expand Up @@ -143,6 +144,7 @@ export const App: React.FC = () => {
setMessages(restoredMessages || []);
setInputValue("");
setImages([]);
setFiles([]);
setIsTyping(false);
setCurrentActivity("");
setStreamingText("");
Expand Down Expand Up @@ -239,7 +241,13 @@ Always use your tools to interact with the document. Never ask users to save, ex
// Add user message with images
setMessages((prev) => [...prev, {
id: crypto.randomUUID(),
text: inputValue || (images.length > 0 ? `Sent ${images.length} image${images.length > 1 ? 's' : ''}` : ''),
text: inputValue || (
images.length > 0
? `Sent ${images.length} image${images.length > 1 ? 's' : ''}`
: files.length > 0
? `Sent ${files.length} file${files.length > 1 ? 's' : ''}`
: ''
),
sender: "user",
timestamp: new Date(),
images: images.length > 0 ? images.map(img => ({ dataUrl: img.dataUrl, name: img.name })) : undefined,
Expand All @@ -255,25 +263,26 @@ Always use your tools to interact with the document. Never ask users to save, ex
setError("");

try {
// Upload images to server and get file paths
// Upload local files to the server and get file paths
const attachments: Array<{ type: "file", path: string, displayName?: string }> = [];


const uploadItem = async (dataUrl: string, name: string, endpoint: string) => {
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ dataUrl, name }),
});

if (!response.ok) {
throw new Error(`Failed to upload ${name}: ${response.statusText}`);
}

return response.json();
};

for (const image of userImages) {
try {
const response = await fetch('/api/upload-image', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
dataUrl: image.dataUrl,
name: image.name
}),
});

if (!response.ok) {
throw new Error(`Failed to upload image: ${response.statusText}`);
}

const result = await response.json();
const result = await uploadItem(image.dataUrl, image.name, '/api/upload-image');
attachments.push({
type: "file",
path: result.path,
Expand All @@ -285,6 +294,20 @@ Always use your tools to interact with the document. Never ask users to save, ex
}
}

for (const file of files) {
try {
const result = await uploadItem(file.dataUrl, file.name, '/api/upload-file');
attachments.push({
type: "file",
path: result.path,
displayName: file.name,
});
} catch (uploadError: any) {
console.error('File upload error:', uploadError);
setError(`Failed to upload file: ${uploadError.message}`);
}
}

const addDebugMessage = (text: string) => {
setMessages((prev) => [...prev, {
id: `debug-${Date.now()}`,
Expand All @@ -297,7 +320,7 @@ Always use your tools to interact with the document. Never ask users to save, ex
let eventCount = 0;
trafficStats.reset();
for await (const event of session.query({
prompt: userInput || "Here are some images for you to analyze.",
prompt: userInput || (files.length > 0 ? "Here are some files for you to analyze." : "Here are some images for you to analyze."),
attachments: attachments.length > 0 ? attachments : undefined
})) {
eventCount++;
Expand Down Expand Up @@ -433,6 +456,8 @@ Always use your tools to interact with the document. Never ask users to save, ex
onSend={handleSend}
images={images}
onImagesChange={setImages}
files={files}
onFilesChange={setFiles}
/>
</div>
</FluentProvider>
Expand Down
Loading