feat(chat): 实现流式传输功能并优化聊天界面

- 新增流式传输相关功能,包括开始流、处理数据和结束流
- 优化聊天界面样式,增加消息发送和停止按钮
- 改进代码差异显示和文件创建功能
- 添加用户配置支持,用于自定义API地址和密钥
- 重构部分代码以提高可维护性
refactor
钟良源 8 months ago
parent cedac4ff73
commit 24e437f647

@ -72,7 +72,7 @@
<!-- 聊天输入表单 -->
<form id="chat-form">
<input type="text" id="user-input" placeholder="输入你的问题..." required />
<button type="submit">发送</button>
<button type="submit" id="send-btn">发送</button>
</form>
<div id="loading" class="hidden">AI 正在思考...</div>
</div>

@ -1,5 +1,11 @@
import {detectLanguage, getFileExtension} from "../../src/utils/common"
let selectedCodeForContext = '';
let contextFilePath = ''; // 存储当前上下文文件路径
let streamingMessageElement = null; // 用于存储当前流式消息的DOM元素
let streamingMessageContent = ''; // 用于存储当前流式消息的内容
let currentReader = null; // 用于存储当前流的reader以便可以取消
let isRequestInProgress = false; // 标记是否有请求正在进行
const vscode = acquireVsCodeApi();
document.addEventListener('DOMContentLoaded', () => {
@ -7,6 +13,7 @@ document.addEventListener('DOMContentLoaded', () => {
const userInput = document.getElementById('user-input');
const chatBox = document.getElementById('chat-box');
const loading = document.getElementById('loading');
const sendBtn = document.getElementById('send-btn'); // 获取发送按钮
// 初始化隐藏工作区
const workspaceContainer = document.querySelector('.workspace-container');
@ -14,11 +21,25 @@ document.addEventListener('DOMContentLoaded', () => {
workspaceContainer.style.display = 'none';
}
chatForm.addEventListener('submit', (e) => {
chatForm.addEventListener('submit', async (e) => {
e.preventDefault();
// 如果有请求正在进行,点击按钮将中断请求
if (isRequestInProgress) {
// 发送取消请求的消息到扩展
vscode.postMessage({
command: 'cancelRequest'
});
resetSendButton();
hideLoading();
return;
}
const text = userInput.value.trim();
if (!text) return;
// 发送消息并禁用按钮
vscode.postMessage({
command: 'ask',
text,
@ -29,8 +50,36 @@ document.addEventListener('DOMContentLoaded', () => {
// addMessage('user', text);
showLoading();
userInput.value = '';
// 切换按钮为停止按钮
isRequestInProgress = true;
if (sendBtn) {
sendBtn.textContent = '停止';
sendBtn.classList.add('stop-button');
}
});
// 重置发送按钮为初始状态
function resetSendButton() {
isRequestInProgress = false;
const sendBtn = document.getElementById('send-btn');
if (sendBtn) {
sendBtn.textContent = '发送';
sendBtn.classList.remove('stop-button');
}
}
// 显示加载状态
function showLoading() {
loading.classList.remove('hidden');
chatBox.scrollTop = chatBox.scrollHeight;
}
// 隐藏加载状态
function hideLoading() {
loading.classList.add('hidden');
}
// 判断内容是否为 Markdown简单判断
function isMarkdown(content) {
if (!content || content.trim() === '') return false;
@ -49,7 +98,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
// 处理添加消息
function addMessage(role, content, codeDiff) {
function addMessage(role, content, codeDiffs) {
const msgDiv = document.createElement('div');
msgDiv.className = `message ${role}`;
@ -64,24 +113,29 @@ document.addEventListener('DOMContentLoaded', () => {
msgDiv.appendChild(div);
}
// 如果有代码差异,添加查看差异按钮
if (codeDiff) {
const diffButton = document.createElement('button');
diffButton.className = 'diff-button';
diffButton.textContent = '查看代码差异';
diffButton.onclick = () => showCodeDiff(codeDiff);
msgDiv.appendChild(diffButton);
}
// 如果是AI消息且包含代码块添加"创建文件"按钮
// 如果是AI消息且包含代码块为每个代码块添加按钮
if (role === 'ai') {
const codeBlocks = msgDiv.querySelectorAll('pre code');
codeBlocks.forEach((block, index) => {
// 为每个代码块添加"创建文件"按钮
const createFileButton = document.createElement('button');
createFileButton.className = 'create-file-button';
createFileButton.textContent = `生成代码文件`;
createFileButton.onclick = () => createNewFileFromCode(block.textContent, index);
msgDiv.appendChild(createFileButton);
// 将按钮插入到代码块后面
block.parentNode.after(createFileButton);
// 如果有代码差异,也为每个代码块添加查看差异按钮
// codeDiffs是一个数组每个元素对应一个代码块的差异信息
if (codeDiffs && codeDiffs[index]) {
const diffButton = document.createElement('button');
diffButton.className = 'diff-button';
diffButton.textContent = `查看代码差异`;
diffButton.onclick = () => showCodeDiff(codeDiffs[index]);
// 将差异按钮插入到创建文件按钮后面
createFileButton.after(diffButton);
}
});
}
@ -97,6 +151,147 @@ document.addEventListener('DOMContentLoaded', () => {
// 滚动到底部
chatBox.scrollTop = chatBox.scrollHeight;
return msgDiv; // 返回创建的消息元素
}
// 开始流式传输
function startStreaming() {
// 创建一个新的AI消息元素
streamingMessageElement = document.createElement('div');
streamingMessageElement.className = 'message ai streaming';
// 添加流式消息内容容器
const contentDiv = document.createElement('div');
contentDiv.className = 'streaming-content';
streamingMessageElement.appendChild(contentDiv);
// 添加光标元素
const cursor = document.createElement('span');
cursor.className = 'streaming-cursor';
cursor.textContent = '▋';
streamingMessageElement.appendChild(cursor);
chatBox.appendChild(streamingMessageElement);
streamingMessageContent = '';
// 滚动到底部
chatBox.scrollTop = chatBox.scrollHeight;
}
// 处理流式数据
function handleStreamData(data) {
if (!streamingMessageElement) return;
let processedData = data;
if (data.startsWith('data:')) {
// 移除data:前缀
processedData = data.replace(/^data:/, '');
// 逐个匹配换行符,当出现两个或更多连续换行符时删除多余的
processedData = processedData.replace(/^(\n{2,})/, (match) => {
// 删除所有连续的换行符
return '';
});
}
// 更新内容
streamingMessageContent += processedData;
// 更新显示
const contentDiv = streamingMessageElement.querySelector('.streaming-content');
if (contentDiv) {
// 如果是Markdown内容我们需要特殊处理
if (isMarkdown(streamingMessageContent)) {
contentDiv.innerHTML = marked.parse(streamingMessageContent);
} else {
contentDiv.textContent = streamingMessageContent;
}
// 高亮代码块
const codeBlocks = contentDiv.querySelectorAll('pre code');
codeBlocks.forEach(block => {
if (hljs && !block.dataset.highlighted) {
hljs.highlightElement(block);
block.dataset.highlighted = 'true';
}
});
}
// 滚动到底部
chatBox.scrollTop = chatBox.scrollHeight;
}
// 结束流式传输
function endStreaming(content, codeDiffs = null) {
if (streamingMessageElement) {
// 移除流式传输类和光标
streamingMessageElement.classList.remove('streaming');
const cursor = streamingMessageElement.querySelector('.streaming-cursor');
if (cursor) {
cursor.remove();
}
// 如果是AI消息且包含代码块为每个代码块添加按钮
if (streamingMessageElement.classList.contains('ai')) {
const codeBlocks = streamingMessageElement.querySelectorAll('pre code');
codeBlocks.forEach((block, index) => {
// 为每个代码块添加"创建文件"按钮
const createFileButton = document.createElement('button');
createFileButton.className = 'create-file-button';
createFileButton.textContent = `生成代码文件`;
createFileButton.onclick = () => createNewFileFromCode(block.textContent, index);
// 将按钮插入到代码块后面
block.parentNode.after(createFileButton);
// 如果有代码差异,也为每个代码块添加查看差异按钮
// codeDiffs是一个数组每个元素对应一个代码块的差异信息
if (codeDiffs && codeDiffs[index]) {
const diffButton = document.createElement('button');
diffButton.className = 'diff-button';
diffButton.textContent = `查看代码差异`;
diffButton.onclick = () => showCodeDiff(codeDiffs[index]);
// 将差异按钮插入到创建文件按钮后面
createFileButton.after(diffButton);
}
});
}
// 高亮代码块
const codeBlocks = streamingMessageElement.querySelectorAll('pre code');
codeBlocks.forEach(block => {
if (hljs) {
hljs.highlightElement(block);
}
});
streamingMessageElement = null;
streamingMessageContent = '';
}
// 保存消息到历史记录
currentSessionHistory.push({role: 'assistant', content: content});
hideLoading();
}
// 处理请求取消
function handleRequestCancelled() {
// 重置发送按钮
resetSendButton();
// 如果有正在流式传输的消息,结束它
if (streamingMessageElement) {
streamingMessageElement.classList.remove('streaming');
const cursor = streamingMessageElement.querySelector('.streaming-cursor');
if (cursor) {
cursor.remove();
}
streamingMessageElement = null;
streamingMessageContent = '';
}
hideLoading();
}
// 显示代码差异
@ -184,14 +379,6 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
function showLoading() {
loading.classList.remove('hidden');
}
function hideLoading() {
loading.classList.add('hidden');
}
// 更新工作区文件列表
function updateWorkspaceFiles(files) {
const workspaceContainer = document.querySelector('.workspace-container');
@ -255,7 +442,19 @@ document.addEventListener('DOMContentLoaded', () => {
const message = event.data;
console.log(message)
if (message.command === 'addMessage') {
addMessage(message.role, message.content,message.codeDiff);
addMessage(message.role, message.content, message.codeDiff);
} else if (message.command === 'startStream') {
// 开始流式传输
startStreaming();
} else if (message.command === 'streamData') {
// 处理流式数据
handleStreamData(message.data);
} else if (message.command === 'endStream') {
// 结束流式传输
endStreaming(message.content, message.codeDiffs);
} else if (message.command === 'requestCancelled') {
// 请求被取消
handleRequestCancelled();
} else if (message.command === 'hideLoading') {
hideLoading();
} else if (message.command === 'addToInput') {
@ -392,6 +591,7 @@ document.addEventListener('DOMContentLoaded', () => {
document.getElementById('diff-modal').classList.add('hidden');
}
});
// 从代码创建新文件
function createNewFileFromCode(codeContent, index) {
// 简单的语言检测
@ -407,41 +607,4 @@ document.addEventListener('DOMContentLoaded', () => {
language: language
});
}
// 简单的语言检测
function detectLanguage(code) {
// 可以根据代码特征进行简单判断
if (code.includes('import React') || code.includes('from react')) {
return 'javascript'; // React代码
} else if (code.includes('public class') || code.includes('private static')) {
return 'java';
} else if (code.includes('def ') && code.includes(':')) {
return 'python';
} else if (code.includes('function ') || code.includes('const ') || code.includes('let ')) {
return 'javascript';
} else if (code.includes('interface ') && code.includes('export ')) {
return 'typescript';
} else if (code.includes('<?php')) {
return 'php';
} else if (code.includes('using System')) {
return 'csharp';
}
return 'text'; // 默认
}
// 根据语言获取文件扩展名
function getFileExtension(language) {
const extensions = {
'javascript': '.js',
'typescript': '.ts',
'python': '.py',
'java': '.java',
'html': '.html',
'css': '.css',
'php': '.php',
'csharp': '.cs',
'text': '.txt'
};
return extensions[language] || '.txt';
}
});

@ -88,20 +88,28 @@ code {
#chat-form button {
padding: 8px 16px;
background-color: var(--vscode-button-background);
color: var(--vscode-button-foreground);
background-color: #007acc;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-family: var(--vscode-font-family);
font-size: var(--vscode-font-size);
font-size: 14px;
transition: background-color 0.2s;
}
#chat-form button:hover {
background-color: var(--vscode-button-hoverBackground);
background-color: #005a9e;
}
/* 停止按钮样式 */
#chat-form button.stop-button {
background-color: #f44336;
}
#chat-form button.stop-button:hover {
background-color: #d32f2f;
}
/* 加载提示 */
#loading {
text-align: center;
padding: 10px;
@ -388,6 +396,40 @@ code {
background-color: #45a049;
}
.code-button-container {
display: flex;
gap: 10px;
margin-top: 10px;
flex-wrap: wrap;
}
.code-button-container .create-file-button {
margin: 0;
}
/* 流式消息样式 */
.message.ai.streaming {
position: relative;
}
.streaming-content {
display: inline-block;
width: 100%;
}
.streaming-cursor {
display: inline-block;
width: 0.7em;
animation: blink 1s infinite;
color: var(--vscode-editor-foreground);
vertical-align: baseline;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
/* 工作区容器 */
.workspace-container {
margin: 10px 0;

@ -14,6 +14,26 @@
"onView:ai-chat-sidebar-view"
],
"contributes": {
"configuration": {
"type": "object",
"title": "AI Chat Configuration",
"properties": {
"ai-chat.api.url": {
"type": "string",
"description": "完整的API地址优先级最高"
},
"ai-chat.api.baseUrl": {
"type": "string",
"description": "API基础地址",
"default": "https://p13-ai.ngsk.tech:7001"
},
"ai-chat.api.key": {
"type": "string",
"description": "API密钥",
"default": "dev-token"
}
}
},
"viewsContainers": {
"activitybar": [
{

@ -0,0 +1,81 @@
import vscode from "vscode";
import path from "path";
import {getWebviewContent} from "./utils/webView";
import {MessageHandler} from "./MessageHandler";
export class AISidebarViewProvider implements vscode.WebviewViewProvider {
public static readonly viewType = 'ai-chat-sidebar-view';
private _view?: vscode.WebviewView;
private _extensionPath: string;
private _context: vscode.ExtensionContext;
private _messageHandler: MessageHandler;
constructor(extensionPath: string, context: vscode.ExtensionContext) {
this._extensionPath = extensionPath;
this._context = context;
this._messageHandler = new MessageHandler(this, context);
}
public resolveWebviewView(
webviewView: vscode.WebviewView,
context: vscode.WebviewViewResolveContext,
_token: vscode.CancellationToken,
) {
this._view = webviewView;
webviewView.webview.options = {
enableScripts: true,
localResourceRoots: [
vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview'))
]
};
webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
webviewView.webview.onDidReceiveMessage(async (message) => {
this._messageHandler.handleMessage(message);
});
}
public sendSelectedCode(code: string) {
if (this._view) {
this._view.show?.(true);
this._postMessage({
command: 'addToInput',
role: 'user',
content: code
});
}
}
private _getHtmlForWebview(webview: vscode.Webview) {
const styleUri = webview.asWebviewUri(
vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview', 'style.css'))
);
const scriptUri = webview.asWebviewUri(
vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview', 'script.js'))
);
const highlightScriptUri = webview.asWebviewUri(
vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview', 'highlight.min.js'))
);
const highlightStyleUri = webview.asWebviewUri(
vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview', 'styles', 'atom-one-dark.css'))
);
const markedScriptUri = webview.asWebviewUri(
vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview', 'marked.min.js'))
);
return getWebviewContent(styleUri, scriptUri, highlightScriptUri, highlightStyleUri, markedScriptUri);
}
public _postMessage(message: any) {
if (this._view) {
this._view.webview.postMessage(message);
}
}
}

@ -0,0 +1,489 @@
import vscode from "vscode";
import path from "path";
import {callQwenAPI} from "./utils/modelApi";
import {generateCodeDiff, getFileExtension} from "./utils/common";
// 定义全局变量来存储面板状态
let currentSessionHistory: { role: string; content: string }[] = [];
// 存储工作区变更的文件,包含状态信息
let workspaceChanges: {
[key: string]: {
original: string;
modified: string;
status: 'pending' | 'accepted' | 'rejected'
}
} = {};
export class MessageHandler {
private provider: any; // 这里应该使用AISidebarViewProvider类型但由于循环依赖问题暂时使用any
private context: vscode.ExtensionContext;
private currentReader: ReadableStreamDefaultReader<Uint8Array> | null = null; // 添加当前reader引用
constructor(provider: any, context: vscode.ExtensionContext) {
this.provider = provider;
this.context = context;
this.currentReader = null;
}
public async handleMessage(message: any) {
switch (message.command) {
case 'ask':
await this.handleAsk(message);
break;
case 'cancelRequest':
await this.handleCancelRequest();
break;
case 'selectContextFile':
await this.handleSelectContextFile();
break;
case 'getFileList':
await this.handleGetFileList();
break;
case 'selectFileByPath':
await this.handleSelectFileByPath(message);
break;
case 'showCodeDiffInEditor':
await this.handleShowCodeDiffInEditor(message);
break;
case 'acceptChanges':
await this.handleAcceptChanges(message);
break;
case 'rejectChanges':
await this.handleRejectChanges(message);
break;
case 'openWorkspaceFile':
await this.handleOpenWorkspaceFile(message);
break;
case 'getCodeChanges':
this.handleGetCodeChanges();
break;
case 'createNewFile':
await this.handleCreateNewFile(message);
break;
case 'restoreState':
this.handleRestoreState(message);
break;
}
}
private async handleAsk(message: any) {
const question = message.text;
const fileContent = message.fileContent;
const fileName = message.fileContentPath ? path.basename(message.fileContentPath) : "current_file.txt";
const history = currentSessionHistory;
currentSessionHistory = [...history, {role: 'user', content: question}];
this.provider._postMessage({command: 'addMessage', role: 'user', content: question});
try {
const response = await callQwenAPI(question, history, fileContent, this.context, fileName);
// 发送开始流式传输的消息
this.provider._postMessage({command: 'startStream', role: 'assistant'});
// 处理流式响应
const reader = response.body?.getReader();
this.currentReader = reader; // 保存reader引用
const decoder = new TextDecoder('utf-8');
let aiMessage = '';
if (reader) {
try {
while (true) {
const {done, value} = await reader.read();
if (done) break;
const chunk = decoder.decode(value, {stream: true});
// 检查是否包含event: end事件
let processedChunk = chunk;
if (chunk.includes('event: end')) {
// 只处理event: end之前的内容
const endIndex = chunk.indexOf('event: end');
processedChunk = chunk.substring(0, endIndex);
// 处理截取后的内容
if (processedChunk.startsWith('data:')) {
// 移除data:前缀
processedChunk = processedChunk.replace(/^data:/, '');
// 逐个匹配换行符,当出现两个或更多连续换行符时删除多余的
processedChunk = processedChunk.replace(/^(\n{2,})/, (match) => {
// 删除所有连续的换行符
return '';
});
}
if (processedChunk) {
aiMessage += processedChunk;
// 发送流式数据到前端
this.provider._postMessage({
command: 'streamData',
data: processedChunk
});
}
// 停止继续处理
break;
}
if (chunk.startsWith('data:')) {
// 移除data:前缀
processedChunk = chunk.replace(/^data:/, '');
// 逐个匹配换行符,当出现两个或更多连续换行符时删除多余的
processedChunk = processedChunk.replace(/^(\n{2,})/, (match) => {
// 删除所有连续的换行符
return '';
});
}
aiMessage += processedChunk;
// 发送流式数据到前端
this.provider._postMessage({
command: 'streamData',
data: processedChunk
});
}
} catch (readError: any) {
// 检查是否是由于取消请求导致的中断
if (readError.name === 'AbortError' || readError.message.includes('cancel')) {
console.log('请求已被用户中断');
// 通知前端请求已中断
this.provider._postMessage({command: 'requestCancelled'});
// 重置状态但不抛出错误
this.provider._postMessage({command: 'hideLoading'});
return;
}
// 如果是其他错误,重新抛出
throw readError;
} finally {
// 清理reader引用
this.currentReader = null;
}
// 流传输完成后,更新会话历史
currentSessionHistory = [...currentSessionHistory, {role: 'assistant', content: aiMessage}];
// 解析AI响应中的多个代码块并生成独立的codeDiff信息
const codeBlocks = aiMessage.match(/```[\s\S]*?```/g) || [];
// 存储所有生成的codeDiff
const allCodeDiffs = [];
for (const block of codeBlocks) {
// 生成codeDiff
const codeDiff = generateCodeDiff(fileContent, block);
if (codeDiff) {
// @ts-ignore
allCodeDiffs.push(codeDiff);
}
}
console.log("allCodeDiffs:",allCodeDiffs)
// 通知前端流传输完成
this.provider._postMessage({
command: 'endStream',
content: aiMessage,
codeDiffs: allCodeDiffs // 传递多个codeDiff信息
});
// 如果有代码差异,保存到工作区变更中
if (allCodeDiffs.length > 0 && fileContent && message.fileContentPath) {
// 为每个codeDiff创建独立的workspaceChange
for (let i = 0; i < allCodeDiffs.length; i++) {
const codeDiff = allCodeDiffs[i];
const filePath = message.fileContentPath;
// 创建唯一的工作区变更键
const changeKey = `${filePath}`;
workspaceChanges[changeKey] = {
original: fileContent,
// @ts-ignore
modified: codeDiff.modifiedCode,
status: 'pending'
};
}
// 通知 WebView 更新工作区文件列表
this.provider._postMessage({
command: 'updateWorkspaceFiles',
files: workspaceChanges
});
}
}
} catch (error: any) {
// 检查是否是由于取消请求导致的中断
if (error.name === 'AbortError' || error.message.includes('cancel') || error.message.includes('中断')) {
console.log('请求已被用户中断');
this.provider._postMessage({command: 'requestCancelled'});
} else {
this.provider._postMessage({command: 'addMessage', role: 'ai', content: '调用失败:' + error.message});
}
}
this.provider._postMessage({command: 'hideLoading'});
}
private async handleSelectContextFile() {
// 执行文件选择逻辑
const uris = await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
title: "选择一个文件作为上下文"
});
if (uris && uris.length > 0) {
const selectedFileUri = uris[0];
const fileContent = await vscode.workspace.fs.readFile(selectedFileUri);
const decodedContent = new TextDecoder("utf-8").decode(fileContent);
this.provider._postMessage({
command: 'setContextFile',
fileName: selectedFileUri.fsPath,
fileContent: decodedContent
});
}
}
private async handleGetFileList() {
// 执行获取文件列表逻辑
const workspaceFolders = vscode.workspace.workspaceFolders;
if (workspaceFolders && workspaceFolders.length > 0) {
const fileUris = await vscode.workspace.findFiles('**/*', '**/node_modules/**');
const files = fileUris.map(uri => {
const relativePath = vscode.workspace.asRelativePath(uri);
return {
path: uri.fsPath,
relativePath: relativePath,
name: path.basename(uri.fsPath),
isDirectory: false // 简化处理,实际可以根据扩展名判断
};
});
this.provider._postMessage({
command: 'fileList',
files: files
});
}
}
private async handleSelectFileByPath(message: any) {
// 根据路径选择文件
try {
const fileUri = vscode.Uri.file(message.filePath);
const fileContent = await vscode.workspace.fs.readFile(fileUri);
const decodedContent = new TextDecoder("utf-8").decode(fileContent);
this.provider._postMessage({
command: 'setContextFile',
fileName: fileUri.fsPath,
fileContent: decodedContent
});
} catch (error: any) {
vscode.window.showErrorMessage(`无法读取文件: ${error.message}`);
}
}
private async handleShowCodeDiffInEditor(message: any) {
// 在编辑器中显示代码差异
try {
const originalCode = message.originalCode || '';
const modifiedCode = message.modifiedCode || '';
// 创建原始代码的虚拟文档
const originalUri = vscode.Uri.parse('untitled:original-code.txt');
const originalDoc = await vscode.workspace.openTextDocument(originalUri);
const originalEditor = await vscode.window.showTextDocument(originalDoc, vscode.ViewColumn.One, true);
await originalEditor.edit(edit => {
edit.insert(new vscode.Position(0, 0), originalCode);
});
// 创建修改后代码的虚拟文档
const modifiedUri = vscode.Uri.parse('untitled:modified-code.txt');
const modifiedDoc = await vscode.workspace.openTextDocument(modifiedUri);
const modifiedEditor = await vscode.window.showTextDocument(modifiedDoc, vscode.ViewColumn.Two, true);
await modifiedEditor.edit(edit => {
edit.insert(new vscode.Position(0, 0), modifiedCode);
});
// 使用 VS Code 的 diff 命令显示差异
await vscode.commands.executeCommand(
'vscode.diff',
originalUri,
modifiedUri,
'Code Diff - Original ↔ Modified'
);
} catch (error: any) {
vscode.window.showErrorMessage(`显示代码差异失败: ${error.message}`);
}
}
private async handleAcceptChanges(message: any) {
// 接受代码变更
try {
const {filePath, modifiedContent} = message;
const fileUri = vscode.Uri.file(filePath);
// 将修改后的内容写入文件
const encodedContent = new TextEncoder().encode(modifiedContent);
await vscode.workspace.fs.writeFile(fileUri, encodedContent);
// 更新工作区变更状态
if (workspaceChanges[filePath]) {
workspaceChanges[filePath].status = 'accepted';
}
// 通知 WebView 更新工作区文件列表
this.provider._postMessage({
command: 'updateWorkspaceFiles',
files: workspaceChanges
});
vscode.window.showInformationMessage('代码变更已应用');
} catch (error: any) {
vscode.window.showErrorMessage(`应用代码变更失败: ${error.message}`);
}
}
private async handleRejectChanges(message: any) {
// 拒绝代码变更
try {
const {filePath} = message;
// 更新工作区变更状态
if (workspaceChanges[filePath]) {
workspaceChanges[filePath].status = 'rejected';
}
// 通知 WebView 更新工作区文件列表
this.provider._postMessage({
command: 'updateWorkspaceFiles',
files: workspaceChanges
});
vscode.window.showInformationMessage('代码变更已拒绝');
} catch (error: any) {
vscode.window.showErrorMessage(`拒绝代码变更失败: ${error.message}`);
}
}
private async handleOpenWorkspaceFile(message: any) {
// 打开工作区文件
try {
const {filePath} = message;
const fileUri = vscode.Uri.file(filePath);
// 检查文件是否已经打开
let fileAlreadyOpen = false;
const openedTextDocuments = vscode.workspace.textDocuments;
for (const doc of openedTextDocuments) {
if (doc.uri.fsPath === fileUri.fsPath) {
fileAlreadyOpen = true;
// 激活已打开的文件
await vscode.window.showTextDocument(doc, {
viewColumn: vscode.ViewColumn.One // 切换到已打开的文件
});
break;
}
}
// 如果文件未打开,则在新窗口打开
if (!fileAlreadyOpen) {
await vscode.window.showTextDocument(fileUri, {
viewColumn: vscode.ViewColumn.One // 在第一列打开文件
});
}
} catch (error: any) {
vscode.window.showErrorMessage(`打开文件失败: ${error.message}`);
}
}
private handleGetCodeChanges() {
// 获取代码变更
this.provider._postMessage({
command: 'codeChanges',
changes: workspaceChanges
});
}
private async handleCreateNewFile(message: any) {
// 创建新文件功能
try {
const {fileName, content, language} = message;
// 获取根工作区路径
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
vscode.window.showErrorMessage('未找到工作区');
return;
}
// 根据语言确定文件扩展名
const fileExtension = getFileExtension(language);
const fullFileName = fileName.endsWith(fileExtension) ? fileName : `${fileName}${fileExtension}`;
// 创建文件路径(在根目录下)
const filePath = path.join(workspaceFolders[0].uri.fsPath, fullFileName);
const fileUri = vscode.Uri.file(filePath);
// 将内容写入文件
const encodedContent = new TextEncoder().encode(content);
await vscode.workspace.fs.writeFile(fileUri, encodedContent);
// 在编辑器中打开新创建的文件
await vscode.window.showTextDocument(fileUri, {
viewColumn: vscode.ViewColumn.One
});
vscode.window.showInformationMessage(`文件 ${fullFileName} 已创建`);
} catch (error: any) {
vscode.window.showErrorMessage(`生成代码文件失败: ${error.message}`);
}
}
private handleRestoreState(message: any) {
// 恢复面板状态
currentSessionHistory = message.history || [];
// 恢复显示历史消息
currentSessionHistory.forEach(msg => {
this.provider._postMessage({
command: 'addMessage',
role: msg.role,
content: msg.content,
codeDiff: null
});
});
// 恢复工作区变更
if (message.workspaceChanges) {
this.provider._postMessage({
command: 'updateWorkspaceFiles',
files: message.workspaceChanges
});
}
}
// 添加处理取消请求的方法
private async handleCancelRequest() {
console.log('收到取消请求');
if (this.currentReader) {
try {
await this.currentReader.cancel();
this.currentReader = null;
console.log('请求已取消');
} catch (error) {
console.error('取消请求时出错:', error);
}
}
}
}

@ -1,18 +1,5 @@
import * as vscode from 'vscode';
import * as path from 'path';
import {callQwenAPI} from "./utils/modelApi";
import {generateCodeDiff,getFileExtension} from "./utils/common";
// 定义全局变量来存储面板状态
let currentSessionHistory: { role: string; content: string }[] = [];
// 存储工作区变更的文件,包含状态信息
let workspaceChanges: {
[key: string]: {
original: string;
modified: string;
status: 'pending' | 'accepted' | 'rejected'
}
} = {};
import {AISidebarViewProvider} from "./AISidebarViewProvider"
export function activate(context: vscode.ExtensionContext) {
console.log('Extension activated');
@ -30,7 +17,7 @@ export function activate(context: vscode.ExtensionContext) {
vscode.window.registerWebviewViewProvider(
'ai-chat-sidebar-view',
provider,
{ webviewOptions: { retainContextWhenHidden: true } }
{webviewOptions: {retainContextWhenHidden: true}}
)
);
@ -64,448 +51,3 @@ export function activate(context: vscode.ExtensionContext) {
// 添加到 subscriptions
context.subscriptions.push(statusBarItem, openWebviewCommand, addToChatCommand);
}
class AISidebarViewProvider implements vscode.WebviewViewProvider {
public static readonly viewType = 'ai-chat-sidebar-view';
private _view?: vscode.WebviewView;
private _extensionPath: string;
private _context: vscode.ExtensionContext;
constructor(extensionPath: string, context: vscode.ExtensionContext) {
this._extensionPath = extensionPath;
this._context = context;
}
public resolveWebviewView(
webviewView: vscode.WebviewView,
context: vscode.WebviewViewResolveContext,
_token: vscode.CancellationToken,
) {
this._view = webviewView;
webviewView.webview.options = {
enableScripts: true,
localResourceRoots: [
vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview'))
]
};
webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
webviewView.webview.onDidReceiveMessage(async (message) => {
switch (message.command) {
case 'ask':
const question = message.text;
const fileContent = message.fileContent;
const history = currentSessionHistory;
currentSessionHistory = [...history, { role: 'user', content: question }];
this._postMessage({ command: 'addMessage', role: 'user', content: question });
try {
const response = await callQwenAPI(question, history, fileContent, this._context);
console.log("response:", response);
const aiMessage = response.choices[0]?.message?.content || '未获取到回复';
// 检查是否包含代码修改
const codeDiff = generateCodeDiff(fileContent, aiMessage);
console.log("codeDiff:",codeDiff)
// 如果有代码差异,保存到工作区变更中
if (codeDiff && fileContent && message.fileContentPath) {
workspaceChanges[message.fileContentPath] = {
original: fileContent,
modified: codeDiff.modifiedCode,
status: 'pending'
};
// 通知 WebView 更新工作区文件列表
this._postMessage({
command: 'updateWorkspaceFiles',
files: workspaceChanges
});
}
// 更新当前会话历史
currentSessionHistory = [...currentSessionHistory, { role: 'assistant', content: aiMessage }];
this._postMessage({
command: 'addMessage',
role: 'ai',
content: aiMessage,
codeDiff: codeDiff // 发送代码差异信息
});
} catch (error: any) {
this._postMessage({ command: 'addMessage', role: 'ai', content: '调用失败:' + error.message });
}
this._postMessage({ command: 'hideLoading' });
break;
case 'selectContextFile':
// 执行文件选择逻辑
const uris = await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
title: "选择一个文件作为上下文"
});
if (uris && uris.length > 0) {
const selectedFileUri = uris[0];
const fileContent = await vscode.workspace.fs.readFile(selectedFileUri);
const decodedContent = new TextDecoder("utf-8").decode(fileContent);
this._postMessage({
command: 'setContextFile',
fileName: selectedFileUri.fsPath,
fileContent: decodedContent
});
}
break;
case 'getFileList':
// 执行获取文件列表逻辑
const workspaceFolders = vscode.workspace.workspaceFolders;
if (workspaceFolders && workspaceFolders.length > 0) {
const fileUris = await vscode.workspace.findFiles('**/*', '**/node_modules/**');
const files = fileUris.map(uri => {
const relativePath = vscode.workspace.asRelativePath(uri);
return {
path: uri.fsPath,
relativePath: relativePath,
name: path.basename(uri.fsPath),
isDirectory: false // 简化处理,实际可以根据扩展名判断
};
});
this._postMessage({
command: 'fileList',
files: files
});
}
break;
case 'selectFileByPath':
// 根据路径选择文件
try {
const fileUri = vscode.Uri.file(message.filePath);
const fileContent = await vscode.workspace.fs.readFile(fileUri);
const decodedContent = new TextDecoder("utf-8").decode(fileContent);
this._postMessage({
command: 'setContextFile',
fileName: fileUri.fsPath,
fileContent: decodedContent
});
} catch (error: any) {
vscode.window.showErrorMessage(`无法读取文件: ${error.message}`);
}
break;
case 'showCodeDiffInEditor':
// 在编辑器中显示代码差异
try {
const originalCode = message.originalCode || '';
const modifiedCode = message.modifiedCode || '';
// 创建原始代码的虚拟文档
const originalUri = vscode.Uri.parse('untitled:original-code.txt');
const originalDoc = await vscode.workspace.openTextDocument(originalUri);
const originalEditor = await vscode.window.showTextDocument(originalDoc, vscode.ViewColumn.One, true);
await originalEditor.edit(edit => {
edit.insert(new vscode.Position(0, 0), originalCode);
});
// 创建修改后代码的虚拟文档
const modifiedUri = vscode.Uri.parse('untitled:modified-code.txt');
const modifiedDoc = await vscode.workspace.openTextDocument(modifiedUri);
const modifiedEditor = await vscode.window.showTextDocument(modifiedDoc, vscode.ViewColumn.Two, true);
await modifiedEditor.edit(edit => {
edit.insert(new vscode.Position(0, 0), modifiedCode);
});
// 使用 VS Code 的 diff 命令显示差异
await vscode.commands.executeCommand(
'vscode.diff',
originalUri,
modifiedUri,
'Code Diff - Original ↔ Modified'
);
} catch (error: any) {
vscode.window.showErrorMessage(`显示代码差异失败: ${error.message}`);
}
break;
case 'acceptChanges':
// 接受代码变更
try {
const { filePath, modifiedContent } = message;
const fileUri = vscode.Uri.file(filePath);
// 将修改后的内容写入文件
const encodedContent = new TextEncoder().encode(modifiedContent);
await vscode.workspace.fs.writeFile(fileUri, encodedContent);
// 更新工作区变更状态
if (workspaceChanges[filePath]) {
workspaceChanges[filePath].status = 'accepted';
}
// 通知 WebView 更新工作区文件列表
this._postMessage({
command: 'updateWorkspaceFiles',
files: workspaceChanges
});
vscode.window.showInformationMessage('代码变更已应用');
} catch (error: any) {
vscode.window.showErrorMessage(`应用代码变更失败: ${error.message}`);
}
break;
case 'rejectChanges':
// 拒绝代码变更
try {
const { filePath } = message;
// 更新工作区变更状态
if (workspaceChanges[filePath]) {
workspaceChanges[filePath].status = 'rejected';
}
// 通知 WebView 更新工作区文件列表
this._postMessage({
command: 'updateWorkspaceFiles',
files: workspaceChanges
});
vscode.window.showInformationMessage('代码变更已拒绝');
} catch (error: any) {
vscode.window.showErrorMessage(`拒绝代码变更失败: ${error.message}`);
}
break;
case 'openWorkspaceFile':
// 打开工作区文件
try {
const { filePath } = message;
const fileUri = vscode.Uri.file(filePath);
// 检查文件是否已经打开
let fileAlreadyOpen = false;
const openedTextDocuments = vscode.workspace.textDocuments;
for (const doc of openedTextDocuments) {
if (doc.uri.fsPath === fileUri.fsPath) {
fileAlreadyOpen = true;
// 激活已打开的文件
await vscode.window.showTextDocument(doc, {
viewColumn: vscode.ViewColumn.One // 切换到已打开的文件
});
break;
}
}
// 如果文件未打开,则在新窗口打开
if (!fileAlreadyOpen) {
await vscode.window.showTextDocument(fileUri, {
viewColumn: vscode.ViewColumn.One // 在第一列打开文件
});
}
} catch (error: any) {
vscode.window.showErrorMessage(`打开文件失败: ${error.message}`);
}
break;
case 'getCodeChanges':
// 获取代码变更
this._postMessage({
command: 'codeChanges',
changes: workspaceChanges
});
break;
case 'createNewFile':
// 创建新文件功能
try {
const { fileName, content, language } = message;
// 获取根工作区路径
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
vscode.window.showErrorMessage('未找到工作区');
return;
}
// 根据语言确定文件扩展名
const fileExtension = getFileExtension(language);
const fullFileName = fileName.endsWith(fileExtension) ? fileName : `${fileName}${fileExtension}`;
// 创建文件路径(在根目录下)
const filePath = path.join(workspaceFolders[0].uri.fsPath, fullFileName);
const fileUri = vscode.Uri.file(filePath);
// 将内容写入文件
const encodedContent = new TextEncoder().encode(content);
await vscode.workspace.fs.writeFile(fileUri, encodedContent);
// 在编辑器中打开新创建的文件
await vscode.window.showTextDocument(fileUri, {
viewColumn: vscode.ViewColumn.One
});
vscode.window.showInformationMessage(`文件 ${fullFileName} 已创建`);
} catch (error: any) {
vscode.window.showErrorMessage(`生成代码文件失败: ${error.message}`);
}
break;
case 'restoreState':
// 恢复面板状态
currentSessionHistory = message.history || [];
// 恢复显示历史消息
currentSessionHistory.forEach(msg => {
this._postMessage({
command: 'addMessage',
role: msg.role,
content: msg.content,
codeDiff: null
});
});
// 恢复工作区变更
if (message.workspaceChanges) {
this._postMessage({
command: 'updateWorkspaceFiles',
files: message.workspaceChanges
});
}
break;
}
});
}
public sendSelectedCode(code: string) {
if (this._view) {
this._view.show?.(true);
this._postMessage({
command: 'addToInput',
role: 'user',
content: code
});
}
}
private _getHtmlForWebview(webview: vscode.Webview) {
const styleUri = webview.asWebviewUri(
vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview', 'style.css'))
);
const scriptUri = webview.asWebviewUri(
vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview', 'script.js'))
);
const highlightScriptUri = webview.asWebviewUri(
vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview', 'highlight.min.js'))
);
const highlightStyleUri = webview.asWebviewUri(
vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview', 'styles', 'atom-one-dark.css'))
);
const markedScriptUri = webview.asWebviewUri(
vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview', 'marked.min.js'))
);
return getWebviewContent(styleUri, scriptUri, highlightScriptUri, highlightStyleUri, markedScriptUri);
}
private _postMessage(message: any) {
if (this._view) {
this._view.webview.postMessage(message);
}
}
}
function getWebviewContent(styleUri: vscode.Uri, scriptUri: vscode.Uri,highlightScriptUri:vscode.Uri,highlightStyleUri:vscode.Uri,markedScriptUri:vscode.Uri): string {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>AI Chat</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="${styleUri}" />
<link href="${highlightStyleUri}" rel="stylesheet" />
</head>
<body>
<div class="chat-container">
<!---->
<div id="chat-box"></div>
<!---->
<div class="context-container">
<button id="select-context-btn" class="select-context-btn" title="选择上下文文件">+</button>
<div id="context-placeholder" class="context-placeholder">
<span class="placeholder-text"></span>
</div>
<div id="context-tab" class="context-tab hidden">
<span id="context-file-name" class="file-name"></span>
<button id="close-context-btn" class="close-btn" title="清除上下文">×</button>
</div>
</div>
<!-- -->
<div class="workspace-container" style="display: none;">
<h3></h3>
<div id="workspace-files" class="workspace-files">
<div class="no-changes"></div>
</div>
</div>
<!-- -->
<div id="file-browser-modal" class="modal hidden">
<div class="modal-content">
<div class="modal-header">
<h3></h3>
<span id="close-modal" class="close-modal">&times;</span>
</div>
<div class="modal-body">
<div class="file-search">
<input type="text" id="file-search-input" placeholder="搜索文件..." />
</div>
<div id="file-list" class="file-list">
<div class="loading">...</div>
</div>
</div>
</div>
</div>
<!-- -->
<div id="diff-modal" class="modal hidden">
<div class="modal-content diff-modal-content">
<div class="modal-header">
<h3></h3>
<span id="close-diff-modal" class="close-modal">&times;</span>
</div>
<div class="modal-body">
<div id="diff-content" class="diff-content"></div>
<div class="diff-actions">
<button id="accept-changes-btn" class="diff-action-btn accept"></button>
<button id="reject-changes-btn" class="diff-action-btn reject"></button>
</div>
</div>
</div>
</div>
<!-- -->
<form id="chat-form">
<input type="text" id="user-input" placeholder="输入你的问题..." required />
<button type="submit"></button>
</form>
<div id="loading" class="hidden">AI ...</div>
</div>
<script src="${scriptUri}"></script>
<script src="${highlightScriptUri}"></script>
<script src="${markedScriptUri}"></script>
</body>
</html>`;
}

@ -81,3 +81,24 @@ export function getFileExtension(language: string): string {
return languageMap[language?.toLowerCase()] || '.txt';
}
// 简单的语言检测
export function detectLanguage(code) {
// 可以根据代码特征进行简单判断
if (code.includes('import React') || code.includes('from react')) {
return 'javascript'; // React代码
} else if (code.includes('public class') || code.includes('private static')) {
return 'java';
} else if (code.includes('def ') && code.includes(':')) {
return 'python';
} else if (code.includes('function ') || code.includes('const ') || code.includes('let ')) {
return 'javascript';
} else if (code.includes('interface ') && code.includes('export ')) {
return 'typescript';
} else if (code.includes('<?php')) {
return 'php';
} else if (code.includes('using System')) {
return 'csharp';
}
return 'text'; // 默认
}

@ -1,13 +1,80 @@
import vscode from "vscode";
import path from "path";
// 获取API配置的函数
function getApiConfig(context: vscode.ExtensionContext) {
// 从用户配置中获取设置
const config = vscode.workspace.getConfiguration('ai-chat.api');
const userUrl = config.get<string>('url');
const userBaseUrl = config.get<string>('baseUrl');
const userApiKey = config.get<string>('key');
// 如果用户配置了完整URL优先使用
if (userUrl) {
return {
url: userUrl,
apiKey: userApiKey || 'dev-token'
};
}
// 如果用户配置了基础URL使用基础URL加上默认路径
if (userBaseUrl) {
return {
url: `${userBaseUrl}/comp/api/v1/chat/completions-stream`,
apiKey: userApiKey || 'dev-token'
};
}
// 检测code-server环境
const codeServerUrl = process.env.CODE_SERVER_URL;
if (codeServerUrl) {
// 在code-server环境中尝试使用相对路径或者基于当前地址的API地址
try {
const baseUrl = new URL(codeServerUrl);
return {
url: `${baseUrl.origin}/comp/api/v1/chat/completions-stream`,
apiKey: userApiKey || 'dev-token'
};
} catch (e) {
// 如果解析失败,使用默认地址
}
}
// 检查CODE_SERVER_CONFIG环境变量
const codeServerConfig = process.env.CODE_SERVER_CONFIG;
if (codeServerConfig) {
try {
// 尝试从配置中解析bindAddr
const configObj = JSON.parse(codeServerConfig);
if (configObj.bindAddr) {
const [host, port] = configObj.bindAddr.split(':');
if (host && port) {
return {
url: `http://${host}:${port}/comp/api/v1/chat/completions-stream`,
apiKey: userApiKey || 'dev-token'
};
}
}
} catch (e) {
// 解析失败则继续使用默认配置
}
}
// 默认配置
return {
url: 'https://p13-ai.ngsk.tech:7001/comp/api/v1/chat/completions-stream',
apiKey: userApiKey || 'dev-token'
};
}
export async function callQwenAPI(
question: string,
history: any[],
fileContent: string,
context: vscode.ExtensionContext
context: vscode.ExtensionContext,
filename: string = ""
): Promise<any> {
const apiKey = 'dev-token';
const url = 'https://aicomp.ngsk.tech:7001/api/v1/chat/completions';
const { url, apiKey } = getApiConfig(context);
const messages = [
{
@ -17,34 +84,41 @@ export async function callQwenAPI(
...history,
{
role: 'user',
content: `【当前文件内容】:
\`\`\`${fileContent}\`\`\`
:
${question}`
content: question
}
];
console.log("messages:",messages)
console.log("messages:", messages)
try {
const response = await fetch(url, {
const params = {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
// 'Accept': 'text/event-stream'
},
body: JSON.stringify({
model: 'qwen2.5-local',
messages
model: 'Qwen2_5_Coder',
messages,
context_file: {
filename: filename,
section_type: "code",
content: fileContent
}
})
});
}
console.log("params:", JSON.stringify(params))
// @ts-ignore
const response = await fetch(url, params);
if (!response.ok) {
console.log("请求失败:", response)
throw new Error('API 请求失败');
}
return await response.json();
return response;
} catch (error: any) {
console.error(error);
throw error;

@ -0,0 +1,88 @@
import vscode from "vscode";
export function getWebviewContent(styleUri: vscode.Uri, scriptUri: vscode.Uri,highlightScriptUri:vscode.Uri,highlightStyleUri:vscode.Uri,markedScriptUri:vscode.Uri): string {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>AI Chat</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="${styleUri}" />
<link href="${highlightStyleUri}" rel="stylesheet" />
</head>
<body>
<div class="chat-container">
<!---->
<div id="chat-box"></div>
<!---->
<div class="context-container">
<button id="select-context-btn" class="select-context-btn" title="选择上下文文件">+</button>
<div id="context-placeholder" class="context-placeholder">
<span class="placeholder-text"></span>
</div>
<div id="context-tab" class="context-tab hidden">
<span id="context-file-name" class="file-name"></span>
<button id="close-context-btn" class="close-btn" title="清除上下文">×</button>
</div>
</div>
<!-- -->
<div class="workspace-container" style="display: none;">
<h3></h3>
<div id="workspace-files" class="workspace-files">
<div class="no-changes"></div>
</div>
</div>
<!-- -->
<div id="file-browser-modal" class="modal hidden">
<div class="modal-content">
<div class="modal-header">
<h3></h3>
<span id="close-modal" class="close-modal">&times;</span>
</div>
<div class="modal-body">
<div class="file-search">
<input type="text" id="file-search-input" placeholder="搜索文件..." />
</div>
<div id="file-list" class="file-list">
<div class="loading">...</div>
</div>
</div>
</div>
</div>
<!-- -->
<div id="diff-modal" class="modal hidden">
<div class="modal-content diff-modal-content">
<div class="modal-header">
<h3></h3>
<span id="close-diff-modal" class="close-modal">&times;</span>
</div>
<div class="modal-body">
<div id="diff-content" class="diff-content"></div>
<div class="diff-actions">
<button id="accept-changes-btn" class="diff-action-btn accept"></button>
<button id="reject-changes-btn" class="diff-action-btn reject"></button>
</div>
</div>
</div>
</div>
<!-- -->
<form id="chat-form">
<input type="text" id="user-input" placeholder="输入你的问题..." required />
<button type="submit"></button>
</form>
<div id="loading" class="hidden">AI ...</div>
</div>
<script src="${scriptUri}"></script>
<script src="${highlightScriptUri}"></script>
<script src="${markedScriptUri}"></script>
</body>
</html>`;
}
Loading…
Cancel
Save