You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

656 lines
24 KiB
JavaScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

let selectedCodeForContext = '';
let contextFilePath = ''; // 存储当前上下文文件路径
let streamingMessageElement = null; // 用于存储当前流式消息的DOM元素
let streamingMessageContent = ''; // 用于存储当前流式消息的内容
let currentReader = null; // 用于存储当前流的reader以便可以取消
let isRequestInProgress = false; // 标记是否有请求正在进行
let currentSessionHistory = []; // 存储当前会话历史
const vscode = acquireVsCodeApi();
document.addEventListener('DOMContentLoaded', () => {
const chatForm = document.getElementById('chat-form');
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');
if (workspaceContainer) {
workspaceContainer.style.display = 'none';
}
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,
fileContent: selectedCodeForContext,
fileContentPath: contextFilePath // 添加文件路径
});
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;
const markdownPatterns = [
/^#/m, // 标题 #
/\*\*[^*]+\*\*/g, // 加粗 **text**
/__[^_]+__/g, // 加粗 __text__
/^- /gm, // 无序列表
/^\* /gm, // 无序列表
/^\d+\. /gm, // 有序列表
/```/g // 代码块
];
return markdownPatterns.some(pattern => pattern.test(content));
}
// 处理添加消息
function addMessage(role, content, codeDiffs) {
const msgDiv = document.createElement('div');
msgDiv.className = `message ${role}`;
// 渲染内容
if (isMarkdown(content)) {
console.log("content:", content);
msgDiv.innerHTML = marked.parse(content);
console.log("msgDiv.innerHTML:", msgDiv.innerHTML);
} else {
const div = document.createElement('div');
div.textContent = content;
msgDiv.appendChild(div);
}
// 如果是AI消息且包含代码块为每个代码块添加按钮
if (role === 'ai') {
const codeBlocks = msgDiv.querySelectorAll('pre code');
// 确保 codeDiffs 是数组
const diffs = Array.isArray(codeDiffs) ? codeDiffs : [];
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);
// 如果有代码差异,也为每个代码块添加查看差异按钮
if (diffs[index]) {
const diffButton = document.createElement('button');
diffButton.className = 'diff-button';
diffButton.textContent = `查看代码差异`;
diffButton.onclick = () => showCodeDiff(diffs[index]);
// 将差异按钮插入到创建文件按钮后面
createFileButton.after(diffButton);
}
});
}
chatBox.appendChild(msgDiv);
// 高亮代码块
const codeBlocks = msgDiv.querySelectorAll('pre code');
codeBlocks.forEach(block => {
if (hljs) {
hljs.highlightElement(block);
}
});
// 滚动到底部
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 = '';
}
isRequestInProgress = false
// 保存消息到历史记录
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();
}
// 显示代码差异
function showCodeDiff(codeDiff) {
const diffModal = document.getElementById('diff-modal');
const diffContent = document.getElementById('diff-content');
diffContent.innerHTML = '';
// 显示添加的行
if (codeDiff.added && codeDiff.added.length > 0) {
codeDiff.added.forEach((line, index) => {
const diffRow = document.createElement('div');
diffRow.className = 'diff-row';
// 修复行号显示问题
const lineNumber = document.createElement('div');
lineNumber.className = 'diff-line-number';
lineNumber.textContent = index + 1; // 使用索引+1作为行号
const diffLine = document.createElement('div');
diffLine.className = 'diff-line diff-added';
diffLine.textContent = `+ ${line}`;
diffRow.appendChild(lineNumber);
diffRow.appendChild(diffLine);
diffContent.appendChild(diffRow);
});
}
// 显示删除的行
if (codeDiff.removed && codeDiff.removed.length > 0) {
codeDiff.removed.forEach((line, index) => {
const diffRow = document.createElement('div');
diffRow.className = 'diff-row';
// 修复行号显示问题
const lineNumber = document.createElement('div');
lineNumber.className = 'diff-line-number';
lineNumber.textContent = index + 1; // 使用索引+1作为行号
const diffLine = document.createElement('div');
diffLine.className = 'diff-line diff-removed';
diffLine.textContent = `- ${line}`;
diffRow.appendChild(lineNumber);
diffRow.appendChild(diffLine);
diffContent.appendChild(diffRow);
});
}
// 添加接受和拒绝按钮的事件监听
document.getElementById('accept-changes-btn').onclick = () => acceptChanges(codeDiff.modifiedCode);
document.getElementById('reject-changes-btn').onclick = () => rejectChanges();
diffModal.classList.remove('hidden');
}
// 接受代码变更
function acceptChanges(modifiedCode) {
if (contextFilePath) {
// 发送消息到插件以接受变更
vscode.postMessage({
command: 'acceptChanges',
filePath: contextFilePath,
modifiedContent: modifiedCode
});
// 关闭模态框
document.getElementById('diff-modal').classList.add('hidden');
}
}
// 拒绝代码变更
function rejectChanges() {
if (contextFilePath) {
// 发送消息到插件以拒绝变更
vscode.postMessage({
command: 'rejectChanges',
filePath: contextFilePath
});
// 关闭模态框
document.getElementById('diff-modal').classList.add('hidden');
}
}
// 更新工作区文件列表
function updateWorkspaceFiles(files) {
const workspaceContainer = document.querySelector('.workspace-container');
const workspaceFilesContainer = document.getElementById('workspace-files');
// 如果没有文件变更,则隐藏工作区
if (!files || Object.keys(files).length === 0) {
workspaceContainer.style.display = 'none';
return;
}
// 显示工作区
workspaceContainer.style.display = 'block';
workspaceFilesContainer.innerHTML = '';
Object.keys(files).forEach(filePath => {
const fileChange = files[filePath];
const fileItem = document.createElement('div');
fileItem.className = 'workspace-file-item';
// 获取文件名
const fileName = filePath.split(/[\/\\]/).pop();
// 根据状态设置不同的显示样式
let statusText = '';
let statusClass = '';
switch (fileChange.status) {
case 'accepted':
statusText = '✓ 已接受';
statusClass = 'status-accepted';
break;
case 'rejected':
statusText = '✗ 已拒绝';
statusClass = 'status-rejected';
break;
default:
statusText = '待处理';
statusClass = 'status-pending';
}
fileItem.innerHTML = `
<span class="file-icon">📄</span>
<span class="file-name" title="${filePath}">${fileName}</span>
<span class="file-status ${statusClass}">${statusText}</span>
`;
fileItem.addEventListener('click', () => {
// 发送消息到插件以打开文件
vscode.postMessage({
command: 'openWorkspaceFile',
filePath: filePath
});
});
workspaceFilesContainer.appendChild(fileItem);
});
}
window.addEventListener('message', (event) => {
const message = event.data;
console.log(message)
if (message.command === 'addMessage') {
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') {
userInput.value = message.content; // 插入选中内容到输入框
selectedCodeForContext = message.content;
userInput.focus(); // 自动聚焦输入框
} else if (message.command === 'fileList') {
const fileListContainer = document.getElementById('file-list');
if (message.error) {
fileListContainer.innerHTML = `<div class="loading">错误: ${message.error}</div>`;
return;
}
if (!message.files || message.files.length === 0) {
fileListContainer.innerHTML = '<div class="loading">未找到文件</div>';
return;
}
// 渲染文件列表
fileListContainer.innerHTML = '';
message.files.forEach(file => {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
fileItem.innerHTML = ` <span class="file-icon">${file.isDirectory ? '📁' : '📄'}</span>
<span class="file-name" title="${file.relativePath}">${file.relativePath}</span>
`;
fileItem.addEventListener('click', () => {
// 选择文件
vscode.postMessage({
command: 'selectFileByPath',
filePath: file.path
});
// 关闭模态框
document.getElementById('file-browser-modal').classList.add('hidden');
// 清空文件搜索输入框
document.getElementById('file-search-input').value = '';
// 保存当前上下文文件路径
contextFilePath = file.path;
});
fileListContainer.appendChild(fileItem);
});
} else if (message.command === 'setContextFile') {
const contextPlaceholder = document.getElementById('context-placeholder');
const contextTab = document.getElementById('context-tab');
const fileNameElement = document.getElementById('context-file-name');
const fullPath = message.fileName;
const fileName = fullPath.split(/[\/\\]/).pop();
fileNameElement.innerText = fileName;
fileNameElement.title = fullPath;
selectedCodeForContext = message.fileContent;
contextFilePath = fullPath; // 保存上下文文件路径
// 隐藏占位符,显示文件标签
contextPlaceholder.classList.add('hidden');
contextTab.classList.remove('hidden');
} else if (message.command === 'updateWorkspaceFiles') {
// 更新工作区文件列表
updateWorkspaceFiles(message.files);
} else if (message.command === 'restoreState') {
// 恢复面板状态
currentSessionHistory = message.history || [];
// 恢复显示历史消息
currentSessionHistory.forEach(msg => {
addMessage(msg.role, msg.content, null);
});
// 恢复工作区变更
if (message.workspaceChanges) {
updateWorkspaceFiles(message.workspaceChanges);
}
}
});
// 打开文件浏览器
const selectContextBtn = document.getElementById('select-context-btn');
if (selectContextBtn) {
selectContextBtn.addEventListener('click', () => {
// 显示模态框
const fileBrowserModal = document.getElementById('file-browser-modal');
if (fileBrowserModal) {
fileBrowserModal.classList.remove('hidden');
}
// 请求文件列表
vscode.postMessage({
command: 'getFileList'
});
});
}
// 关闭模态框
document.getElementById('close-modal').addEventListener('click', () => {
document.getElementById('file-browser-modal').classList.add('hidden');
});
// 点击模态框外部关闭
document.getElementById('file-browser-modal').addEventListener('click', (e) => {
if (e.target.id === 'file-browser-modal') {
document.getElementById('file-browser-modal').classList.add('hidden');
}
});
// 文件搜索功能
document.getElementById('file-search-input').addEventListener('input', (e) => {
const searchTerm = e.target.value.toLowerCase();
const fileItems = document.querySelectorAll('.file-item');
fileItems.forEach(item => {
const fileName = item.querySelector('.file-name').textContent.toLowerCase();
if (fileName.includes(searchTerm)) {
item.style.display = '';
} else {
item.style.display = 'none';
}
});
});
// 添加关闭按钮事件监听
document.getElementById('close-context-btn').addEventListener('click', () => {
const contextPlaceholder = document.getElementById('context-placeholder');
const contextTab = document.getElementById('context-tab');
contextTab.classList.add('hidden');
contextPlaceholder.classList.remove('hidden');
selectedCodeForContext = '';
contextFilePath = '';
});
// 关闭差异模态框
document.getElementById('close-diff-modal').addEventListener('click', () => {
document.getElementById('diff-modal').classList.add('hidden');
});
// 点击模态框外部关闭
document.getElementById('diff-modal').addEventListener('click', (e) => {
if (e.target.id === 'diff-modal') {
document.getElementById('diff-modal').classList.add('hidden');
}
});
// 从代码创建新文件
function createNewFileFromCode(codeContent, index) {
// 简单的语言检测
const language = detectLanguage(codeContent);
// 请求创建文件
const fileName = `new_file_${index + 1}${getFileExtension(language)}`;
vscode.postMessage({
command: 'createNewFile',
fileName: fileName,
content: codeContent,
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';
}
});