503 lines
14 KiB
TypeScript
503 lines
14 KiB
TypeScript
import { getWaveformPreviewContent } from "./waveformPreviewContent";
|
||
import {
|
||
getModelSelectorContent,
|
||
getModelSelectorStyles,
|
||
getModelSelectorScript,
|
||
} from "./modelSelector";
|
||
import {
|
||
getModeSelectorContent,
|
||
getModeSelectorStyles,
|
||
getModeSelectorScript,
|
||
} from "./agentModeSelector";
|
||
import {
|
||
getContextButtonContent,
|
||
getContextButtonStyles,
|
||
getContextButtonScript,
|
||
} from "./contextButton";
|
||
import {
|
||
getContextDisplayContent,
|
||
getContextDisplayStyles,
|
||
getContextDisplayScript,
|
||
} from "./contextDisplay";
|
||
import {
|
||
getContextCompressContent,
|
||
getContextCompressStyles,
|
||
getContextCompressScript,
|
||
} from "./contextCompress";
|
||
import {
|
||
getFilePathTagStyles,
|
||
getFilePathTagScript,
|
||
} from "./filePathTag";
|
||
import {
|
||
getOptimizeButtonContent,
|
||
getOptimizeButtonStyles,
|
||
getOptimizeButtonScript,
|
||
} from "./optimizeButton";
|
||
import {
|
||
getExampleShowcaseContent,
|
||
getExampleShowcaseStyles,
|
||
getExampleShowcaseScript,
|
||
} from "./exampleShowcase";
|
||
import {
|
||
getChangePanelContent,
|
||
getChangePanelStyles,
|
||
getChangePanelScript,
|
||
} from "./changePanel";
|
||
import { sendIconSvg, stopIconSvg } from "../constants/toolIcons";
|
||
|
||
/**
|
||
* 获取输入区域的 HTML 内容
|
||
*/
|
||
export function getInputAreaContent(
|
||
autoIcon: string = "",
|
||
liteIcon: string = "",
|
||
syIcon: string = "",
|
||
maxIcon: string = ""
|
||
): string {
|
||
return `
|
||
<div class="input-area centered" id="inputArea">
|
||
<div class="input-group">
|
||
<div class="input-wrapper">
|
||
<!-- 代码变更面板 -->
|
||
${getChangePanelContent()}
|
||
<!-- 顶部工具栏 -->
|
||
<div class="input-top-toolbar">
|
||
${getContextButtonContent()}
|
||
</div>
|
||
<!-- 上下文显示区域 -->
|
||
${getContextDisplayContent()}
|
||
<textarea
|
||
id="messageInput"
|
||
placeholder="输入您的问题,按 Enter 发送,Shift + Enter 换行..."
|
||
onkeydown="if(event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendMessage(); }"
|
||
></textarea>
|
||
<div class="input-bottom-row">
|
||
<div class="mode-selector">
|
||
${getModeSelectorContent()}
|
||
${getModelSelectorContent(autoIcon, liteIcon, syIcon, maxIcon)}
|
||
</div>
|
||
<div class="input-actions">
|
||
${getContextCompressContent()}
|
||
${getOptimizeButtonContent()}
|
||
<button id="sendButton" onclick="handleSendOrStop()">
|
||
${sendIconSvg}
|
||
<span style="display: none;">${stopIconSvg}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 展示区域:案例和 Web 端链接 -->
|
||
${getExampleShowcaseContent()}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* 获取输入区域的样式
|
||
*/
|
||
export function getInputAreaStyles(): string {
|
||
return `
|
||
${getModeSelectorStyles()}
|
||
${getModelSelectorStyles()}
|
||
${getContextButtonStyles()}
|
||
${getContextDisplayStyles()}
|
||
${getFilePathTagStyles()}
|
||
${getContextCompressStyles()}
|
||
${getOptimizeButtonStyles()}
|
||
${getExampleShowcaseStyles()}
|
||
${getChangePanelStyles()}
|
||
.input-area {
|
||
border-top: 1px solid var(--vscode-panel-border);
|
||
padding-top: 15px;
|
||
flex-shrink: 0;
|
||
transition: all 0.3s ease;
|
||
}
|
||
/* 居中模式:未发起对话时 */
|
||
.input-area.centered {
|
||
position: absolute;
|
||
top: 60%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
width: calc(100% - 40px);
|
||
max-width: 800px;
|
||
border-top: none;
|
||
padding-top: 0;
|
||
}
|
||
/* 底部模式:发起对话后 */
|
||
.input-area.bottom {
|
||
position: relative;
|
||
transform: none;
|
||
}
|
||
.input-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
background: var(--vscode-input-background);
|
||
border: 1px solid var(--vscode-input-border);
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15), 0 2px 6px rgba(0, 0, 0, 0.1);
|
||
transition: all 0.3s ease;
|
||
}
|
||
.input-group:hover {
|
||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2), 0 3px 8px rgba(0, 0, 0, 0.15);
|
||
}
|
||
.input-group:focus-within {
|
||
border-color: var(--vscode-focusBorder);
|
||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25), 0 3px 10px rgba(0, 0, 0, 0.2);
|
||
}
|
||
.input-wrapper {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
width: 100%;
|
||
}
|
||
/* 顶部工具栏样式 */
|
||
.input-top-toolbar {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
gap: 12px;
|
||
}
|
||
.input-bottom-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
margin-bottom: -17px;
|
||
}
|
||
.mode-selector {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
position: relative;
|
||
}
|
||
.input-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
/* Tooltip 样式 */
|
||
.tooltip {
|
||
position: relative;
|
||
display: inline-block;
|
||
}
|
||
.tooltip .tooltiptext {
|
||
visibility: hidden;
|
||
width: auto;
|
||
background: #1e1e1e;
|
||
color: #ffffff;
|
||
text-align: center;
|
||
border-radius: 6px;
|
||
padding: 6px 12px;
|
||
position: absolute;
|
||
z-index: 1000;
|
||
bottom: 150%;
|
||
left: 50%;
|
||
transform: translateX(-50%) translateY(10px);
|
||
opacity: 0;
|
||
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6), 0 2px 4px rgba(0, 0, 0, 0.3);
|
||
white-space: nowrap;
|
||
letter-spacing: 0.3px;
|
||
}
|
||
.tooltip .tooltiptext::after {
|
||
content: "";
|
||
position: absolute;
|
||
top: 100%;
|
||
left: 50%;
|
||
margin-left: -6px;
|
||
border-width: 6px;
|
||
border-style: solid;
|
||
border-color: #1e1e1e transparent transparent transparent;
|
||
}
|
||
.tooltip .tooltiptext::before {
|
||
content: "";
|
||
position: absolute;
|
||
top: 100%;
|
||
left: 50%;
|
||
margin-left: -7px;
|
||
border-width: 7px;
|
||
border-style: solid;
|
||
border-color: rgba(255, 255, 255, 0.2) transparent transparent transparent;
|
||
z-index: -1;
|
||
}
|
||
.tooltip:hover .tooltiptext {
|
||
visibility: visible;
|
||
opacity: 1;
|
||
transform: translateX(-50%) translateY(0);
|
||
}
|
||
textarea {
|
||
width: 100%;
|
||
padding: 10px;
|
||
background: transparent;
|
||
color: var(--vscode-input-foreground);
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-family: inherit;
|
||
resize: none;
|
||
min-height: 40px;
|
||
max-height: 200px;
|
||
outline: none;
|
||
box-sizing: border-box;
|
||
overflow-y: auto;
|
||
line-height: 1.5;
|
||
}
|
||
textarea:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
background: rgba(128, 128, 128, 0.1);
|
||
}
|
||
/* 简洁的滚动条样式 */
|
||
textarea::-webkit-scrollbar {
|
||
width: 8px;
|
||
}
|
||
textarea::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
textarea::-webkit-scrollbar-thumb {
|
||
background: rgba(128, 128, 128, 0.5);
|
||
border-radius: 4px;
|
||
}
|
||
textarea::-webkit-scrollbar-button {
|
||
display: none;
|
||
}
|
||
button {
|
||
padding: 0 20px;
|
||
background: var(--vscode-button-background);
|
||
color: var(--vscode-button-foreground);
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
transition: background 0.2s ease;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
button:hover {
|
||
background: var(--vscode-button-hoverBackground);
|
||
}
|
||
/* 发送按钮状态样式 */
|
||
#sendButton {
|
||
position: relative;
|
||
min-width: 32px;
|
||
padding: 6px 8px;
|
||
}
|
||
#sendButton svg {
|
||
width: 14px;
|
||
height: 14px;
|
||
display: block;
|
||
}
|
||
#sendButton.sending {
|
||
background: var(--vscode-button-background);
|
||
}
|
||
#sendButton.sending:hover {
|
||
background: var(--vscode-button-hoverBackground);
|
||
}
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* 获取输入区域的脚本
|
||
*/
|
||
export function getInputAreaScript(): string {
|
||
return `
|
||
// 注意:getModeSelectorScript() 已在 webviewContent.ts 开头加载,这里不再重复加载
|
||
${getModelSelectorScript()}
|
||
${getContextDisplayScript()}
|
||
${getContextButtonScript()}
|
||
${getContextCompressScript()}
|
||
${getOptimizeButtonScript()}
|
||
${getChangePanelScript()}
|
||
${getFilePathTagScript()}
|
||
|
||
// 对话状态管理
|
||
let isConversationActive = false;
|
||
let hasMessages = false; // 是否已有消息
|
||
|
||
// 工作区检测状态
|
||
let hasCheckedWorkspace = false; // 是否已经检测过工作区
|
||
let hasWorkspace = true; // 工作区状态
|
||
|
||
${getExampleShowcaseScript()}
|
||
|
||
// 切换输入框布局模式
|
||
function updateInputAreaLayout() {
|
||
const inputArea = document.getElementById('inputArea');
|
||
if (!inputArea) return;
|
||
|
||
if (hasMessages) {
|
||
// 有消息时,移到底部
|
||
inputArea.classList.remove('centered');
|
||
inputArea.classList.add('bottom');
|
||
} else {
|
||
// 无消息时,居中显示
|
||
inputArea.classList.add('centered');
|
||
inputArea.classList.remove('bottom');
|
||
}
|
||
}
|
||
|
||
// 自动调整 textarea 高度
|
||
function autoResizeTextarea() {
|
||
if (messageInput) {
|
||
messageInput.style.height = 'auto';
|
||
messageInput.style.height = messageInput.scrollHeight + 'px';
|
||
}
|
||
}
|
||
|
||
// 监听输入事件,自动调整高度
|
||
if (messageInput) {
|
||
messageInput.addEventListener('input', autoResizeTextarea);
|
||
|
||
// 监听点击事件,检测工作区状态、试用期过期和邀请码验证状态
|
||
messageInput.addEventListener('focus', () => {
|
||
if (!hasCheckedWorkspace) {
|
||
hasCheckedWorkspace = true;
|
||
vscode.postMessage({ command: 'checkWorkspace' });
|
||
}
|
||
// 检查试用期是否过期
|
||
vscode.postMessage({ command: 'checkTrialExpiration' });
|
||
// 检查邀请码验证状态
|
||
vscode.postMessage({ command: 'checkInvitationCode' });
|
||
});
|
||
|
||
// 初始化时调整一次高度
|
||
autoResizeTextarea();
|
||
}
|
||
|
||
// 切换发送按钮状态
|
||
function setSendButtonState(isSending) {
|
||
const sendButton = document.getElementById('sendButton');
|
||
const children = sendButton.children;
|
||
const sendIconContainer = children[0]; // 第一个子元素是发送图标的 SVG
|
||
const stopIconContainer = children[1]; // 第二个子元素是包含暂停图标的 span
|
||
|
||
if (isSending) {
|
||
sendButton.classList.add('sending');
|
||
sendIconContainer.style.display = 'none';
|
||
stopIconContainer.style.display = 'block';
|
||
isConversationActive = true;
|
||
// 禁用输入框
|
||
messageInput.disabled = true;
|
||
messageInput.placeholder = '正在处理中,请稍候...';
|
||
} else {
|
||
sendButton.classList.remove('sending');
|
||
sendIconContainer.style.display = 'block';
|
||
stopIconContainer.style.display = 'none';
|
||
isConversationActive = false;
|
||
// 启用输入框
|
||
messageInput.disabled = false;
|
||
messageInput.placeholder = '输入您的问题,按 Enter 发送,Shift + Enter 换行...';
|
||
}
|
||
}
|
||
|
||
// 处理发送或停止
|
||
function handleSendOrStop() {
|
||
if (isConversationActive) {
|
||
// 当前正在对话,执行停止操作
|
||
vscode.postMessage({ command: 'abortDialog' });
|
||
setSendButtonState(false);
|
||
} else {
|
||
// 当前未在对话,执行发送操作
|
||
sendMessage();
|
||
}
|
||
}
|
||
|
||
function sendMessage() {
|
||
const text = messageInput.value.trim();
|
||
if (!text) return;
|
||
|
||
// 如果正在对话中,阻止发送新消息
|
||
if (isConversationActive) {
|
||
return;
|
||
}
|
||
|
||
// 检查工作区状态
|
||
if (!hasWorkspace) {
|
||
// 如果没有工作区,阻止发送并清空输入框
|
||
messageInput.value = '';
|
||
autoResizeTextarea();
|
||
return;
|
||
}
|
||
|
||
const mode = getCurrentMode(); // 从模式选择器组件获取当前模式
|
||
const model = getCurrentModel(); // 从模型选择器组件获取当前模型
|
||
const planMode = document.getElementById('planToggle')?.checked || false;
|
||
|
||
// 获取上下文项
|
||
const contextItems = window.getContextItems ? window.getContextItems() : [];
|
||
|
||
// 构建显示消息:如果有上下文项,添加路径前缀
|
||
let displayText = text;
|
||
if (contextItems.length > 0) {
|
||
const contextPaths = contextItems
|
||
.map(item => item.displayPath || item.path)
|
||
.join(' ');
|
||
if (contextPaths) {
|
||
displayText = contextPaths + ' ' + text;
|
||
}
|
||
}
|
||
|
||
addMessage(displayText, 'user');
|
||
|
||
// 重置分段消息容器,强制下次创建新容器
|
||
currentSegmentedMessage = null;
|
||
|
||
// 标记已有消息,切换布局到底部
|
||
hasMessages = true;
|
||
updateInputAreaLayout();
|
||
|
||
// 切换按钮为暂停状态
|
||
setSendButtonState(true);
|
||
|
||
vscode.postMessage({
|
||
command: 'sendMessage',
|
||
text: text,
|
||
mode: mode,
|
||
model: model,
|
||
planMode: planMode,
|
||
contextItems: contextItems
|
||
});
|
||
messageInput.value = '';
|
||
autoResizeTextarea(); // 重置输入框高度
|
||
messageInput.focus();
|
||
|
||
// 清空上下文项
|
||
if (window.clearContextItems) {
|
||
window.clearContextItems();
|
||
}
|
||
|
||
// 重置优化状态
|
||
resetOptimizeButton();
|
||
}
|
||
|
||
// 全局函数:重置输入框布局(用于清空对话时)
|
||
window.resetInputAreaLayout = function() {
|
||
hasMessages = false;
|
||
updateInputAreaLayout();
|
||
};
|
||
|
||
// 全局函数:检查是否有消息(用于页面加载时)
|
||
window.checkMessagesAndUpdateLayout = function() {
|
||
const messagesContainer = document.getElementById('messages');
|
||
if (messagesContainer) {
|
||
const messageElements = messagesContainer.querySelectorAll('.message');
|
||
hasMessages = messageElements.length > 0;
|
||
updateInputAreaLayout();
|
||
}
|
||
};
|
||
|
||
// 页面加载时检查消息状态
|
||
setTimeout(() => {
|
||
if (window.checkMessagesAndUpdateLayout) {
|
||
window.checkMessagesAndUpdateLayout();
|
||
}
|
||
}, 100);
|
||
`;
|
||
}
|