🏗️ 步骤1:基础Monaco编辑器搭建
1.1 项目初始化
bash
# 创建项目
npm create vite@latest ai-editor -- --template react-ts
cd ai-editor
# 安装核心依赖
npm install monaco-editor @monaco-editor/react
npm install @uiw/react-monacoeditor # 更好的React包装器
npm install zustand @tanstack/react-query # 状态管理和API
npm install tailwindcss @headlessui/react # UI框架
npm install lucide-react # 图标库
# 配置Tailwind
npx tailwindcss init -p
1.2 编辑器核心组件
tsx
// src/components/Editor/BaseEditor.tsx
import MonacoEditor from '@uiw/react-monacoeditor';
import { useRef, useEffect, useState } from 'react';
import { editor } from 'monaco-editor';
interface BaseEditorProps {
language?: string;
theme?: string;
value?: string;
onChange?: (value: string) => void;
}
export default function BaseEditor({
language = 'typescript',
theme = 'vs-dark',
value = '',
onChange
}: BaseEditorProps) {
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const [isReady, setIsReady] = useState(false);
// 初始化编辑器选项
const editorOptions: editor.IStandaloneEditorConstructionOptions = {
language,
theme,
fontSize: 14,
lineHeight: 20,
minimap: { enabled: true },
scrollBeyondLastLine: false,
wordWrap: 'on',
wrappingIndent: 'indent',
suggestOnTriggerCharacters: true,
acceptSuggestionOnEnter: 'on',
snippetSuggestions: 'inline',
parameterHints: { enabled: true },
quickSuggestions: {
other: true,
comments: true,
strings: true
},
automaticLayout: true,
formatOnPaste: true,
formatOnType: true,
tabSize: 2,
insertSpaces: true,
autoIndent: 'full',
bracketPairColorization: { enabled: true },
guides: {
bracketPairs: true,
bracketPairsHorizontal: true
},
renderWhitespace: 'selection',
renderControlCharacters: true,
mouseWheelZoom: true,
smoothScrolling: true,
contextmenu: true,
links: true,
copyWithSyntaxHighlighting: true,
dragAndDrop: true,
showDeprecated: true,
inlineSuggest: { enabled: true },
padding: { top: 10, bottom: 10 }
};
// 语言配置
useEffect(() => {
if (isReady && editorRef.current) {
// 配置TypeScript
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.ESNext,
allowNonTsExtensions: true,
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
module: monaco.languages.typescript.ModuleKind.ESNext,
strict: true,
esModuleInterop: true,
skipLibCheck: true,
forceConsistentCasingInFileNames: true,
resolveJsonModule: true,
allowSyntheticDefaultImports: true,
experimentalDecorators: true,
emitDecoratorMetadata: true,
jsx: monaco.languages.typescript.JsxEmit.React,
reactNamespace: 'React',
allowJs: true,
checkJs: true,
declaration: true,
declarationMap: true,
noImplicitAny: false,
noImplicitReturns: false,
noUnusedLocals: false,
noUnusedParameters: false,
preserveConstEnums: true,
removeComments: false,
sourceMap: true,
lib: ['ESNext', 'DOM', 'DOM.Iterable'],
});
// 设置TypeScript类型定义
monaco.languages.typescript.typescriptDefaults.addExtraLib(
`declare module "*.module.css" {
const classes: { [key: string]: string };
export default classes;
}`,
'file:///node_modules/@types/custom/index.d.ts'
);
}
}, [isReady]);
// 编辑器挂载回调
const handleEditorMount = (editor: editor.IStandaloneCodeEditor) => {
editorRef.current = editor;
setIsReady(true);
// 添加自定义命令
editor.addCommand(
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS,
() => {
console.log('文件已保存');
// 这里可以添加保存逻辑
},
'!suggestWidgetVisible'
);
// 添加右键菜单
editor.onContextMenu(() => {
// 自定义右键菜单
});
};
return (
<div className="editor-container h-full w-full relative">
<div className="toolbar bg-gray-900 border-b border-gray-700 p-2 flex items-center">
<select
className="bg-gray-800 text-white px-3 py-1 rounded mr-4"
onChange={(e) => {/* 切换语言 */}}
>
<option value="typescript">TypeScript</option>
<option value="javascript">JavaScript</option>
<option value="python">Python</option>
<option value="java">Java</option>
<option value="go">Go</option>
</select>
<button className="px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 mr-2">
运行
</button>
<button className="px-3 py-1 bg-green-600 text-white rounded hover:bg-green-700">
格式化
</button>
</div>
<MonacoEditor
height="calc(100vh - 48px)"
language={language}
value={value}
options={editorOptions}
onChange={onChange}
onMount={handleEditorMount}
/>
</div>
);
}
1.3 主题和语言扩展
typescript
// src/utils/editor-themes.ts
import { editor } from 'monaco-editor';
export const registerCustomThemes = () => {
// 深色主题
monaco.editor.defineTheme('ai-dark', {
base: 'vs-dark',
inherit: true,
rules: [
{ token: 'comment', foreground: '6A9955' },
{ token: 'keyword', foreground: '569CD6' },
{ token: 'string', foreground: 'CE9178' },
{ token: 'number', foreground: 'B5CEA8' },
{ token: 'type', foreground: '4EC9B0' },
{ token: 'function', foreground: 'DCDCAA' },
{ token: 'variable', foreground: '9CDCFE' },
],
colors: {
'editor.background': '#1E1E1E',
'editor.foreground': '#D4D4D4',
'editorLineNumber.foreground': '#858585',
'editor.selectionBackground': '#264F78',
'editor.inactiveSelectionBackground': '#3A3D41',
'editorIndentGuide.background': '#404040',
'editorIndentGuide.activeBackground': '#707070',
}
});
// 浅色主题
monaco.editor.defineTheme('ai-light', {
base: 'vs',
inherit: true,
rules: [
{ token: 'comment', foreground: '008000' },
{ token: 'keyword', foreground: '0000FF' },
{ token: 'string', foreground: 'A31515' },
{ token: 'number', foreground: '098658' },
{ token: 'type', foreground: '267F99' },
{ token: 'function', foreground: '795E26' },
],
colors: {
'editor.background': '#FFFFFF',
'editorLineNumber.foreground': '#2B91AF',
'editor.selectionBackground': '#ADD6FF',
}
});
};
// 扩展语言支持
export const registerLanguageFeatures = async () => {
// 动态加载语言支持
const languages = [
'typescript',
'javascript',
'python',
'java',
'go',
'rust',
'cpp',
'csharp',
'php',
'html',
'css',
'json',
'yaml',
'markdown',
'sql'
];
for (const lang of languages) {
try {
await import(`monaco-editor/esm/vs/basic-languages/${lang}/${lang}.contribution.js`);
} catch (error) {
console.log(`Language ${lang} not available as basic language`);
}
}
};
1.4 状态管理
typescript
// src/store/editorStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface EditorState {
// 编辑器状态
currentFile: string;
content: string;
language: string;
theme: string;
// 编辑器配置
fontSize: number;
wordWrap: boolean;
minimap: boolean;
lineNumbers: boolean;
// 操作历史
history: Array<{ content: string; timestamp: number }>;
historyIndex: number;
// Actions
updateContent: (content: string) => void;
changeLanguage: (language: string) => void;
changeTheme: (theme: string) => void;
saveToHistory: () => void;
undo: () => void;
redo: () => void;
}
export const useEditorStore = create<EditorState>()(
persist(
(set, get) => ({
// 初始状态
currentFile: 'untitled.ts',
content: '// Start coding here...',
language: 'typescript',
theme: 'vs-dark',
fontSize: 14,
wordWrap: true,
minimap: true,
lineNumbers: true,
history: [{ content: '// Start coding here...', timestamp: Date.now() }],
historyIndex: 0,
// 更新内容
updateContent: (content: string) => {
const { history, historyIndex } = get();
const newHistory = history.slice(0, historyIndex + 1);
newHistory.push({ content, timestamp: Date.now() });
set({
content,
history: newHistory,
historyIndex: newHistory.length - 1,
});
},
// 切换语言
changeLanguage: (language: string) => set({ language }),
// 切换主题
changeTheme: (theme: string) => set({ theme }),
// 保存历史
saveToHistory: () => {
const { content, history, historyIndex } = get();
const newHistory = history.slice(0, historyIndex + 1);
newHistory.push({ content, timestamp: Date.now() });
set({
history: newHistory,
historyIndex: newHistory.length - 1,
});
},
// 撤销
undo: () => {
const { historyIndex, history } = get();
if (historyIndex > 0) {
const prevContent = history[historyIndex - 1].content;
set({
content: prevContent,
historyIndex: historyIndex - 1,
});
}
},
// 重做
redo: () => {
const { historyIndex, history } = get();
if (historyIndex < history.length - 1) {
const nextContent = history[historyIndex + 1].content;
set({
content: nextContent,
historyIndex: historyIndex + 1,
});
}
},
}),
{
name: 'editor-storage',
partialize: (state) => ({
fontSize: state.fontSize,
theme: state.theme,
language: state.language,
wordWrap: state.wordWrap,
minimap: state.minimap,
lineNumbers: state.lineNumbers,
}),
}
)
);
🧠 步骤2:集成OpenAI API补全
2.1 OpenAI服务封装
typescript
// src/services/openaiService.ts
import OpenAI from 'openai';
export interface CodeCompletionOptions {
model?: string;
temperature?: number;
maxTokens?: number;
context?: string;
language?: string;
}
export class OpenAIService {
private openai: OpenAI;
private apiKey: string;
constructor(apiKey: string) {
this.apiKey = apiKey;
this.openai = new OpenAI({
apiKey,
dangerouslyAllowBrowser: true // 注意:生产环境应该通过后端代理
});
}
// 代码补全
async getCodeCompletion(
prompt: string,
options: CodeCompletionOptions = {}
): Promise<string[]> {
const {
model = 'gpt-4',
temperature = 0.3,
maxTokens = 100,
context = '',
language = 'typescript'
} = options;
const systemPrompt = this.buildSystemPrompt(language, context);
try {
const response = await this.openai.chat.completions.create({
model,
messages: [
{
role: 'system',
content: systemPrompt
},
{
role: 'user',
content: prompt
}
],
temperature,
max_tokens: maxTokens,
n: 3, // 返回3个建议
stop: ['\n\n', '//', '/*', '"""', "'''"]
});
return response.choices
.map(choice => choice.message.content?.trim() || '')
.filter(content => content.length > 0);
} catch (error) {
console.error('OpenAI API Error:', error);
throw error;
}
}
// 构建系统提示词
private buildSystemPrompt(language: string, context: string): string {
return `You are an expert ${language} programmer. Complete the code based on the context.
${context ? `Context: ${context}\n` : ''}
Rules:
1. Return only the completion code, no explanations
2. Keep the same coding style
3. Include proper indentation
4. If completing a line, don't repeat the beginning
5. Consider best practices and performance`;
}
// 代码解释
async explainCode(code: string, language: string): Promise<string> {
const response = await this.openai.chat.completions.create({
model: 'gpt-4',
messages: [
{
role: 'system',
content: `Explain the ${language} code in simple terms. Focus on:
1. What the code does
2. Key functions/methods used
3. Potential issues
4. Suggested improvements`
},
{
role: 'user',
content: code
}
],
temperature: 0.2
});
return response.choices[0].message.content || '';
}
// 代码重构
async refactorCode(code: string, language: string, instruction?: string): Promise<string> {
const response = await this.openai.chat.completions.create({
model: 'gpt-4',
messages: [
{
role: 'system',
content: `Refactor the ${language} code to be more efficient, readable, and maintainable.
${instruction || 'Focus on best practices and performance.'}`
},
{
role: 'user',
content: code
}
],
temperature: 0.3
});
return response.choices[0].message.content || '';
}
// 调试帮助
async debugCode(error: string, code: string): Promise<string> {
const response = await this.openai.chat.completions.create({
model: 'gpt-4',
messages: [
{
role: 'system',
content: 'Analyze the error and provide a solution. Include:
1. Error explanation
2. Root cause
3. Step-by-step fix
4. Preventive measures'
},
{
role: 'user',
content: `Error: ${error}\n\nCode:\n${code}`
}
],
temperature: 0.2
});
return response.choices[0].message.content || '';
}
}
// 单例模式
let openaiInstance: OpenAIService | null = null;
export const getOpenAIService = (apiKey?: string): OpenAIService => {
if (!openaiInstance) {
const key = apiKey || localStorage.getItem('openai-api-key') || '';
if (!key) throw new Error('OpenAI API key not found');
openaiInstance = new OpenAIService(key);
}
return openaiInstance;
};
2.2 Monaco AI补全提供器
typescript
// src/components/Editor/AICompletionProvider.ts
import { editor, languages } from 'monaco-editor';
import { getOpenAIService } from '../../services/openaiService';
export class AICompletionProvider implements languages.CompletionItemProvider {
triggerCharacters = ['.', ' ', '(', '[', '{', '\n', '\t'];
async provideCompletionItems(
model: editor.ITextModel,
position: editor.Position
): Promise<languages.CompletionList> {
try {
// 获取光标前的文本
const textBeforeCursor = model.getValueInRange({
startLineNumber: 1,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column
});
// 获取语言
const language = model.getLanguageId();
// 获取上下文(当前函数或类)
const context = this.extractContext(model, position);
// 调用OpenAI获取建议
const aiService = getOpenAIService();
const suggestions = await aiService.getCodeCompletion(
textBeforeCursor,
{
language,
context
}
);
// 转换为Monaco的补全项
const items: languages.CompletionItem[] = suggestions.map((suggestion, index) => ({
label: suggestion.split('\n')[0], // 只显示第一行
kind: languages.CompletionItemKind.Snippet,
insertText: suggestion,
detail: 'AI Suggestion',
documentation: {
value: `**AI Generated Code**\n\n\`\`\`${language}\n${suggestion}\n\`\`\``,
isTrusted: true
},
sortText: `ai_${index.toString().padStart(3, '0')}`, // 让AI建议排在前面
range: {
startLineNumber: position.lineNumber,
startColumn: position.column,
endLineNumber: position.lineNumber,
endColumn: position.column
},
command: {
title: 'Accept AI Suggestion',
id: 'acceptAICompletion',
arguments: [suggestion]
}
}));
return {
suggestions: items,
incomplete: false
};
} catch (error) {
console.error('AI completion error:', error);
return { suggestions: [], incomplete: false };
}
}
// 提取代码上下文
private extractContext(model: editor.ITextModel, position: editor.Position): string {
const currentLine = position.lineNumber;
const lineCount = model.getLineCount();
// 向前查找50行,向后查找20行
const startLine = Math.max(1, currentLine - 50);
const endLine = Math.min(lineCount, currentLine + 20);
let context = '';
for (let i = startLine; i <= endLine; i++) {
context += model.getLineContent(i) + '\n';
}
return context;
}
// 获取光标所在位置的符号
private getWordAtPosition(model: editor.ITextModel, position: editor.Position): string {
const wordInfo = model.getWordAtPosition(position);
return wordInfo ? wordInfo.word : '';
}
}
// 内联AI建议
export class InlineAIProvider {
private debounceTimer: NodeJS.Timeout | null = null;
async getInlineSuggestions(
model: editor.ITextModel,
position: editor.Position
): Promise<string | null> {
try {
// 清除之前的计时器
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
return new Promise((resolve) => {
this.debounceTimer = setTimeout(async () => {
const lineContent = model.getLineContent(position.lineNumber);
const textBeforeCursor = lineContent.substring(0, position.column - 1);
if (textBeforeCursor.trim().length < 3) {
resolve(null);
return;
}
const aiService = getOpenAIService();
const suggestions = await aiService.getCodeCompletion(
textBeforeCursor,
{
maxTokens: 30,
language: model.getLanguageId()
}
);
if (suggestions.length > 0) {
// 找到最合适的建议
const bestSuggestion = this.findBestSuggestion(
textBeforeCursor,
suggestions[0]
);
resolve(bestSuggestion);
} else {
resolve(null);
}
}, 500); // 500ms防抖
});
} catch (error) {
console.error('Inline suggestions error:', error);
return null;
}
}
private findBestSuggestion(prefix: string, suggestion: string): string {
// 清理建议,移除重复的prefix部分
let cleanSuggestion = suggestion;
// 如果建议以prefix开头,移除prefix
if (suggestion.startsWith(prefix)) {
cleanSuggestion = suggestion.substring(prefix.length);
}
// 确保建议不是空的
if (cleanSuggestion.trim() === '') {
return '';
}
return cleanSuggestion;
}
}
2.3 集成到编辑器
tsx
// src/components/Editor/AIEnhancedEditor.tsx
import { useEffect, useRef } from 'react';
import BaseEditor from './BaseEditor';
import { AICompletionProvider, InlineAIProvider } from './AICompletionProvider';
import { editor } from 'monaco-editor';
interface AIEnhancedEditorProps {
onAISuggestion?: (suggestion: string) => void;
onAIExplain?: (explanation: string) => void;
}
export default function AIEnhancedEditor({
onAISuggestion,
onAIExplain
}: AIEnhancedEditorProps) {
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const inlineAIProvider = useRef(new InlineAIProvider());
const completionProvider = useRef(new AICompletionProvider());
// 初始化AI功能
useEffect(() => {
if (editorRef.current) {
const editorInstance = editorRef.current;
const model = editorInstance.getModel();
if (model) {
// 注册AI补全提供器
const disposable = monaco.languages.registerCompletionItemProvider(
model.getLanguageId(),
completionProvider.current
);
// 监听输入事件,触发内联建议
editorInstance.onDidChangeModelContent(async (e) => {
const position = editorInstance.getPosition();
if (!position) return;
const suggestion = await inlineAIProvider.current.getInlineSuggestions(
model,
position
);
if (suggestion) {
// 显示内联建议
this.showInlineSuggestion(editorInstance, suggestion);
}
});
// 注册AI相关命令
editorInstance.addAction({
id: 'explain-code',
label: 'Explain This Code',
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyE],
run: async () => {
const selection = editorInstance.getSelection();
const code = selection
? model.getValueInRange(selection)
: model.getValue();
// 调用解释功能
// ... 这里需要实现
}
});
return () => {
disposable.dispose();
};
}
}
}, []);
const showInlineSuggestion = (
editorInstance: editor.IStandaloneCodeEditor,
suggestion: string
) => {
// 创建装饰器来显示内联建议
const decorations = editorInstance.createDecorationsCollection([
{
range: new monaco.Range(1, 1, 1, 1),
options: {
after: {
content: suggestion,
inlineClassName: 'inline-ai-suggestion',
cursorStops: monaco.CursorStop.Right
},
hoverMessage: { value: 'AI Suggestion' },
stickiness: monaco.editor.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges
}
}
]);
};
const handleEditorMount = (editor: editor.IStandaloneCodeEditor) => {
editorRef.current = editor;
// 设置API密钥
const apiKey = localStorage.getItem('openai-api-key');
if (!apiKey) {
this.showAPIKeyPrompt();
}
};
const showAPIKeyPrompt = () => {
// 显示API密钥输入模态框
const key = prompt('Enter your OpenAI API key:');
if (key) {
localStorage.setItem('openai-api-key', key);
}
};
return (
<div className="ai-enhanced-editor">
<BaseEditor
onMount={handleEditorMount}
// ... 其他props
/>
{/* AI工具栏 */}
<div className="ai-toolbar absolute top-2 right-2 flex space-x-2">
<button
onClick={() => this.explainSelection()}
className="px-3 py-1 bg-purple-600 text-white rounded hover:bg-purple-700"
title="Explain selected code"
>
🤖 Explain
</button>
<button
onClick={() => this.refactorSelection()}
className="px-3 py-1 bg-green-600 text-white rounded hover:bg-green-700"
title="Refactor selected code"
>
🔄 Refactor
</button>
<button
onClick={() => this.generateTests()}
className="px-3 py-1 bg-yellow-600 text-white rounded hover:bg-yellow-700"
title="Generate tests"
>
🧪 Tests
</button>
</div>
</div>
);
}
2.4 配置面板
tsx
// src/components/Settings/AISettings.tsx
import { useState } from 'react';
export default function AISettings() {
const [apiKey, setApiKey] = useState(localStorage.getItem('openai-api-key') || '');
const [model, setModel] = useState('gpt-4');
const [temperature, setTemperature] = useState(0.3);
const [maxTokens, setMaxTokens] = useState(100);
const [autoComplete, setAutoComplete] = useState(true);
const [inlineSuggestions, setInlineSuggestions] = useState(true);
const saveSettings = () => {
localStorage.setItem('openai-api-key', apiKey);
localStorage.setItem('ai-model', model);
localStorage.setItem('ai-temperature', temperature.toString());
localStorage.setItem('ai-max-tokens', maxTokens.toString());
localStorage.setItem('ai-auto-complete', autoComplete.toString());
localStorage.setItem('ai-inline-suggestions', inlineSuggestions.toString());
// 通知编辑器重新初始化
window.dispatchEvent(new CustomEvent('ai-settings-changed'));
};
return (
<div className="ai-settings p-6 bg-gray-900 text-white rounded-lg">
<h2 className="text-xl font-bold mb-4">AI Settings</h2>
<div className="space-y-4">
<div>
<label className="block mb-2">OpenAI API Key</label>
<input
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
className="w-full p-2 bg-gray-800 rounded"
placeholder="sk-..."
/>
<p className="text-sm text-gray-400 mt-1">
Your key is stored locally and never sent to our servers
</p>
</div>
<div>
<label className="block mb-2">Model</label>
<select
value={model}
onChange={(e) => setModel(e.target.value)}
className="w-full p-2 bg-gray-800 rounded"
>
<option value="gpt-4">GPT-4</option>
<option value="gpt-3.5-turbo">GPT-3.5 Turbo</option>
<option value="text-davinci-003">Text Davinci</option>
</select>
</div>
<div>
<label className="block mb-2">
Temperature: {temperature.toFixed(1)}
</label>
<input
type="range"
min="0"
max="1"
step="0.1"
value={temperature}
onChange={(e) => setTemperature(parseFloat(e.target.value))}
className="w-full"
/>
<p className="text-sm text-gray-400">
Lower = more focused, Higher = more creative
</p>
</div>
<div>
<label className="block mb-2">Max Tokens per completion</label>
<input
type="number"
value={maxTokens}
onChange={(e) => setMaxTokens(parseInt(e.target.value))}
className="w-full p-2 bg-gray-800 rounded"
min="10"
max="1000"
/>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="autoComplete"
checked={autoComplete}
onChange={(e) => setAutoComplete(e.target.checked)}
className="w-4 h-4"
/>
<label htmlFor="autoComplete">Enable AI Auto-complete</label>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="inlineSuggestions"
checked={inlineSuggestions}
onChange={(e) => setInlineSuggestions(e.target.checked)}
className="w-4 h-4"
/>
<label htmlFor="inlineSuggestions">Show inline suggestions</label>
</div>
<button
onClick={saveSettings}
className="w-full py-2 bg-blue-600 hover:bg-blue-700 rounded"
>
Save Settings
</button>
</div>
</div>
);
}
💬 步骤3:添加聊天式AI助手
3.1 AI聊天组件
tsx
// src/components/Chat/AIAssistant.tsx
import { useState, useRef, useEffect } from 'react';
import { Send, Bot, User, Copy, ThumbsUp, ThumbsDown } from 'lucide-react';
import { getOpenAIService } from '../../services/openaiService';
interface Message {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
timestamp: Date;
isError?: boolean;
feedback?: 'positive' | 'negative';
}
interface ChatContext {
code?: string;
language?: string;
filePath?: string;
selection?: string;
}
export default function AIAssistant() {
const [messages, setMessages] = useState<Message[]>([
{
id: '1',
role: 'assistant',
content: 'Hello! I\'m your AI coding assistant. I can help you write, explain, refactor, and debug code. What would you like to work on?',
timestamp: new Date()
}
]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [context, setContext] = useState<ChatContext>({});
const messagesEndRef = useRef<HTMLDivElement>(null);
// 自动滚动到底部
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
// 发送消息
const sendMessage = async () => {
if (!input.trim() || isLoading) return;
const userMessage: Message = {
id: Date.now().toString(),
role: 'user',
content: input,
timestamp: new Date()
};
setMessages(prev => [...prev, userMessage]);
setInput('');
setIsLoading(true);
try {
const aiService = getOpenAIService();
// 构建带上下文的对话历史
const chatHistory = messages
.slice(-10) // 只发送最近10条消息
.map(msg => ({
role: msg.role,
content: msg.content
}));
// 如果有代码上下文,添加到系统提示中
let systemPrompt = 'You are an expert programming assistant. Help the user with coding tasks.';
if (context.code) {
systemPrompt += `\n\nCurrent code context:\n\`\`\`${context.language || 'text'}\n${context.code}\n\`\`\``;
}
const response = await aiService.openai.chat.completions.create({
model: 'gpt-4',
messages: [
{ role: 'system', content: systemPrompt },
...chatHistory,
{ role: 'user', content: input }
],
temperature: 0.7,
stream: true // 启用流式响应
});
// 处理流式响应
let assistantMessage = '';
const assistantMessageId = (Date.now() + 1).toString();
const assistantMessageObj: Message = {
id: assistantMessageId,
role: 'assistant',
content: '',
timestamp: new Date()
};
setMessages(prev => [...prev, assistantMessageObj]);
// 读取流式数据
const reader = response.toReadableStream().getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
setIsLoading(false);
return;
}
try {
const parsed = JSON.parse(data);
const delta = parsed.choices[0]?.delta?.content;
if (delta) {
assistantMessage += delta;
// 更新消息内容
setMessages(prev => prev.map(msg =>
msg.id === assistantMessageId
? { ...msg, content: assistantMessage }
: msg
));
}
} catch (e) {
console.error('Error parsing stream data:', e);
}
}
}
}
setIsLoading(false);
} catch (error) {
console.error('Chat error:', error);
const errorMessage: Message = {
id: Date.now().toString(),
role: 'assistant',
content: 'Sorry, I encountered an error. Please check your API key and try again.',
timestamp: new Date(),
isError: true
};
setMessages(prev => [...prev, errorMessage]);
setIsLoading(false);
}
};
// 复制消息内容
const copyMessage = (content: string) => {
navigator.clipboard.writeText(content);
};
// 提供反馈
const provideFeedback = (messageId: string, type: 'positive' | 'negative') => {
setMessages(prev => prev.map(msg =>
msg.id === messageId
? { ...msg, feedback: type }
: msg
));
};
// 预设动作
const presetActions = [
{ label: 'Explain this code', action: 'Explain the selected code' },
{ label: 'Refactor', action: 'Refactor this to be more efficient' },
{ label: 'Find bugs', action: 'Find potential bugs in this code' },
{ label: 'Write tests', action: 'Write unit tests for this code' },
{ label: 'Optimize', action: 'Optimize this code for performance' },
{ label: 'Add comments', action: 'Add documentation comments' }
];
return (
<div className="ai-assistant h-full flex flex-col bg-gray-900">
{/* 头部 */}
<div className="p-4 border-b border-gray-700">
<div className="flex items-center space-x-3">
<div className="p-2 bg-purple-600 rounded-lg">
<Bot size={24} />
</div>
<div>
<h2 className="text-lg font-semibold">AI Assistant</h2>
<p className="text-sm text-gray-400">Powered by GPT-4</p>
</div>
</div>
</div>
{/* 消息区域 */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map((message) => (
<div
key={message.id}
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-[80%] rounded-lg p-4 ${
message.role === 'user'
? 'bg-blue-600 text-white'
: message.isError
? 'bg-red-900 text-white'
: 'bg-gray-800 text-gray-100'
}`}
>
<div className="flex items-center mb-2">
{message.role === 'user' ? (
<User size={16} className="mr-2" />
) : (
<Bot size={16} className="mr-2" />
)}
<span className="text-sm font-medium">
{message.role === 'user' ? 'You' : 'AI Assistant'}
</span>
<span className="text-xs text-gray-400 ml-auto">
{message.timestamp.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
})}
</span>
</div>
<div className="prose prose-invert max-w-none">
{message.content.split('\n').map((line, i) => (
<p key={i} className="mb-2 last:mb-0">
{line}
</p>
))}
{/* 代码块检测和格式化 */}
{message.content.includes('```') && (
<div className="mt-2">
<pre className="bg-gray-900 p-3 rounded overflow-x-auto">
<code>
{message.content
.match(/```[\s\S]*?```/g)
?.map((codeBlock, idx) => (
<div key={idx}>{codeBlock.replace(/```[a-z]*\n?|\n?```/g, '')}</div>
))}
</code>
</pre>
</div>
)}
</div>
{/* 消息操作 */}
{message.role === 'assistant' && !message.isError && (
<div className="flex items-center space-x-2 mt-3">
<button
onClick={() => copyMessage(message.content)}
className="p-1 hover:bg-gray-700 rounded"
title="Copy"
>
<Copy size={16} />
</button>
{!message.feedback && (
<>
<button
onClick={() => provideFeedback(message.id, 'positive')}
className="p-1 hover:bg-gray-700 rounded"
title="Helpful"
>
<ThumbsUp size={16} />
</button>
<button
onClick={() => provideFeedback(message.id, 'negative')}
className="p-1 hover:bg-gray-700 rounded"
title="Not helpful"
>
<ThumbsDown size={16} />
</button>
</>
)}
{message.feedback && (
<span className="text-sm text-gray-400">
{message.feedback === 'positive' ? '👍 Thanks!' : '👎 Noted'}
</span>
)}
</div>
)}
</div>
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-gray-800 rounded-lg p-4">
<div className="flex items-center space-x-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
<span>AI is thinking...</span>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* 预设动作 */}
<div className="px-4 py-2 border-t border-gray-700">
<div className="flex flex-wrap gap-2 mb-2">
{presetActions.map((action, index) => (
<button
key={index}
onClick={() => {
setInput(action.action);
setTimeout(() => sendMessage(), 100);
}}
className="px-3 py-1 bg-gray-800 hover:bg-gray-700 rounded-full text-sm"
>
{action.label}
</button>
))}
</div>
</div>
{/* 输入区域 */}
<div className="p-4 border-t border-gray-700">
<div className="flex space-x-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && !e.shiftKey && sendMessage()}
placeholder="Ask about your code..."
className="flex-1 p-3 bg-gray-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600"
disabled={isLoading}
/>
<button
onClick={sendMessage}
disabled={isLoading || !input.trim()}
className="p-3 bg-purple-600 hover:bg-purple-700 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
>
<Send size={20} />
</button>
</div>
<p className="text-xs text-gray-400 mt-2">
Press Enter to send, Shift+Enter for new line
</p>
</div>
</div>
);
}
3.2 代码上下文集成
typescript
// src/utils/codeContext.ts
import { editor } from 'monaco-editor';
export interface CodeContext {
currentFile: string;
language: string;
fullCode: string;
selectedCode: string | null;
imports: string[];
exports: string[];
functions: Array<{
name: string;
parameters: string[];
returnType: string;
body: string;
}>;
classes: Array<{
name: string;
methods: string[];
properties: string[];
}>;
cursorPosition: {
line: number;
column: number;
};
}
export class CodeContextExtractor {
static extract(editorInstance: editor.IStandaloneCodeEditor): CodeContext {
const model = editorInstance.getModel();
if (!model) {
throw new Error('Editor model not found');
}
const position = editorInstance.getPosition();
const selection = editorInstance.getSelection();
const fullCode = model.getValue();
const language = model.getLanguageId();
const selectedCode = selection
? model.getValueInRange(selection)
: null;
// 解析AST(简化版)
const astInfo = this.parseAST(fullCode, language);
return {
currentFile: 'current.ts', // 可以从文件系统获取
language,
fullCode,
selectedCode,
imports: astInfo.imports,
exports: astInfo.exports,
functions: astInfo.functions,
classes: astInfo.classes,
cursorPosition: position
? { line: position.lineNumber, column: position.column }
: { line: 1, column: 1 }
};
}
private static parseAST(code: string, language: string): any {
// 简化的解析逻辑
// 实际中可以使用 Tree-sitter 或 Monaco 的 AST
const imports: string[] = [];
const exports: string[] = [];
const functions: any[] = [];
const classes: any[] = [];
const lines = code.split('\n');
lines.forEach((line, index) => {
const trimmed = line.trim();
// 检测导入
if (trimmed.startsWith('import ')) {
imports.push(trimmed);
}
// 检测导出
if (trimmed.startsWith('export ')) {
exports.push(trimmed);
}
// 检测函数(简化)
if (trimmed.match(/^(export\s+)?(async\s+)?function\s+\w+/)) {
const match = trimmed.match(/function\s+(\w+)/);
if (match) {
functions.push({
name: match[1],
parameters: this.extractParameters(trimmed),
returnType: this.extractReturnType(trimmed),
body: this.extractFunctionBody(lines, index)
});
}
}
// 检测类
if (trimmed.match(/^(export\s+)?class\s+\w+/)) {
const match = trimmed.match(/class\s+(\w+)/);
if (match) {
classes.push({
name: match[1],
methods: this.extractClassMethods(lines, index),
properties: this.extractClassProperties(lines, index)
});
}
}
});
return { imports, exports, functions, classes };
}
private static extractParameters(line: string): string[] {
const paramsMatch = line.match(/\(([^)]*)\)/);
if (!paramsMatch) return [];
return paramsMatch[1]
.split(',')
.map(p => p.trim())
.filter(p => p.length > 0);
}
private static extractReturnType(line: string): string {
const returnMatch = line.match(/\)\s*:\s*(\w+)/);
return returnMatch ? returnMatch[1] : 'void';
}
private static extractFunctionBody(lines: string[], startIndex: number): string {
let braceCount = 0;
let bodyLines: string[] = [];
let foundFirstBrace = false;
for (let i = startIndex; i < lines.length; i++) {
const line = lines[i];
if (!foundFirstBrace) {
if (line.includes('{')) {
foundFirstBrace = true;
braceCount = (line.match(/{/g) || []).length - (line.match(/}/g) || []).length;
}
continue;
}
bodyLines.push(line);
braceCount += (line.match(/{/g) || []).length;
braceCount -= (line.match(/}/g) || []).length;
if (braceCount <= 0) {
break;
}
}
return bodyLines.join('\n');
}
private static extractClassMethods(lines: string[], startIndex: number): string[] {
const methods: string[] = [];
let braceCount = 0;
let inClass = false;
for (let i = startIndex; i < lines.length; i++) {
const line = lines[i];
if (!inClass) {
if (line.includes('{')) {
inClass = true;
braceCount = 1;
}
continue;
}
// 检测方法
const methodMatch = line.match(/(public|private|protected)?\s*(static)?\s*(\w+)\s*\(/);
if (methodMatch && braceCount === 1) {
methods.push(methodMatch[3]);
}
braceCount += (line.match(/{/g) || []).length;
braceCount -= (line.match(/}/g) || []).length;
if (braceCount <= 0) {
break;
}
}
return methods;
}
private static extractClassProperties(lines: string[], startIndex: number): string[] {
const properties: string[] = [];
let braceCount = 0;
let inClass = false;
for (let i = startIndex; i < lines.length; i++) {
const line = lines[i];
if (!inClass) {
if (line.includes('{')) {
inClass = true;
braceCount = 1;
}
continue;
}
// 检测属性(简化)
const propMatch = line.match(/(public|private|protected)?\s*(\w+)\s*(=|:)/);
if (propMatch && braceCount === 1 && !line.includes('(')) {
properties.push(propMatch[2]);
}
braceCount += (line.match(/{/g) || []).length;
braceCount -= (line.match(/}/g) || []).length;
if (braceCount <= 0) {
break;
}
}
return properties;
}
}
3.3 集成到主界面
tsx
// src/App.tsx
import { useState } from 'react';
import AIEnhancedEditor from './components/Editor/AIEnhancedEditor';
import AIAssistant from './components/Chat/AIAssistant';
import AISettings from './components/Settings/AISettings';
import { Menu, Settings, MessageSquare, Code, Zap } from 'lucide-react';
export default function App() {
const [activeTab, setActiveTab] = useState<'editor' | 'chat' | 'settings'>('editor');
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
return (
<div className="h-screen bg-gray-950 text-white flex flex-col">
{/* 顶部导航栏 */}
<header className="bg-gray-900 border-b border-gray-800 p-4 flex items-center justify-between">
<div className="flex items-center space-x-4">
<button
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
className="p-2 hover:bg-gray-800 rounded"
>
<Menu size={20} />
</button>
<div className="flex items-center space-x-2">
<Code size={24} className="text-blue-400" />
<h1 className="text-xl font-bold">AI Code Editor</h1>
<span className="text-xs bg-blue-600 px-2 py-1 rounded">Beta</span>
</div>
</div>
<div className="flex items-center space-x-4">
<button className="flex items-center space-x-2 px-4 py-2 bg-gradient-to-r from-blue-600 to-purple-600 rounded-lg hover:opacity-90">
<Zap size={18} />
<span>Generate Code</span>
</button>
<div className="flex space-x-2">
<button
onClick={() => setActiveTab('editor')}
className={`p-2 rounded ${activeTab === 'editor' ? 'bg-gray-800' : 'hover:bg-gray-800'}`}
title="Editor"
>
<Code size={20} />
</button>
<button
onClick={() => setActiveTab('chat')}
className={`p-2 rounded ${activeTab === 'chat' ? 'bg-gray-800' : 'hover:bg-gray-800'}`}
title="AI Assistant"
>
<MessageSquare size={20} />
</button>
<button
onClick={() => setActiveTab('settings')}
className={`p-2 rounded ${activeTab === 'settings' ? 'bg-gray-800' : 'hover:bg-gray-800'}`}
title="Settings"
>
<Settings size={20} />
</button>
</div>
</div>
</header>
{/* 主内容区 */}
<div className="flex-1 flex overflow-hidden">
{/* 侧边栏 */}
{isSidebarOpen && (
<div className="w-64 bg-gray-900 border-r border-gray-800 flex flex-col">
<div className="p-4 border-b border-gray-800">
<h3 className="font-semibold">Project Files</h3>
<div className="mt-2 space-y-1">
{/* 文件树组件 */}
<div className="text-sm text-gray-400">Coming soon...</div>
</div>
</div>
<div className="p-4 border-b border-gray-800">
<h3 className="font-semibold mb-2">AI Tools</h3>
<div className="space-y-2">
<button className="w-full text-left px-3 py-2 hover:bg-gray-800 rounded">
Code Review
</button>
<button className="w-full text-left px-3 py-2 hover:bg-gray-800 rounded">
Generate Tests
</button>
<button className="w-full text-left px-3 py-2 hover:bg-gray-800 rounded">
Documentation
</button>
</div>
</div>
</div>
)}
{/* 主编辑区 */}
<div className="flex-1 flex overflow-hidden">
{/* 编辑器区域 */}
<div className={`flex-1 ${activeTab === 'editor' ? 'block' : 'hidden'}`}>
<AIEnhancedEditor />
</div>
{/* 聊天区域 */}
<div className={`flex-1 ${activeTab === 'chat' ? 'block' : 'hidden'}`}>
<AIAssistant />
</div>
{/* 设置区域 */}
<div className={`flex-1 p-6 overflow-y-auto ${activeTab === 'settings' ? 'block' : 'hidden'}`}>
<AISettings />
</div>
</div>
</div>
{/* 底部状态栏 */}
<footer className="bg-gray-900 border-t border-gray-800 px-4 py-2 flex items-center justify-between text-sm">
<div className="flex items-center space-x-4">
<span className="text-green-400">● Ready</span>
<span>TypeScript 5.3</span>
<span>UTF-8</span>
<span>Spaces: 2</span>
</div>
<div className="flex items-center space-x-4">
<span>AI: Online</span>
<span>Tokens used: 124</span>
<button className="text-blue-400 hover:text-blue-300">
Buy more credits
</button>
</div>
</footer>
</div>
);
}
🤖 步骤4:替换为本地模型降低成本
4.1 本地模型服务架构
yaml
# docker-compose.yml
version: '3.8'
services:
# Ollama服务 (运行本地模型)
ollama:
image: ollama/ollama:latest
container_name: ai-editor-ollama
ports:
- "11434:11434"
volumes:
- ./models:/root/.ollama
- ./ollama-config:/etc/ollama
environment:
- OLLAMA_KEEP_ALIVE=24h
- OLLAMA_HOST=0.0.0.0
restart: unless-stopped
command: serve
# 本地模型API代理
model-proxy:
build: ./model-proxy
container_name: ai-editor-proxy
ports:
- "3001:3001"
environment:
- OLLAMA_HOST=ollama:11434
- OPENAI_API_KEY=${OPENAI_API_KEY}
volumes:
- ./model-proxy:/app
depends_on:
- ollama
restart: unless-stopped
# 向量数据库 (用于代码检索增强)
qdrant:
image: qdrant/qdrant:latest
container_name: ai-editor-qdrant
ports:
- "6333:6333"
volumes:
- ./qdrant_storage:/qdrant/storage
restart: unless-stopped
volumes:
ollama_models:
qdrant_storage:
4.2 本地模型服务
typescript
// server/model-proxy/index.ts
import express from 'express';
import cors from 'cors';
import { Ollama } from 'ollama';
import OpenAI from 'openai';
import { HfInference } from '@huggingface/inference';
const app = express();
app.use(cors());
app.use(express.json());
// 初始化模型客户端
const ollama = new Ollama({ host: process.env.OLLAMA_HOST || 'http://localhost:11434' });
const hf = new HfInference(process.env.HUGGINGFACE_TOKEN);
// 可用模型配置
const MODEL_CONFIG = {
'codellama:7b': {
provider: 'ollama',
name: 'codellama:7b',
contextWindow: 4096,
languages: ['python', 'javascript', 'typescript', 'java', 'cpp', 'go']
},
'deepseek-coder:6.7b': {
provider: 'ollama',
name: 'deepseek-coder:6.7b',
contextWindow: 8192,
languages: ['all']
},
'starcoder2:7b': {
provider: 'ollama',
name: 'starcoder2:7b',
contextWindow: 16384,
languages: ['all']
},
'gpt-4': {
provider: 'openai',
name: 'gpt-4',
contextWindow: 8192,
languages: ['all'],
fallback: true
}
};
// 模型路由
app.post('/api/models/available', async (req, res) => {
try {
const ollamaModels = await ollama.list();
const availableModels = ollamaModels.models.map(model => ({
name: model.name,
size: model.size,
modified: model.modified_at
}));
res.json({
local: availableModels,
cloud: ['gpt-4', 'gpt-3.5-turbo']
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 代码补全端点
app.post('/api/complete', async (req, res) => {
const { prompt, model = 'deepseek-coder:6.7b', temperature = 0.3, maxTokens = 100 } = req.body;
try {
const config = MODEL_CONFIG[model];
if (!config) {
return res.status(400).json({ error: 'Model not found' });
}
let completion: string;
if (config.provider === 'ollama') {
const response = await ollama.generate({
model: config.name,
prompt: this.buildCodePrompt(prompt),
options: {
temperature,
num_predict: maxTokens,
top_k: 40,
top_p: 0.95,
stop: ['\n\n', '//', '/*', '```']
}
});
completion = response.response;
} else if (config.provider === 'openai') {
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const response = await openai.chat.completions.create({
model: config.name,
messages: [
{ role: 'system', content: 'You are a coding assistant. Provide only code.' },
{ role: 'user', content: prompt }
],
temperature,
max_tokens: maxTokens
});
completion = response.choices[0].message.content || '';
}
res.json({ completion });
} catch (error) {
console.error('Completion error:', error);
// 如果本地模型失败,尝试回退到GPT-4
if (model !== 'gpt-4' && MODEL_CONFIG['gpt-4']) {
try {
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const response = await openai.chat.completions.create({
model: 'gpt-4',
messages: [
{ role: 'system', content: 'You are a coding assistant. Provide only code.' },
{ role: 'user', content: prompt }
],
temperature: 0.3,
max_tokens: 100
});
return res.json({
completion: response.choices[0].message.content || '',
fallback: true
});
} catch (fallbackError) {
console.error('Fallback also failed:', fallbackError);
}
}
res.status(500).json({ error: 'Completion failed' });
}
});
// 聊天端点
app.post('/api/chat', async (req, res) => {
const { messages, model = 'deepseek-coder:6.7b', stream = false } = req.body;
try {
const config = MODEL_CONFIG[model];
if (!config) {
return res.status(400).json({ error: 'Model not found' });
}
if (config.provider === 'ollama') {
if (stream) {
// 流式响应
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const response = await ollama.chat({
model: config.name,
messages: messages.map((msg: any) => ({
role: msg.role,
content: msg.content
})),
stream: true
});
for await (const part of response) {
res.write(`data: ${JSON.stringify(part)}\n\n`);
}
res.write('data: [DONE]\n\n');
res.end();
} else {
// 普通响应
const response = await ollama.chat({
model: config.name,
messages: messages.map((msg: any) => ({
role: msg.role,
content: msg.content
}))
});
res.json({ message: response.message });
}
}
} catch (error) {
console.error('Chat error:', error);
res.status(500).json({ error: 'Chat failed' });
}
});
// 模型下载端点
app.post('/api/models/pull', async (req, res) => {
const { modelName } = req.body;
try {
const response = await ollama.pull({ model: modelName });
// 流式响应下载进度
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
for await (const progress of response) {
res.write(`data: ${JSON.stringify(progress)}\n\n`);
}
res.write('data: [DONE]\n\n');
res.end();
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 构建代码提示
private buildCodePrompt(prompt: string): string {
return `[INST] You are an expert programmer. Complete the code.
${prompt}
Rules:
1. Return only the completion code
2. No explanations
3. Maintain coding style
4. Use best practices
Completion: [/INST]`;
}
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`Model proxy running on port ${PORT}`);
});
4.3 客户端本地模型集成
typescript
// src/services/localModelService.ts
interface ModelConfig {
name: string;
provider: 'local' | 'openai';
description: string;
contextSize: number;
languages: string[];
size?: string;
requiresGPU?: boolean;
}
export class LocalModelService {
private apiUrl: string;
private currentModel: string;
private availableModels: ModelConfig[] = [];
constructor(apiUrl = 'http://localhost:3001') {
this.apiUrl = apiUrl;
this.currentModel = localStorage.getItem('current-model') || 'deepseek-coder:6.7b';
}
// 获取可用模型
async getAvailableModels(): Promise<ModelConfig[]> {
try {
const response = await fetch(`${this.apiUrl}/api/models/available`);
const data = await response.json();
this.availableModels = [
...data.local.map((model: any) => ({
name: model.name,
provider: 'local' as const,
description: this.getModelDescription(model.name),
contextSize: this.getModelContextSize(model.name),
languages: ['all'],
size: this.formatSize(model.size)
})),
{
name: 'gpt-4',
provider: 'openai' as const,
description: 'OpenAI GPT-4 (Cloud)',
contextSize: 8192,
languages: ['all']
},
{
name: 'gpt-3.5-turbo',
provider: 'openai' as const,
description: 'OpenAI GPT-3.5 Turbo (Cloud)',
contextSize: 4096,
languages: ['all']
}
];
return this.availableModels;
} catch (error) {
console.error('Failed to fetch models:', error);
return [];
}
}
// 下载模型
async pullModel(modelName: string, onProgress?: (progress: any) => void): Promise<void> {
return new Promise((resolve, reject) => {
const eventSource = new EventSource(
`${this.apiUrl}/api/models/pull?model=${encodeURIComponent(modelName)}`
);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.status === 'success') {
eventSource.close();
resolve();
} else if (data.error) {
eventSource.close();
reject(new Error(data.error));
} else {
onProgress?.(data);
}
};
eventSource.onerror = (error) => {
eventSource.close();
reject(error);
};
});
}
// 代码补全
async getCompletion(
prompt: string,
options: {
model?: string;
temperature?: number;
maxTokens?: number;
} = {}
): Promise<string[]> {
const model = options.model || this.currentModel;
const isLocal = !model.startsWith('gpt-');
if (isLocal) {
try {
const response = await fetch(`${this.apiUrl}/api/complete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt,
model,
temperature: options.temperature || 0.3,
maxTokens: options.maxTokens || 100
})
});
const data = await response.json();
if (data.fallback) {
console.log('Used fallback to OpenAI');
}
return [data.completion];
} catch (error) {
console.error('Local model failed, falling back to OpenAI:', error);
// 回退到OpenAI
return this.getOpenAICompletion(prompt, options);
}
} else {
return this.getOpenAICompletion(prompt, options);
}
}
// OpenAI补全(备用)
private async getOpenAICompletion(
prompt: string,
options: any
): Promise<string[]> {
const response = await fetch('/api/openai/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt,
...options,
model: options.model || 'gpt-3.5-turbo'
})
});
const data = await response.json();
return [data.completion];
}
// 聊天
async chat(
messages: Array<{ role: string; content: string }>,
options: {
model?: string;
stream?: boolean;
onChunk?: (chunk: string) => void;
} = {}
): Promise<string> {
const model = options.model || this.currentModel;
const isLocal = !model.startsWith('gpt-');
if (isLocal && options.stream) {
return this.streamChat(messages, model, options.onChunk);
}
const endpoint = isLocal ? `${this.apiUrl}/api/chat` : '/api/openai/chat';
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messages,
model,
stream: false
})
});
const data = await response.json();
return data.message?.content || '';
}
// 流式聊天
private async streamChat(
messages: Array<{ role: string; content: string }>,
model: string,
onChunk?: (chunk: string) => void
): Promise<string> {
return new Promise((resolve, reject) => {
const eventSource = new EventSource(
`${this.apiUrl}/api/chat?model=${encodeURIComponent(model)}&stream=true`
);
let fullResponse = '';
eventSource.onmessage = (event) => {
if (event.data === '[DONE]') {
eventSource.close();
resolve(fullResponse);
return;
}
try {
const data = JSON.parse(event.data);
const chunk = data.message?.content || '';
if (chunk) {
fullResponse += chunk;
onChunk?.(chunk);
}
} catch (error) {
console.error('Error parsing stream data:', error);
}
};
eventSource.onerror = (error) => {
eventSource.close();
reject(error);
};
// 发送初始消息
fetch(`${this.apiUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messages,
model,
stream: true
})
}).catch(reject);
});
}
// 工具函数
private getModelDescription(name: string): string {
const descriptions: Record<string, string> = {
'codellama:7b': 'Meta Code Llama 7B - Good for general programming',
'deepseek-coder:6.7b': 'DeepSeek Coder 6.7B - Excellent for code completion',
'starcoder2:7b': 'StarCoder2 7B - Best for multi-language support',
'mistral:7b': 'Mistral 7B - Balanced performance',
'llama2:7b': 'Llama 2 7B - General purpose'
};
return descriptions[name] || name;
}
private getModelContextSize(name: string): number {
const sizes: Record<string, number> = {
'codellama:7b': 4096,
'deepseek-coder:6.7b': 8192,
'starcoder2:7b': 16384,
'mistral:7b': 8192,
'llama2:7b': 4096
};
return sizes[name] || 4096;
}
private formatSize(bytes: number): string {
const gb = bytes / 1024 / 1024 / 1024;
return `${gb.toFixed(1)} GB`;
}
}
// 单例实例
export const localModelService = new LocalModelService();
4.4 模型管理界面
tsx
// src/components/Models/ModelManager.tsx
import { useState, useEffect } from 'react';
import { Download, Cpu, Cloud, HardDrive, Zap, Check } from 'lucide-react';
import { localModelService, ModelConfig } from '../../services/localModelService';
interface DownloadProgress {
status: string;
completed?: number;
total?: number;
digest?: string;
}
export default function ModelManager() {
const [models, setModels] = useState<ModelConfig[]>([]);
const [currentModel, setCurrentModel] = useState('');
const [downloading, setDownloading] = useState<string | null>(null);
const [progress, setProgress] = useState<DownloadProgress | null>(null);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
loadModels();
checkConnection();
// 定期检查连接
const interval = setInterval(checkConnection, 30000);
return () => clearInterval(interval);
}, []);
const loadModels = async () => {
const availableModels = await localModelService.getAvailableModels();
setModels(availableModels);
const savedModel = localStorage.getItem('current-model') || 'gpt-4';
setCurrentModel(savedModel);
};
const checkConnection = async () => {
try {
await fetch('http://localhost:3001/api/models/available', {
signal: AbortSignal.timeout(5000)
});
setIsConnected(true);
} catch (error) {
setIsConnected(false);
}
};
const downloadModel = async (modelName: string) => {
setDownloading(modelName);
setProgress({ status: 'starting' });
try {
await localModelService.pullModel(modelName, (progressData) => {
setProgress(progressData);
});
setProgress({ status: 'completed' });
setTimeout(() => {
setDownloading(null);
setProgress(null);
loadModels();
}, 2000);
} catch (error) {
setProgress({ status: 'error' });
console.error('Download failed:', error);
}
};
const switchModel = async (modelName: string) => {
setCurrentModel(modelName);
localStorage.setItem('current-model', modelName);
// 通知应用模型已切换
window.dispatchEvent(new CustomEvent('model-changed', {
detail: { model: modelName }
}));
};
const getModelStatus = (model: ModelConfig) => {
if (downloading === model.name) {
return 'downloading';
}
if (model.provider === 'local') {
return models.some(m => m.name === model.name) ? 'installed' : 'available';
}
return 'cloud';
};
const formatProgress = () => {
if (!progress) return '';
if (progress.completed && progress.total) {
const percent = (progress.completed / progress.total) * 100;
return `${percent.toFixed(1)}%`;
}
return progress.status;
};
return (
<div className="model-manager p-6">
<div className="mb-6">
<h2 className="text-xl font-bold mb-2">AI Models</h2>
<div className="flex items-center space-x-2 mb-4">
<div className={`w-3 h-3 rounded-full ${isConnected ? 'bg-green-500' : 'bg-red-500'}`} />
<span className="text-sm">
Local server: {isConnected ? 'Connected' : 'Disconnected'}
</span>
</div>
</div>
{/* 当前模型 */}
<div className="mb-8 p-4 bg-gray-800 rounded-lg">
<div className="flex items-center justify-between">
<div>
<h3 className="font-semibold">Current Model</h3>
<p className="text-gray-400 text-sm">
{models.find(m => m.name === currentModel)?.description || currentModel}
</p>
</div>
<div className="flex items-center space-x-2">
{currentModel.startsWith('gpt-') ? (
<Cloud className="text-blue-400" size={20} />
) : (
<Cpu className="text-green-400" size={20} />
)}
<span className="text-sm">
{currentModel.startsWith('gpt-') ? 'Cloud' : 'Local'}
</span>
</div>
</div>
</div>
{/* 模型列表 */}
<div className="space-y-4">
{models.map((model) => {
const status = getModelStatus(model);
const isCurrent = model.name === currentModel;
return (
<div
key={model.name}
className={`p-4 border rounded-lg ${
isCurrent ? 'border-blue-500 bg-blue-900/20' : 'border-gray-700'
}`}
>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center space-x-3">
{model.provider === 'local' ? (
<Cpu className="text-green-400" />
) : (
<Cloud className="text-blue-400" />
)}
<div>
<h4 className="font-semibold">{model.name}</h4>
<p className="text-sm text-gray-400">{model.description}</p>
</div>
</div>
<div className="flex items-center space-x-2">
{status === 'installed' && (
<Check className="text-green-400" size={18} />
)}
{status === 'downloading' && (
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
)}
</div>
</div>
<div className="flex items-center justify-between text-sm">
<div className="flex items-center space-x-4">
<span className="flex items-center">
<Zap size={14} className="mr-1" />
{model.contextSize.toLocaleString()} tokens
</span>
{model.size && (
<span className="flex items-center">
<HardDrive size={14} className="mr-1" />
{model.size}
</span>
)}
</div>
<div className="flex space-x-2">
{!isCurrent && status === 'installed' && (
<button
onClick={() => switchModel(model.name)}
className="px-3 py-1 bg-gray-700 hover:bg-gray-600 rounded"
>
Switch
</button>
)}
{status === 'available' && model.provider === 'local' && (
<button
onClick={() => downloadModel(model.name)}
disabled={!!downloading}
className="px-3 py-1 bg-green-600 hover:bg-green-700 rounded disabled:opacity-50"
>
<Download size={14} className="inline mr-1" />
Download
</button>
)}
{isCurrent && (
<span className="px-3 py-1 bg-blue-600 rounded">Active</span>
)}
</div>
</div>
{/* 下载进度 */}
{status === 'downloading' && progress && (
<div className="mt-3">
<div className="flex justify-between text-sm mb-1">
<span>Downloading...</span>
<span>{formatProgress()}</span>
</div>
<div className="w-full bg-gray-700 rounded-full h-2">
<div
className="bg-green-500 h-2 rounded-full transition-all duration-300"
style={{
width: progress.completed && progress.total
? `${(progress.completed / progress.total) * 100}%`
: '0%'
}}
/>
</div>
</div>
)}
</div>
);
})}
</div>
{/* 模型统计 */}
<div className="mt-8 p-4 bg-gray-800 rounded-lg">
<h4 className="font-semibold mb-3">Usage Statistics</h4>
<div className="grid grid-cols-2 gap-4">
<div>
<div className="text-sm text-gray-400">Tokens Used (Local)</div>
<div className="text-xl">12,458</div>
</div>
<div>
<div className="text-sm text-gray-400">Tokens Used (Cloud)</div>
<div className="text-xl">4,782</div>
</div>
<div>
<div className="text-sm text-gray-400">Estimated Savings</div>
<div className="text-xl text-green-400">$24.50</div>
</div>
<div>
<div className="text-sm text-gray-400">Avg Response Time</div>
<div className="text-xl">1.2s</div>
</div>
</div>
</div>
</div>
);
}
🔌 步骤5:构建插件系统扩展功能
5.1 插件系统架构
typescript
// src/plugins/types.ts
export interface PluginManifest {
id: string;
name: string;
version: string;
description: string;
author: string;
homepage?: string;
icon?: string;
// 依赖
dependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
// 入口点
main: string;
browser?: string;
// 能力声明
capabilities?: PluginCapability[];
permissions?: PluginPermission[];
// 配置
configuration?: PluginConfiguration[];
// 贡献点
contributes?: PluginContribution;
}
export interface PluginCapability {
type: 'completion' | 'lint' | 'format' | 'test' | 'deploy' | 'chat' | 'ui';
description: string;
}
export interface PluginPermission {
type: 'filesystem' | 'network' | 'editor' | 'ai' | 'terminal';
scope: string;
description: string;
}
export interface PluginConfiguration {
id: string;
type: 'string' | 'number' | 'boolean' | 'select' | 'array';
title: string;
description: string;
default?: any;
enum?: string[];
}
export interface PluginContribution {
commands?: PluginCommand[];
menus?: PluginMenu[];
views?: PluginView[];
languages?: PluginLanguage[];
themes?: PluginTheme[];
snippets?: PluginSnippet[];
}
export interface PluginCommand {
command: string;
title: string;
category?: string;
icon?: string;
keybindings?: string[];
}
export interface PluginMenu {
id: string;
items: PluginMenuItem[];
}
export interface PluginMenuItem {
command: string;
when?: string;
group?: string;
}
export interface PluginView {
id: string;
name: string;
type: 'sidebar' | 'panel' | 'editor';
location?: string;
icon: string;
}
export interface PluginLanguage {
id: string;
extensions: string[];
aliases: string[];
configuration?: string;
}
export interface PluginTheme {
id: string;
label: string;
uiTheme: string;
path: string;
}
export interface PluginSnippet {
language: string;
snippets: Record<string, string>;
}
// 插件上下文
export interface PluginContext {
workspace: WorkspaceAPI;
editor: EditorAPI;
ai: AIAPI;
ui: UIIAPI;
storage: StorageAPI;
events: EventBus;
}
// API接口
export interface WorkspaceAPI {
getCurrentFile(): Promise<string>;
readFile(path: string): Promise<string>;
writeFile(path: string, content: string): Promise<void>;
listFiles(): Promise<string[]>;
}
export interface EditorAPI {
getSelection(): string | null;
replaceSelection(content: string): void;
insertText(content: string): void;
getCursorPosition(): { line: number; column: number };
setCursorPosition(line: number, column: number): void;
getLanguage(): string;
setLanguage(language: string): void;
}
export interface AIAPI {
complete(prompt: string, options?: any): Promise<string[]>;
chat(messages: any[], options?: any): Promise<string>;
explain(code: string): Promise<string>;
refactor(code: string, instruction: string): Promise<string>;
}
export interface UIIAPI {
showMessage(message: string, type?: 'info' | 'warning' | 'error'): void;
showInputBox(options: any): Promise<string>;
showQuickPick(items: string[]): Promise<string>;
createWebview(id: string, options: any): any;
}
export interface StorageAPI {
get(key: string): Promise<any>;
set(key: string, value: any): Promise<void>;
delete(key: string): Promise<void>;
}
export interface EventBus {
on(event: string, callback: Function): void;
off(event: string, callback: Function): void;
emit(event: string, data?: any): void;
}
5.2 插件管理器实现
typescript
// src/plugins/PluginManager.ts
import { PluginManifest, PluginContext } from './types';
interface PluginInstance {
manifest: PluginManifest;
module: any;
exports: any;
activated: boolean;
context: PluginContext;
}
export class PluginManager {
private plugins: Map<string, PluginInstance> = new Map();
private pluginContexts: Map<string, PluginContext> = new Map();
private pluginDirectory: string = '/plugins';
// API实现
private workspaceAPI: WorkspaceAPI = {
getCurrentFile: async () => {
return localStorage.getItem('current-file') || 'untitled.ts';
},
readFile: async (path: string) => {
const response = await fetch(`/api/files/${encodeURIComponent(path)}`);
return response.text();
},
writeFile: async (path: string, content: string) => {
await fetch(`/api/files/${encodeURIComponent(path)}`, {
method: 'PUT',
body: content
});
},
listFiles: async () => {
const response = await fetch('/api/files');
return response.json();
}
};
private editorAPI: EditorAPI = {
getSelection: () => {
const editor = this.getEditorInstance();
return editor?.getSelection()?.toString() || null;
},
replaceSelection: (content: string) => {
const editor = this.getEditorInstance();
const selection = editor?.getSelection();
if (selection && editor) {
editor.executeEdits('plugin', [{
range: selection,
text: content
}]);
}
},
// ... 其他方法
};
// 加载插件
async loadPlugin(manifest: PluginManifest): Promise<string> {
const pluginId = manifest.id;
if (this.plugins.has(pluginId)) {
throw new Error(`Plugin ${pluginId} already loaded`);
}
// 检查依赖
await this.checkDependencies(manifest);
// 加载插件模块
const moduleUrl = `${this.pluginDirectory}/${pluginId}/${manifest.main}`;
const module = await import(/* @vite-ignore */ moduleUrl);
// 创建插件上下文
const context: PluginContext = {
workspace: this.workspaceAPI,
editor: this.editorAPI,
ai: this.createAIPluginAPI(pluginId),
ui: this.createUIPluginAPI(pluginId),
storage: this.createStorageAPI(pluginId),
events: this.createEventBus(pluginId)
};
const instance: PluginInstance = {
manifest,
module,
exports: module.default || module,
activated: false,
context
};
this.plugins.set(pluginId, instance);
this.pluginContexts.set(pluginId, context);
return pluginId;
}
// 激活插件
async activatePlugin(pluginId: string): Promise<void> {
const instance = this.plugins.get(pluginId);
if (!instance) {
throw new Error(`Plugin ${pluginId} not found`);
}
if (instance.activated) {
return;
}
try {
// 调用插件的activate函数
if (typeof instance.exports.activate === 'function') {
await instance.exports.activate(instance.context);
}
instance.activated = true;
// 注册贡献点
await this.registerContributions(pluginId, instance);
console.log(`Plugin ${pluginId} activated`);
} catch (error) {
console.error(`Failed to activate plugin ${pluginId}:`, error);
throw error;
}
}
// 停用插件
async deactivatePlugin(pluginId: string): Promise<void> {
const instance = this.plugins.get(pluginId);
if (!instance || !instance.activated) {
return;
}
try {
// 调用插件的deactivate函数
if (typeof instance.exports.deactivate === 'function') {
await instance.exports.deactivate();
}
// 清理贡献点
await this.unregisterContributions(pluginId);
instance.activated = false;
console.log(`Plugin ${pluginId} deactivated`);
} catch (error) {
console.error(`Failed to deactivate plugin ${pluginId}:`, error);
}
}
// 注册插件贡献
private async registerContributions(pluginId: string, instance: PluginInstance): Promise<void> {
const { contributes } = instance.manifest;
if (!contributes) {
return;
}
// 注册命令
if (contributes.commands) {
this.registerCommands(pluginId, contributes.commands);
}
// 注册视图
if (contributes.views) {
this.registerViews(pluginId, contributes.views);
}
// 注册语言
if (contributes.languages) {
this.registerLanguages(pluginId, contributes.languages);
}
// 注册代码片段
if (contributes.snippets) {
this.registerSnippets(pluginId, contributes.snippets);
}
}
// 注册命令
private registerCommands(pluginId: string, commands: any[]): void {
commands.forEach(command => {
const commandId = `${pluginId}.${command.command}`;
// 注册到编辑器命令系统
monaco.editor.registerCommand(commandId, (...args: any[]) => {
const instance = this.plugins.get(pluginId);
if (instance?.activated && instance.exports[command.command]) {
return instance.exports[command.command](...args);
}
});
// 如果有关联的快捷键,注册快捷键
if (command.keybindings) {
this.registerKeybindings(commandId, command.keybindings);
}
});
}
// 注册视图
private registerViews(pluginId: string, views: any[]): void {
views.forEach(view => {
// 发送事件通知UI添加视图
window.dispatchEvent(new CustomEvent('plugin-view-registered', {
detail: {
pluginId,
view
}
}));
});
}
// 创建插件特定的API
private createAIPluginAPI(pluginId: string): AIAPI {
return {
complete: async (prompt: string, options?: any) => {
// 限制插件可以使用的token数量
const limitedOptions = {
...options,
maxTokens: Math.min(options?.maxTokens || 100, 500)
};
// 记录使用量
this.recordPluginUsage(pluginId, 'ai', 'complete');
// 调用AI服务
return localModelService.getCompletion(prompt, limitedOptions);
},
// ... 其他AI方法
};
}
private createUIPluginAPI(pluginId: string): UIIAPI {
return {
showMessage: (message: string, type = 'info') => {
// 显示插件消息
window.dispatchEvent(new CustomEvent('plugin-message', {
detail: { pluginId, message, type }
}));
},
showInputBox: async (options: any) => {
// 显示输入框
return new Promise((resolve) => {
window.dispatchEvent(new CustomEvent('plugin-input-request', {
detail: { pluginId, options, resolve }
}));
});
},
// ... 其他UI方法
};
}
private createStorageAPI(pluginId: string): StorageAPI {
const storageKey = `plugin:${pluginId}`;
return {
get: async (key: string) => {
const storage = JSON.parse(localStorage.getItem(storageKey) || '{}');
return storage[key];
},
set: async (key: string, value: any) => {
const storage = JSON.parse(localStorage.getItem(storageKey) || '{}');
storage[key] = value;
localStorage.setItem(storageKey, JSON.stringify(storage));
},
delete: async (key: string) => {
const storage = JSON.parse(localStorage.getItem(storageKey) || '{}');
delete storage[key];
localStorage.setItem(storageKey, JSON.stringify(storage));
}
};
}
private createEventBus(pluginId: string): EventBus {
const prefix = `plugin:${pluginId}:`;
return {
on: (event: string, callback: Function) => {
const handler = (e: CustomEvent) => callback(e.detail);
window.addEventListener(`${prefix}${event}`, handler as EventListener);
// 返回取消订阅的函数
return () => {
window.removeEventListener(`${prefix}${event}`, handler as EventListener);
};
},
off: (event: string, callback: Function) => {
window.removeEventListener(`${prefix}${event}`, callback as EventListener);
},
emit: (event: string, data?: any) => {
window.dispatchEvent(new CustomEvent(`${prefix}${event}`, { detail: data }));
}
};
}
// 记录插件使用量
private recordPluginUsage(pluginId: string, service: string, action: string): void {
const usageKey = `usage:${pluginId}`;
const usage = JSON.parse(localStorage.getItem(usageKey) || '{}');
usage[service] = usage[service] || {};
usage[service][action] = (usage[service][action] || 0) + 1;
usage.lastUsed = Date.now();
localStorage.setItem(usageKey, JSON.stringify(usage));
}
// 获取编辑器实例
private getEditorInstance(): any {
return (window as any).monacoEditor;
}
}
// 全局插件管理器实例
export const pluginManager = new PluginManager();
5.3 示例插件:代码质量检查
javascript
// plugins/code-quality/manifest.json
{
"id": "code-quality",
"name": "Code Quality Checker",
"version": "1.0.0",
"description": "Checks code quality and provides suggestions",
"author": "AI Editor Team",
"main": "dist/index.js",
"capabilities": ["lint", "format"],
"permissions": [
{
"type": "editor",
"scope": "read",
"description": "Read code from editor"
}
],
"contributes": {
"commands": [
{
"command": "checkQuality",
"title": "Check Code Quality",
"category": "Quality",
"icon": "🔍"
},
{
"command": "fixIssues",
"title": "Fix All Issues",
"category": "Quality"
}
],
"views": [
{
"id": "qualityPanel",
"name": "Code Quality",
"type": "sidebar",
"icon": "shield"
}
]
},
"configuration": [
{
"id": "strictMode",
"type": "boolean",
"title": "Strict Mode",
"description": "Enable stricter quality checks",
"default": false
}
]
}
typescript
// plugins/code-quality/src/index.ts
export function activate(context: PluginContext) {
console.log('Code Quality plugin activated');
// 注册命令
context.ui.showMessage('Code Quality plugin loaded', 'info');
// 返回插件API
return {
checkQuality: async () => {
const code = await context.editor.getSelection() ||
await context.workspace.getCurrentFile();
const issues = await analyzeCode(code);
// 显示问题
context.ui.showMessage(`Found ${issues.length} issues`, 'info');
// 发送到侧边栏
context.events.emit('issues-detected', issues);
return issues;
},
fixIssues: async () => {
const code = await context.editor.getSelection() ||
await context.workspace.getCurrentFile();
const fixedCode = await fixCode(code);
context.editor.replaceSelection(fixedCode);
return true;
}
};
}
export function deactivate() {
console.log('Code Quality plugin deactivated');
}
// 代码分析函数
async function analyzeCode(code: string): Promise<CodeIssue[]> {
const issues: CodeIssue[] = [];
// 检查代码复杂度
const complexity = calculateComplexity(code);
if (complexity > 10) {
issues.push({
type: 'warning',
message: `High cyclomatic complexity (${complexity})`,
line: 1,
suggestion: 'Consider refactoring into smaller functions'
});
}
// 检查未使用的变量
const unusedVars = findUnusedVariables(code);
unusedVars.forEach(variable => {
issues.push({
type: 'error',
message: `Unused variable: ${variable.name}`,
line: variable.line,
suggestion: 'Remove or use the variable'
});
});
// 检查代码风格
const styleIssues = checkStyle(code);
styleIssues.forEach(issue => {
issues.push({
type: 'info',
message: issue.message,
line: issue.line,
suggestion: issue.suggestion
});
});
return issues;
}
async function fixCode(code: string): Promise<string> {
// 使用AI修复代码
const fixed = await context.ai.refactor(code, 'Fix all quality issues');
return fixed;
}
5.4 插件市场实现
tsx
// src/components/Plugins/PluginMarketplace.tsx
import { useState, useEffect } from 'react';
import { Search, Download, Star, Update, Trash2 } from 'lucide-react';
import { pluginManager } from '../../plugins/PluginManager';
interface PluginListing {
id: string;
name: string;
description: string;
author: string;
version: string;
downloads: number;
rating: number;
tags: string[];
installed: boolean;
updateAvailable: boolean;
}
export default function PluginMarketplace() {
const [plugins, setPlugins] = useState<PluginListing[]>([]);
const [search, setSearch] = useState('');
const [installedPlugins, setInstalledPlugins] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
loadPlugins();
loadInstalledPlugins();
}, []);
const loadPlugins = async () => {
try {
const response = await fetch('https://api.ai-editor.com/plugins');
const data = await response.json();
setPlugins(data);
} catch (error) {
console.error('Failed to load plugins:', error);
}
};
const loadInstalledPlugins = () => {
const installed = JSON.parse(localStorage.getItem('installed-plugins') || '[]');
setInstalledPlugins(installed);
};
const installPlugin = async (pluginId: string) => {
setLoading(true);
try {
// 下载插件manifest
const manifestResponse = await fetch(
`https://api.ai-editor.com/plugins/${pluginId}/manifest`
);
const manifest = await manifestResponse.json();
// 下载插件包
const pluginResponse = await fetch(
`https://api.ai-editor.com/plugins/${pluginId}/download`
);
const pluginData = await pluginResponse.blob();
// 保存到本地存储
const pluginPath = `/plugins/${pluginId}`;
// ... 保存逻辑
// 加载插件
await pluginManager.loadPlugin(manifest);
await pluginManager.activatePlugin(pluginId);
// 更新已安装列表
const updated = [...installedPlugins, pluginId];
setInstalledPlugins(updated);
localStorage.setItem('installed-plugins', JSON.stringify(updated));
window.dispatchEvent(new CustomEvent('plugin-installed', {
detail: { pluginId }
}));
} catch (error) {
console.error('Failed to install plugin:', error);
} finally {
setLoading(false);
}
};
const uninstallPlugin = async (pluginId: string) => {
try {
await pluginManager.deactivatePlugin(pluginId);
// 从本地存储移除
const updated = installedPlugins.filter(id => id !== pluginId);
setInstalledPlugins(updated);
localStorage.setItem('installed-plugins', JSON.stringify(updated));
// 清理文件
// ... 清理逻辑
} catch (error) {
console.error('Failed to uninstall plugin:', error);
}
};
const filteredPlugins = plugins.filter(plugin =>
plugin.name.toLowerCase().includes(search.toLowerCase()) ||
plugin.description.toLowerCase().includes(search.toLowerCase()) ||
plugin.tags.some(tag => tag.toLowerCase().includes(search.toLowerCase()))
);
return (
<div className="plugin-marketplace p-6">
<div className="mb-6">
<h2 className="text-xl font-bold mb-4">Plugin Marketplace</h2>
<div className="flex items-center space-x-4 mb-6">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={20} />
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search plugins..."
className="w-full pl-10 pr-4 py-2 bg-gray-800 rounded-lg"
/>
</div>
<div className="text-sm">
{installedPlugins.length} installed
</div>
</div>
</div>
{/* 已安装插件 */}
<div className="mb-8">
<h3 className="font-semibold mb-4">Installed Plugins</h3>
<div className="space-y-3">
{installedPlugins.map(pluginId => {
const plugin = plugins.find(p => p.id === pluginId);
if (!plugin) return null;
return (
<div key={pluginId} className="p-4 bg-gray-800 rounded-lg">
<div className="flex items-center justify-between">
<div>
<h4 className="font-semibold">{plugin.name}</h4>
<p className="text-sm text-gray-400">{plugin.description}</p>
</div>
<div className="flex items-center space-x-2">
{plugin.updateAvailable && (
<button className="p-2 hover:bg-gray-700 rounded">
<Update size={18} />
</button>
)}
<button
onClick={() => uninstallPlugin(pluginId)}
className="p-2 hover:bg-red-900/30 rounded"
>
<Trash2 size={18} />
</button>
</div>
</div>
</div>
);
})}
</div>
</div>
{/* 可用插件 */}
<div>
<h3 className="font-semibold mb-4">Available Plugins</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{filteredPlugins.map(plugin => {
const isInstalled = installedPlugins.includes(plugin.id);
return (
<div key={plugin.id} className="p-4 bg-gray-800 rounded-lg">
<div className="flex items-start justify-between mb-3">
<div>
<h4 className="font-semibold">{plugin.name}</h4>
<p className="text-sm text-gray-400 mt-1">{plugin.description}</p>
</div>
<div className="flex items-center space-x-1">
<Star size={14} className="text-yellow-400" />
<span className="text-sm">{plugin.rating.toFixed(1)}</span>
</div>
</div>
<div className="flex flex-wrap gap-2 mb-4">
{plugin.tags.map(tag => (
<span
key={tag}
className="px-2 py-1 bg-gray-700 rounded text-xs"
>
{tag}
</span>
))}
</div>
<div className="flex items-center justify-between">
<div className="text-sm text-gray-400">
{plugin.downloads.toLocaleString()} downloads
</div>
<button
onClick={() => installPlugin(plugin.id)}
disabled={isInstalled || loading}
className={`px-4 py-2 rounded ${
isInstalled
? 'bg-gray-700 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700'
}`}
>
{isInstalled ? 'Installed' : 'Install'}
</button>
</div>
</div>
);
})}
</div>
</div>
</div>
);
}
5.5 插件系统集成到主应用
tsx
// src/components/Plugins/PluginHost.tsx
import { useEffect, useState } from 'react';
import { pluginManager } from '../../plugins/PluginManager';
export default function PluginHost() {
const [plugins, setPlugins] = useState<any[]>([]);
const [views, setViews] = useState<any[]>([]);
useEffect(() => {
// 加载内置插件
loadBuiltinPlugins();
// 监听插件事件
window.addEventListener('plugin-view-registered', handleViewRegistered);
window.addEventListener('plugin-installed', handlePluginInstalled);
return () => {
window.removeEventListener('plugin-view-registered', handleViewRegistered);
window.removeEventListener('plugin-installed', handlePluginInstalled);
};
}, []);
const loadBuiltinPlugins = async () => {
const builtinPlugins = [
{
id: 'code-quality',
manifest: {
id: 'code-quality',
name: 'Code Quality',
version: '1.0.0',
main: './plugins/code-quality/index.js'
}
},
{
id: 'git-integration',
manifest: {
id: 'git-integration',
name: 'Git Integration',
version: '1.0.0',
main: './plugins/git/index.js'
}
},
{
id: 'test-generator',
manifest: {
id: 'test-generator',
name: 'Test Generator',
version: '1.0.0',
main: './plugins/test-generator/index.js'
}
}
];
for (const plugin of builtinPlugins) {
try {
await pluginManager.loadPlugin(plugin.manifest);
await pluginManager.activatePlugin(plugin.id);
} catch (error) {
console.error(`Failed to load plugin ${plugin.id}:`, error);
}
}
};
const handleViewRegistered = (event: CustomEvent) => {
const { pluginId, view } = event.detail;
setViews(prev => [...prev, { pluginId, ...view }]);
};
const handlePluginInstalled = (event: CustomEvent) => {
const { pluginId } = event.detail;
console.log(`Plugin ${pluginId} installed`);
};
return (
<div className="plugin-host">
{/* 插件视图容器 */}
<div className="plugin-views">
{views.map(view => (
<div key={`${view.pluginId}-${view.id}`} className="plugin-view">
<div className="plugin-view-header">
<h3>{view.name}</h3>
</div>
<div className="plugin-view-content">
{/* 视图内容会通过WebComponent或iframe加载 */}
</div>
</div>
))}
</div>
</div>
);
}
🎯 总结与部署
项目结构
text
ai-editor/
├── src/
│ ├── components/
│ │ ├── Editor/ # 编辑器组件
│ │ ├── Chat/ # AI聊天助手
│ │ ├── Settings/ # 设置面板
│ │ ├── Models/ # 模型管理
│ │ └── Plugins/ # 插件系统
│ ├── services/ # 各种服务
│ ├── plugins/ # 插件系统核心
│ ├── store/ # 状态管理
│ └── utils/ # 工具函数
├── server/ # 后端服务
│ ├── model-proxy/ # 模型代理
│ └── plugin-server/ # 插件服务器
├── plugins/ # 插件目录
├── public/
└── package.json
部署脚本
json
{
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"dev:full": "concurrently \"npm run dev\" \"npm run server\"",
"server": "cd server && npm run dev",
"docker:up": "docker-compose up -d",
"docker:down": "docker-compose down",
"docker:build": "docker-compose build",
"plugins:build": "cd plugins && npm run build"
}
}
Docker部署
dockerfile
# 前端Dockerfile
FROM node:18-alpine as builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
后续优化方向
-
性能优化
-
WebWorker中运行AI模型推理
-
代码索引和缓存
-
虚拟滚动大型文件
-
-
协作功能
-
实时协同编辑
-
共享AI会话
-
代码审查工作流
-
-
高级AI功能
-
代码库学习(RAG)
-
个性化AI助手
-
自动测试生成
-
-
企业功能
-
团队管理
-
私有模型部署
-
审计日志
-
这个详细的实现方案涵盖了从基础编辑器到完整插件系统的所有关键步骤。每个组件都可以独立开发并逐步集成。建议从步骤1和2开始,快速获得可用的AI编辑器,然后逐步添加更高级的功能。