Skip to main content
Glama

Office MCP Server

by walkingzzzy
MainInterface.tsx13.1 kB
import React, { useState, useEffect, useRef } from 'react'; import { ChatPanel } from './ChatPanel'; import { DiffPreviewPanel } from './DiffPreviewPanel'; import { VersionHistory } from './VersionHistory'; import { ChangesList } from './ChangesList'; import { Change, ChatMessage } from '../types'; import { ChangeManager } from '../services/ChangeManager'; import { HighlightManager } from '../services/HighlightManager'; import { AIService } from '../services/AIService'; import { WordAdapter } from '../services/WordAdapter'; import { ExcelAdapter } from '../services/ExcelAdapter'; import { PowerPointAdapter } from '../services/PowerPointAdapter'; import './MainInterface.css'; interface MainInterfaceProps { changeManager: ChangeManager; highlightManager: HighlightManager; } export const MainInterface: React.FC<MainInterfaceProps> = ({ changeManager, highlightManager }) => { const [changes, setChanges] = useState<Change[]>([]); const [selectedChangeId, setSelectedChangeId] = useState<string>(); const [chatMessages, setChatMessages] = useState<ChatMessage[]>([]); const [versions, setVersions] = useState<any[]>([]); const [activeTab, setActiveTab] = useState<'chat' | 'diff' | 'history'>('chat'); const [isLoading, setIsLoading] = useState(false); const [connectionStatus, setConnectionStatus] = useState<'disconnected' | 'connecting' | 'connected'>('disconnected'); // AI服务实例 const aiServiceRef = useRef<AIService | null>(null); const documentAdapterRef = useRef<WordAdapter | ExcelAdapter | PowerPointAdapter | null>(null); const [currentDocumentType, setCurrentDocumentType] = useState<'word' | 'excel' | 'powerpoint'>('word'); const [currentFilename, setCurrentFilename] = useState<string>('document.docx'); useEffect(() => { // 监听修改变化 const handleChangesUpdate = (updatedChanges: Change[]) => { setChanges(updatedChanges); }; changeManager.on('changes:updated', handleChangesUpdate); // 初始化数据 setChanges(changeManager.getAllChanges()); // 初始化AI服务和文档适配器 const initializeServices = async () => { try { setConnectionStatus('connecting'); // 创建AI服务实例 aiServiceRef.current = new AIService({ baseURL: 'http://localhost:3000', model: 'openai', enableWebSocket: true }); // 检测当前文档类型并创建适配器 if (Office.context.host === Office.HostType.Word) { documentAdapterRef.current = new WordAdapter(); setCurrentDocumentType('word'); setCurrentFilename('document.docx'); } else if (Office.context.host === Office.HostType.Excel) { documentAdapterRef.current = new ExcelAdapter(); setCurrentDocumentType('excel'); setCurrentFilename('workbook.xlsx'); } else if (Office.context.host === Office.HostType.PowerPoint) { documentAdapterRef.current = new PowerPointAdapter(); setCurrentDocumentType('powerpoint'); setCurrentFilename('presentation.pptx'); } // 创建对话 await aiServiceRef.current.createConversation( currentDocumentType, currentFilename ); // 设置WebSocket事件监听 aiServiceRef.current.onProgress((progress) => { console.log('AI进度更新:', progress); // 可以在UI上显示进度 }); aiServiceRef.current.onError((error) => { console.error('AI服务错误:', error); const errorMessage: ChatMessage = { id: `error-${Date.now()}`, type: 'error', content: `错误: ${error.message}`, timestamp: Date.now() }; setChatMessages(prev => [...prev, errorMessage]); }); setConnectionStatus('connected'); // 添加欢迎消息 const welcomeMessage: ChatMessage = { id: 'welcome', type: 'assistant', content: `您好!我是Office AI助手,已准备好帮助您编辑${currentDocumentType === 'word' ? 'Word文档' : currentDocumentType === 'excel' ? 'Excel工作簿' : 'PowerPoint演示文稿'}。请告诉我您需要什么帮助。`, timestamp: Date.now() }; setChatMessages([welcomeMessage]); } catch (error) { console.error('初始化服务失败:', error); setConnectionStatus('disconnected'); const errorMessage: ChatMessage = { id: 'init-error', type: 'error', content: '无法连接到AI服务,请检查Bridge Server是否正常运行。', timestamp: Date.now() }; setChatMessages([errorMessage]); } }; initializeServices(); return () => { changeManager.off('changes:updated', handleChangesUpdate); // 清理AI服务连接 if (aiServiceRef.current) { aiServiceRef.current.disconnect(); } }; }, [changeManager]); const handleSendMessage = async (content: string) => { if (!aiServiceRef.current || connectionStatus !== 'connected') { const errorMessage: ChatMessage = { id: `error-${Date.now()}`, type: 'error', content: '未连接到AI服务,请刷新页面重试。', timestamp: Date.now() }; setChatMessages(prev => [...prev, errorMessage]); return; } setIsLoading(true); // 添加用户消息 const userMessage: ChatMessage = { id: `msg-${Date.now()}`, type: 'user', content, timestamp: Date.now() }; setChatMessages(prev => [...prev, userMessage]); try { // 读取当前文档数据 let documentData: ArrayBuffer | undefined; if (documentAdapterRef.current) { try { const buffer = await documentAdapterRef.current.getDocumentData(); documentData = buffer.buffer as ArrayBuffer; } catch (error) { console.warn('读取文档数据失败,将不发送文档内容:', error); } } // 使用流式响应 let aiContent = ''; const aiMessageId = `msg-${Date.now()}-ai`; // 先添加一个占位的AI消息 const aiMessage: ChatMessage = { id: aiMessageId, type: 'assistant', content: '', timestamp: Date.now() }; setChatMessages(prev => [...prev, aiMessage]); // 流式接收AI响应 await aiServiceRef.current.sendMessageStream( content, documentData, // onChunk: 接收到消息片段 (chunk: string) => { aiContent += chunk; setChatMessages(prev => prev.map(msg => msg.id === aiMessageId ? { ...msg, content: aiContent } : msg ) ); }, // onComplete: 消息完成 async (response) => { console.log('AI响应完成:', response); // 如果有文档更新,应用到Office if (response.documentData && documentAdapterRef.current) { try { // Base64解码为ArrayBuffer const binaryString = atob(response.documentData); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } const updatedBuffer = Buffer.from(bytes); await documentAdapterRef.current.updateDocument(updatedBuffer); console.log('文档已更新'); } catch (error) { console.error('更新文档失败:', error); const errorMsg: ChatMessage = { id: `error-${Date.now()}`, type: 'error', content: `文档更新失败: ${(error as Error).message}`, timestamp: Date.now() }; setChatMessages(prev => [...prev, errorMsg]); } } // 如果有工具调用信息,记录日志 if (response.toolCalls && response.toolCalls.length > 0) { console.log('AI执行了以下工具:', response.toolCalls); // 可以在UI上显示工具调用详情 } setIsLoading(false); }, // onError: 错误处理 (error: Error) => { console.error('发送消息失败:', error); const errorMessage: ChatMessage = { id: `error-${Date.now()}`, type: 'error', content: `发送失败: ${error.message}`, timestamp: Date.now() }; setChatMessages(prev => [...prev, errorMessage]); setIsLoading(false); } ); } catch (error) { console.error('发送消息失败:', error); const errorMessage: ChatMessage = { id: `error-${Date.now()}`, type: 'error', content: `发送失败: ${(error as Error).message}`, timestamp: Date.now() }; setChatMessages(prev => [...prev, errorMessage]); setIsLoading(false); } }; const handleAcceptChange = async (changeId: string) => { try { await changeManager.acceptChange(changeId); } catch (error) { console.error('接受修改失败:', error); } }; const handleRejectChange = async (changeId: string) => { try { await changeManager.rejectChange(changeId); } catch (error) { console.error('拒绝修改失败:', error); } }; const handleAcceptAll = async () => { try { await changeManager.acceptAll(); } catch (error) { console.error('批量接受失败:', error); } }; const handleRejectAll = async () => { try { await changeManager.rejectAll(); } catch (error) { console.error('批量拒绝失败:', error); } }; const handleVersionRestore = async (versionId: string) => { try { // 这里应该调用版本管理器 console.log('恢复版本:', versionId); } catch (error) { console.error('恢复版本失败:', error); } }; const handleVersionCompare = (versionId1: string, versionId2: string) => { console.log('对比版本:', versionId1, versionId2); }; const pendingChanges = changes.filter(c => c.status === 'pending'); const stats = changeManager.getStats(); return ( <div className="main-interface"> <div className="interface-header"> <h2>Office AI 助手</h2> <div className="stats-bar"> <span className="stat-item"> 待处理: <strong>{stats.pending}</strong> </span> <span className="stat-item"> 已接受: <strong>{stats.accepted}</strong> </span> <span className="stat-item"> 已拒绝: <strong>{stats.rejected}</strong> </span> </div> </div> <div className="interface-content"> <div className="left-panel"> <div className="tab-navigation"> <button className={activeTab === 'chat' ? 'active' : ''} onClick={() => setActiveTab('chat')} > 💬 对话 </button> <button className={activeTab === 'diff' ? 'active' : ''} onClick={() => setActiveTab('diff')} > 📋 预览 </button> <button className={activeTab === 'history' ? 'active' : ''} onClick={() => setActiveTab('history')} > 📚 历史 </button> </div> <div className="tab-content"> {activeTab === 'chat' && ( <ChatPanel messages={chatMessages} onSendMessage={handleSendMessage} isLoading={isLoading} /> )} {activeTab === 'diff' && ( <DiffPreviewPanel changes={pendingChanges} selectedChangeId={selectedChangeId} onChangeSelect={setSelectedChangeId} /> )} {activeTab === 'history' && ( <VersionHistory versions={versions} onRestore={handleVersionRestore} onCompare={handleVersionCompare} /> )} </div> </div> <div className="right-panel"> <ChangesList changes={pendingChanges} onAccept={handleAcceptChange} onReject={handleRejectChange} onAcceptAll={handleAcceptAll} onRejectAll={handleRejectAll} onView={setSelectedChangeId} /> </div> </div> {isLoading && ( <div className="loading-overlay"> <div className="loading-spinner"> <div className="spinner"></div> <p>AI正在处理您的请求...</p> </div> </div> )} </div> ); };

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/walkingzzzy/office-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server