From 2c6e4f64e8d59d16d475f0969521ab9fcb1d64d2 Mon Sep 17 00:00:00 2001 From: Yatendra Kumar Date: Sun, 7 Jan 2024 13:43:39 +0530 Subject: [PATCH 01/12] fix: provide complete context --- media/chat/scripts/main.js | 936 ++++++++++++++-------------- src/providers/chat_view_provider.ts | 52 +- src/repository/gemini-repository.ts | 6 +- 3 files changed, 504 insertions(+), 490 deletions(-) diff --git a/media/chat/scripts/main.js b/media/chat/scripts/main.js index e6f9fd9a..4e919748 100644 --- a/media/chat/scripts/main.js +++ b/media/chat/scripts/main.js @@ -19,491 +19,491 @@ const activityBarBackground = getComputedStyle(document.documentElement).getProp const activityBarForeground = getComputedStyle(document.documentElement).getPropertyValue("--vscode-activityBar-foreground"); const properties = [ - 'direction', - 'boxSizing', - 'width', - 'height', - 'overflowX', - 'overflowY', - - 'borderTopWidth', - 'borderRightWidth', - 'borderBottomWidth', - 'borderLeftWidth', - 'borderStyle', - - 'paddingTop', - 'paddingRight', - 'paddingBottom', - 'paddingLeft', - - 'fontStyle', - 'fontVariant', - 'fontWeight', - 'fontStretch', - 'fontSize', - 'fontSizeAdjust', - 'lineHeight', - 'fontFamily', - - 'textAlign', - 'textTransform', - 'textIndent', - 'textDecoration', - - 'letterSpacing', - 'wordSpacing', - - 'tabSize', - 'MozTabSize', + 'direction', + 'boxSizing', + 'width', + 'height', + 'overflowX', + 'overflowY', + + 'borderTopWidth', + 'borderRightWidth', + 'borderBottomWidth', + 'borderLeftWidth', + 'borderStyle', + + 'paddingTop', + 'paddingRight', + 'paddingBottom', + 'paddingLeft', + + 'fontStyle', + 'fontVariant', + 'fontWeight', + 'fontStretch', + 'fontSize', + 'fontSizeAdjust', + 'lineHeight', + 'fontFamily', + + 'textAlign', + 'textTransform', + 'textIndent', + 'textDecoration', + + 'letterSpacing', + 'wordSpacing', + + 'tabSize', + 'MozTabSize', ]; const actions = ['workspace']; function getCaretCoordinates(element, position) { - const div = document.createElement('div'); - document.body.appendChild(div); + const div = document.createElement('div'); + document.body.appendChild(div); - const style = div.style; - const computed = getComputedStyle(element); + const style = div.style; + const computed = getComputedStyle(element); - style.whiteSpace = 'pre-wrap'; - style.wordBreak = 'break-word'; - style.position = 'absolute'; - style.visibility = 'hidden'; - style.overflow = 'hidden'; + style.whiteSpace = 'pre-wrap'; + style.wordBreak = 'break-word'; + style.position = 'absolute'; + style.visibility = 'hidden'; + style.overflow = 'hidden'; - properties.forEach(prop => { - style[prop] = computed[prop]; - }); + properties.forEach(prop => { + style[prop] = computed[prop]; + }); - div.textContent = element.value.substring(0, position); + div.textContent = element.value.substring(0, position); - const span = document.createElement('span'); - span.textContent = element.value.substring(position) || '.'; - div.appendChild(span); + const span = document.createElement('span'); + span.textContent = element.value.substring(position) || '.'; + div.appendChild(span); - const coordinates = { - top: span.offsetTop + parseInt(computed['borderTopWidth']), - left: span.offsetLeft + parseInt(computed['borderLeftWidth']), - // height: parseInt(computed['lineHeight']) - height: span.offsetHeight - }; + const coordinates = { + top: span.offsetTop + parseInt(computed['borderTopWidth']), + left: span.offsetLeft + parseInt(computed['borderLeftWidth']), + // height: parseInt(computed['lineHeight']) + height: span.offsetHeight + }; - div.remove(); + div.remove(); - return coordinates; + return coordinates; } class Mentionify { - constructor(ref, menuRef, resolveFn, replaceFn, menuItemFn) { - this.ref = ref; - this.menuRef = menuRef; - this.resolveFn = resolveFn; - this.replaceFn = replaceFn; - this.menuItemFn = menuItemFn; - this.options = []; - - this.makeOptions = this.makeOptions.bind(this); - this.closeMenu = this.closeMenu.bind(this); - this.selectItem = this.selectItem.bind(this); - this.onInput = this.onInput.bind(this); - this.onKeyDown = this.onKeyDown.bind(this); - this.renderMenu = this.renderMenu.bind(this); - - this.ref.addEventListener('input', this.onInput); - this.ref.addEventListener('keydown', this.onKeyDown); - } - - async makeOptions(query) { - const options = await this.resolveFn(query); - if (options.lenght !== 0) { - this.options = options; - this.renderMenu(); - } else { - this.closeMenu(); - } - } - - closeMenu() { - setTimeout(() => { - this.options = []; - this.left = undefined; - this.top = undefined; - this.triggerIdx = undefined; - this.renderMenu(); - }, 0); - } - - selectItem(active) { - return () => { - const preMention = this.ref.value.substr(0, this.triggerIdx); - const option = this.options[active]; - const mention = this.replaceFn(option, this.ref.value[this.triggerIdx]); - const postMention = this.ref.value.substr(this.ref.selectionStart); - const newValue = `${preMention}${mention}${postMention}`; - this.ref.value = newValue; - const caretPosition = this.ref.value.length - postMention.length; - this.ref.setSelectionRange(caretPosition, caretPosition); - this.closeMenu(); - this.ref.focus(); - }; - } - - onInput(ev) { - const positionIndex = this.ref.selectionStart; - const textBeforeCaret = this.ref.value.slice(0, positionIndex); - const tokens = textBeforeCaret.split(/\s/); - const lastToken = tokens[tokens.length - 1]; - const triggerIdx = textBeforeCaret.endsWith(lastToken) - ? textBeforeCaret.length - lastToken.length - : -1; - const maybeTrigger = textBeforeCaret[triggerIdx]; - const keystrokeTriggered = maybeTrigger === '@'; - const highlightRegex = new RegExp(`\\B(@${actions.join("|")})\\b`, 'gi'); - const highlightedText = this.ref.value.replace(highlightRegex, (match) => `${match}`); - this.ref.innerHTML = highlightedText; - this.ref.style.height = "auto"; - this.ref.style.height = this.ref.scrollHeight + "px"; - - if (!keystrokeTriggered) { - this.closeMenu(); - return; - } - - const query = textBeforeCaret.slice(triggerIdx + 1); - this.makeOptions(query); - - const coords = getCaretCoordinates(this.ref, positionIndex); - const { top, left } = this.ref.getBoundingClientRect(); - - setTimeout(() => { - this.active = 0; - this.left = window.scrollX + coords.left + left + this.ref.scrollLeft; - this.top = window.scrollY + coords.top + top + coords.height - this.ref.scrollTop; - this.triggerIdx = triggerIdx; - this.renderMenu(); - }, 0); - } - - onKeyDown(ev) { - let keyCaught = false; - if (this.triggerIdx !== undefined) { - switch (ev.key) { - case 'ArrowDown': - this.active = Math.min(this.active + 1, this.options.length - 1); - this.renderMenu(); - keyCaught = true; - break; - case 'ArrowUp': - this.active = Math.max(this.active - 1, 0); - this.renderMenu(); - keyCaught = true; - break; - case 'Enter': - case 'Tab': - this.selectItem(this.active)(); - keyCaught = true; - break; - } - } - - if (keyCaught) { - ev.preventDefault(); - } - } - - renderMenu() { - if (this.top === undefined) { - this.menuRef.hidden = true; - return; - } - - const caretHeight = this.ref.offsetHeight; - this.menuRef.style.left = this.left + 'px'; - this.menuRef.style.top = (this.top - this.menuRef.offsetHeight - caretHeight) + 'px'; - this.menuRef.innerHTML = ''; - - this.options.forEach((option, idx) => { - this.menuRef.appendChild(this.menuItemFn( - option, - this.selectItem(idx), - this.active === idx)); - }); - - this.menuRef.hidden = false; - } + constructor(ref, menuRef, resolveFn, replaceFn, menuItemFn) { + this.ref = ref; + this.menuRef = menuRef; + this.resolveFn = resolveFn; + this.replaceFn = replaceFn; + this.menuItemFn = menuItemFn; + this.options = []; + + this.makeOptions = this.makeOptions.bind(this); + this.closeMenu = this.closeMenu.bind(this); + this.selectItem = this.selectItem.bind(this); + this.onInput = this.onInput.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + this.renderMenu = this.renderMenu.bind(this); + + this.ref.addEventListener('input', this.onInput); + this.ref.addEventListener('keydown', this.onKeyDown); + } + + async makeOptions(query) { + const options = await this.resolveFn(query); + if (options.lenght !== 0) { + this.options = options; + this.renderMenu(); + } else { + this.closeMenu(); + } + } + + closeMenu() { + setTimeout(() => { + this.options = []; + this.left = undefined; + this.top = undefined; + this.triggerIdx = undefined; + this.renderMenu(); + }, 0); + } + + selectItem(active) { + return () => { + const preMention = this.ref.value.substr(0, this.triggerIdx); + const option = this.options[active]; + const mention = this.replaceFn(option, this.ref.value[this.triggerIdx]); + const postMention = this.ref.value.substr(this.ref.selectionStart); + const newValue = `${preMention}${mention}${postMention}`; + this.ref.value = newValue; + const caretPosition = this.ref.value.length - postMention.length; + this.ref.setSelectionRange(caretPosition, caretPosition); + this.closeMenu(); + this.ref.focus(); + }; + } + + onInput(ev) { + const positionIndex = this.ref.selectionStart; + const textBeforeCaret = this.ref.value.slice(0, positionIndex); + const tokens = textBeforeCaret.split(/\s/); + const lastToken = tokens[tokens.length - 1]; + const triggerIdx = textBeforeCaret.endsWith(lastToken) + ? textBeforeCaret.length - lastToken.length + : -1; + const maybeTrigger = textBeforeCaret[triggerIdx]; + const keystrokeTriggered = maybeTrigger === '@'; + const highlightRegex = new RegExp(`\\B(@${actions.join("|")})\\b`, 'gi'); + const highlightedText = this.ref.value.replace(highlightRegex, (match) => `${match}`); + this.ref.innerHTML = highlightedText; + this.ref.style.height = "auto"; + this.ref.style.height = this.ref.scrollHeight + "px"; + + if (!keystrokeTriggered) { + this.closeMenu(); + return; + } + + const query = textBeforeCaret.slice(triggerIdx + 1); + this.makeOptions(query); + + const coords = getCaretCoordinates(this.ref, positionIndex); + const { top, left } = this.ref.getBoundingClientRect(); + + setTimeout(() => { + this.active = 0; + this.left = window.scrollX + coords.left + left + this.ref.scrollLeft; + this.top = window.scrollY + coords.top + top + coords.height - this.ref.scrollTop; + this.triggerIdx = triggerIdx; + this.renderMenu(); + }, 0); + } + + onKeyDown(ev) { + let keyCaught = false; + if (this.triggerIdx !== undefined) { + switch (ev.key) { + case 'ArrowDown': + this.active = Math.min(this.active + 1, this.options.length - 1); + this.renderMenu(); + keyCaught = true; + break; + case 'ArrowUp': + this.active = Math.max(this.active - 1, 0); + this.renderMenu(); + keyCaught = true; + break; + case 'Enter': + case 'Tab': + this.selectItem(this.active)(); + keyCaught = true; + break; + } + } + + if (keyCaught) { + ev.preventDefault(); + } + } + + renderMenu() { + if (this.top === undefined) { + this.menuRef.hidden = true; + return; + } + + const caretHeight = this.ref.offsetHeight; + this.menuRef.style.left = this.left + 'px'; + this.menuRef.style.top = (this.top - this.menuRef.offsetHeight - caretHeight) + 'px'; + this.menuRef.innerHTML = ''; + + this.options.forEach((option, idx) => { + this.menuRef.appendChild(this.menuItemFn( + option, + this.selectItem(idx), + this.active === idx)); + }); + + this.menuRef.hidden = false; + } } (function () { - const vscode = acquireVsCodeApi(); - - - // Define an empty array, which will be loaded through the displayMessages function - let conversationHistory = []; - let loadingIndicator = document.getElementById('loader'); - - // Handle mexssages sent from the extension to the webview - window.addEventListener("message", (event) => { - const message = event.data; - debugger; - switch (message.type) { - case "addResponse": { - response = message.value; - setResponse(); - break; - } - case "clearResponse": { - response = ""; - break; - } - case "setPrompt": { - document.getElementById("prompt-input").value = message.value; - setResponse(); - break; - } - case "displayMessages": { - conversationHistory = message.value; - displayMessages(); - break; - } - case 'showLoadingIndicator': - loadingIndicator.style.display = 'block'; - break; - case 'hideLoadingIndicator': - loadingIndicator.style.display = 'none'; - break; - case 'updateTheme': { - const preBlocks = document.querySelectorAll("pre"); - preBlocks.forEach((_preBlock) => { - console.log('preBlocks', _preBlock); - const iconContainer = _preBlock.querySelectorAll("div"); - iconContainer.forEach((_iconContainer) => { - _iconContainer.style.backgroundColor = getComputedStyle(document.documentElement).getPropertyValue("--vscode-activityBar-background"); - const icon = _iconContainer.querySelectorAll("svg"); - icon.forEach((_icon) => { - _icon.style.fill = getComputedStyle(document.documentElement).getPropertyValue("--vscode-activityBar-foreground"); - }); - }); - }); - break; - } - } - }); - - function fixCodeBlocks(response) { - // Use a regular expression to find all occurrences of the substring in the string - const REGEX_CODEBLOCK = new RegExp("\`\`\`", "g"); - const matches = response.match(REGEX_CODEBLOCK); - - // Return the number of occurrences of the substring in the response, check if even - const count = matches ? matches.length : 0; - if (count % 2 === 0) { - return response; - } else { - // else append ``` to the end to make the last code block complete - return response.concat("\n\`\`\`"); - } - } - - function setResponse() { - const converter = new showdown.Converter({ - omitExtraWLInCodeBlocks: true, - simplifiedAutoLink: true, - excludeTrailingPunctuationFromURLs: true, - literalMidWordUnderscores: true, - simpleLineBreaks: true, - }); - - response = fixCodeBlocks(response); - html = converter.makeHtml(response); - document.getElementById("response").innerHTML = html; - - const preCodeBlocks = document.querySelectorAll("pre code"); - preCodeBlocks.forEach((_preCodeBlock) => { - _preCodeBlock.classList.add( - "p-1", - "my-2", - "block", - "language-dart" - ); - }); - - const preBlocks = document.querySelectorAll("pre"); - preBlocks.forEach((_preBlock) => { - _preBlock.classList.add("language-dart", "relative", "my-5"); - Prism.highlightElement(_preBlock); - - const iconContainer = document.createElement("div"); - iconContainer.id = "icon-container"; - iconContainer.classList.add("absolute", "top-2", "right-2", "inline-flex", "flex-row", "bg-white", "h-8", "w-16", "z-10", "justify-center", "items-center", "rounded-md", "opacity-0"); - iconContainer.style.backgroundColor = activityBarBackground; - - const _copyIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - _copyIcon.innerHTML = copyIcon; - _copyIcon.id = "copy-icon"; - _copyIcon.classList.add("h-7", "w-7", "inline-flex", "justify-center", "items-center", "cursor-pointer"); - _copyIcon.style.fill = activityBarForeground; - _copyIcon.setAttribute("alt", "Copy"); - iconContainer.appendChild(_copyIcon); - - _copyIcon.addEventListener("click", () => { - const textToCopy = _preBlock.textContent; - navigator.clipboard.writeText(textToCopy); - }); - - const _mergeIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - _mergeIcon.innerHTML = mergeIcon; - _mergeIcon.id = "merge-icon"; - _mergeIcon.classList.add("h-7", "w-7", "inline-flex", "justify-center", "items-center", "cursor-pointer"); - _mergeIcon.style.fill = activityBarForeground; - _mergeIcon.setAttribute("alt", "Merge"); - iconContainer.appendChild(_mergeIcon); - - _mergeIcon.addEventListener("click", () => { - vscode.postMessage({ - type: "pasteCode", - value: _preBlock.textContent, - }); - }); - - _preBlock.appendChild(iconContainer); - - _preBlock.addEventListener("mouseenter", () => { - iconContainer.style.opacity = 1; - }); - - _preBlock.addEventListener("mouseleave", () => { - iconContainer.style.opacity = 0; - }); - }); - - const codeBlocks = document.querySelectorAll("code"); - codeBlocks.forEach((_codeBlock) => { - _codeBlock.classList.add("inline-flex", "max-w-full", "overflow-hidden", "rounded-sm", "cursor-pointer", "language-dart"); - _codeBlock.addEventListener("click", function (e) { - e.preventDefault(); - vscode.postMessage({ - type: "codeSelected", - value: this.innerText, - }); - }); - }); - - const modelResponse = document.querySelectorAll("div.user-gemini-pro"); - modelResponse.forEach((_modelResponse) => { - const pBlocks = _modelResponse.querySelectorAll("p"); - pBlocks.forEach((_pBlock) => { - _pBlock.classList.add("my-3"); - }); - }); - } - - // Function to display messages in the chat container - function displayMessages() { - const dynamicMessagesContainer = document.getElementById("dynamic-messages"); - console.log(conversationHistory); - - // Clear existing messages - dynamicMessagesContainer.innerHTML = ""; - - // Loop through the messages array and create message elements - conversationHistory.slice(2, conversationHistory.length).forEach((message, index) => { - const messageElement = document.createElement("div"); - if (message.role === "model") { - messageElement.classList.add("message", "user-gemini-pro"); // Change class to "user-gemini-pro" - messageElement.innerHTML = `

FlutterGPT: ${markdownToPlain(message.parts)}

`; - } else { - messageElement.classList.add("message", "user-you"); // Change class to "user-you" - messageElement.innerHTML = `

You: ${message.parts}

`; - } - dynamicMessagesContainer.appendChild(messageElement); - }); - - // Scroll the chat container to the most recent message - dynamicMessagesContainer.scrollTop = dynamicMessagesContainer.scrollHeight; - } - - - function markdownToPlain(input) { - const converter = new showdown.Converter({ - omitExtraWLInCodeBlocks: true, - simplifiedAutoLink: true, - excludeTrailingPunctuationFromURLs: true, - literalMidWordUnderscores: true, - simpleLineBreaks: true, - openLinksInNewWindow: true, // Add this option to open links in a new window - ghCodeBlocks: true, // Enable GitHub-style code blocks (optional for better styling) - strikethrough: true, // Enable strikethrough syntax (optional) - tasklists: true // Enable task list syntax for checkboxes (optional) - }); - // response = fixCodeBlocks(input); - html = converter.makeHtml(input); - return html; - } - - const resolveFn = prefix => prefix === '' - ? actions - : actions.filter(action => action.startsWith(prefix)); - - const replaceFn = (action, trigger) => `${trigger}${action} `; - - const menuItemFn = (action, setItem, selected) => { - const div = document.createElement('div'); - div.setAttribute('role', 'option'); - div.className = 'menu-item'; - if (selected) { - div.classList.add('selected'); - div.setAttribute('aria-selected', ''); - } - div.textContent = `@${action}`; - div.onclick = setItem; - return div; - }; - - const mentionify = new Mentionify( - document.getElementById('prompt-input'), - document.getElementById('menu'), - resolveFn, - replaceFn, - menuItemFn - ); - - // Listen for keyup events on the prompt input element - const promptInput = document.getElementById("prompt-input"); - promptInput.addEventListener("keydown", function (e) { - console.log(this.value); - // If the key that was pressed was the Enter key - if (e.key === "Enter" && !e.shiftKey && mentionify.menuRef?.hidden) { - vscode.postMessage({ - type: "prompt", - value: this.value.trim(), - }); - } - }); - - promptInput.addEventListener("keyup", function (e) { - if (e.key === "Enter" && !e.shiftKey) { - promptInput.style.height = "2.3rem"; - } - }); - - // Function to introduce a delay using a Promise - function delay(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - - // Async Function to show the toast - async function showToast() { - const toastContainer = document.getElementById('toast-container'); - - // Display the toast - toastContainer.style.display = 'flex'; - - // Wait for 3 seconds - await delay(3000); - - // Completely hide the toast - toastContainer.style.display = 'none'; - } + const vscode = acquireVsCodeApi(); + + + // Define an empty array, which will be loaded through the displayMessages function + let conversationHistory = []; + let loadingIndicator = document.getElementById('loader'); + + // Handle mexssages sent from the extension to the webview + window.addEventListener("message", (event) => { + const message = event.data; + debugger; + switch (message.type) { + case "addResponse": { + response = message.value; + setResponse(); + break; + } + case "clearResponse": { + response = ""; + break; + } + case "setPrompt": { + document.getElementById("prompt-input").value = message.value; + setResponse(); + break; + } + case "displayMessages": { + conversationHistory = message.value; + displayMessages(); + break; + } + case 'showLoadingIndicator': + loadingIndicator.style.display = 'block'; + break; + case 'hideLoadingIndicator': + loadingIndicator.style.display = 'none'; + break; + case 'updateTheme': { + const preBlocks = document.querySelectorAll("pre"); + preBlocks.forEach((_preBlock) => { + console.log('preBlocks', _preBlock); + const iconContainer = _preBlock.querySelectorAll("div"); + iconContainer.forEach((_iconContainer) => { + _iconContainer.style.backgroundColor = getComputedStyle(document.documentElement).getPropertyValue("--vscode-activityBar-background"); + const icon = _iconContainer.querySelectorAll("svg"); + icon.forEach((_icon) => { + _icon.style.fill = getComputedStyle(document.documentElement).getPropertyValue("--vscode-activityBar-foreground"); + }); + }); + }); + break; + } + } + }); + + function fixCodeBlocks(response) { + // Use a regular expression to find all occurrences of the substring in the string + const REGEX_CODEBLOCK = new RegExp("\`\`\`", "g"); + const matches = response.match(REGEX_CODEBLOCK); + + // Return the number of occurrences of the substring in the response, check if even + const count = matches ? matches.length : 0; + if (count % 2 === 0) { + return response; + } else { + // else append ``` to the end to make the last code block complete + return response.concat("\n\`\`\`"); + } + } + + function setResponse() { + const converter = new showdown.Converter({ + omitExtraWLInCodeBlocks: true, + simplifiedAutoLink: true, + excludeTrailingPunctuationFromURLs: true, + literalMidWordUnderscores: true, + simpleLineBreaks: true, + }); + + response = fixCodeBlocks(response); + html = converter.makeHtml(response); + document.getElementById("response").innerHTML = html; + + const preCodeBlocks = document.querySelectorAll("pre code"); + preCodeBlocks.forEach((_preCodeBlock) => { + _preCodeBlock.classList.add( + "p-1", + "my-2", + "block", + "language-dart" + ); + }); + + const preBlocks = document.querySelectorAll("pre"); + preBlocks.forEach((_preBlock) => { + _preBlock.classList.add("language-dart", "relative", "my-5"); + Prism.highlightElement(_preBlock); + + const iconContainer = document.createElement("div"); + iconContainer.id = "icon-container"; + iconContainer.classList.add("absolute", "top-2", "right-2", "inline-flex", "flex-row", "bg-white", "h-8", "w-16", "z-10", "justify-center", "items-center", "rounded-md", "opacity-0"); + iconContainer.style.backgroundColor = activityBarBackground; + + const _copyIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + _copyIcon.innerHTML = copyIcon; + _copyIcon.id = "copy-icon"; + _copyIcon.classList.add("h-7", "w-7", "inline-flex", "justify-center", "items-center", "cursor-pointer"); + _copyIcon.style.fill = activityBarForeground; + _copyIcon.setAttribute("alt", "Copy"); + iconContainer.appendChild(_copyIcon); + + _copyIcon.addEventListener("click", () => { + const textToCopy = _preBlock.textContent; + navigator.clipboard.writeText(textToCopy); + }); + + const _mergeIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + _mergeIcon.innerHTML = mergeIcon; + _mergeIcon.id = "merge-icon"; + _mergeIcon.classList.add("h-7", "w-7", "inline-flex", "justify-center", "items-center", "cursor-pointer"); + _mergeIcon.style.fill = activityBarForeground; + _mergeIcon.setAttribute("alt", "Merge"); + iconContainer.appendChild(_mergeIcon); + + _mergeIcon.addEventListener("click", () => { + vscode.postMessage({ + type: "pasteCode", + value: _preBlock.textContent, + }); + }); + + _preBlock.appendChild(iconContainer); + + _preBlock.addEventListener("mouseenter", () => { + iconContainer.style.opacity = 1; + }); + + _preBlock.addEventListener("mouseleave", () => { + iconContainer.style.opacity = 0; + }); + }); + + const codeBlocks = document.querySelectorAll("code"); + codeBlocks.forEach((_codeBlock) => { + _codeBlock.classList.add("inline-flex", "max-w-full", "overflow-hidden", "rounded-sm", "cursor-pointer", "language-dart"); + _codeBlock.addEventListener("click", function (e) { + e.preventDefault(); + vscode.postMessage({ + type: "codeSelected", + value: this.innerText, + }); + }); + }); + + const modelResponse = document.querySelectorAll("div.user-gemini-pro"); + modelResponse.forEach((_modelResponse) => { + const pBlocks = _modelResponse.querySelectorAll("p"); + pBlocks.forEach((_pBlock) => { + _pBlock.classList.add("my-3"); + }); + }); + } + + // Function to display messages in the chat container + function displayMessages() { + const dynamicMessagesContainer = document.getElementById("dynamic-messages"); + console.log(conversationHistory); + + // Clear existing messages + dynamicMessagesContainer.innerHTML = ""; + + // Loop through the messages array and create message elements + conversationHistory.forEach((message, index) => { + const messageElement = document.createElement("div"); + if (message.role === "model") { + messageElement.classList.add("message", "user-gemini-pro"); // Change class to "user-gemini-pro" + messageElement.innerHTML = `

FlutterGPT: ${markdownToPlain(message.parts)}

`; + } else { + messageElement.classList.add("message", "user-you"); // Change class to "user-you" + messageElement.innerHTML = `

You: ${message.parts}

`; + } + dynamicMessagesContainer.appendChild(messageElement); + }); + + // Scroll the chat container to the most recent message + dynamicMessagesContainer.scrollTop = dynamicMessagesContainer.scrollHeight; + } + + + function markdownToPlain(input) { + const converter = new showdown.Converter({ + omitExtraWLInCodeBlocks: true, + simplifiedAutoLink: true, + excludeTrailingPunctuationFromURLs: true, + literalMidWordUnderscores: true, + simpleLineBreaks: true, + openLinksInNewWindow: true, // Add this option to open links in a new window + ghCodeBlocks: true, // Enable GitHub-style code blocks (optional for better styling) + strikethrough: true, // Enable strikethrough syntax (optional) + tasklists: true // Enable task list syntax for checkboxes (optional) + }); + // response = fixCodeBlocks(input); + html = converter.makeHtml(input); + return html; + } + + const resolveFn = prefix => prefix === '' + ? actions + : actions.filter(action => action.startsWith(prefix)); + + const replaceFn = (action, trigger) => `${trigger}${action} `; + + const menuItemFn = (action, setItem, selected) => { + const div = document.createElement('div'); + div.setAttribute('role', 'option'); + div.className = 'menu-item'; + if (selected) { + div.classList.add('selected'); + div.setAttribute('aria-selected', ''); + } + div.textContent = `@${action}`; + div.onclick = setItem; + return div; + }; + + const mentionify = new Mentionify( + document.getElementById('prompt-input'), + document.getElementById('menu'), + resolveFn, + replaceFn, + menuItemFn + ); + + // Listen for keyup events on the prompt input element + const promptInput = document.getElementById("prompt-input"); + promptInput.addEventListener("keydown", function (e) { + console.log(this.value); + // If the key that was pressed was the Enter key + if (e.key === "Enter" && !e.shiftKey && mentionify.menuRef?.hidden) { + vscode.postMessage({ + type: "prompt", + value: this.value.trim(), + }); + } + }); + + promptInput.addEventListener("keyup", function (e) { + if (e.key === "Enter" && !e.shiftKey) { + promptInput.style.height = "2.3rem"; + } + }); + + // Function to introduce a delay using a Promise + function delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + // Async Function to show the toast + async function showToast() { + const toastContainer = document.getElementById('toast-container'); + + // Display the toast + toastContainer.style.display = 'flex'; + + // Wait for 3 seconds + await delay(3000); + + // Completely hide the toast + toastContainer.style.display = 'none'; + } })(); diff --git a/src/providers/chat_view_provider.ts b/src/providers/chat_view_provider.ts index e47551ef..03802b21 100644 --- a/src/providers/chat_view_provider.ts +++ b/src/providers/chat_view_provider.ts @@ -55,7 +55,7 @@ export class FlutterGPTViewProvider implements vscode.WebviewViewProvider { break; } - case "pasteCode": + case "pasteCode": { const editor = vscode.window.activeTextEditor; if (editor) { @@ -73,7 +73,7 @@ export class FlutterGPTViewProvider implements vscode.WebviewViewProvider { }); vscode.window.onDidChangeActiveColorTheme(() => { - webviewView.webview.postMessage({type: 'updateTheme'}); + webviewView.webview.postMessage({ type: 'updateTheme' }); }); } @@ -104,7 +104,8 @@ export class FlutterGPTViewProvider implements vscode.WebviewViewProvider { } - private _conversationHistory: Array<{ role: string, parts: string }> = []; + private _publicConversationHistory: Array<{ role: string, parts: string }> = []; + private _privateConversationHistory: Array<{ role: string, parts: string }> = []; private async getResponse(prompt: string) { if (!this._view) { @@ -115,30 +116,47 @@ export class FlutterGPTViewProvider implements vscode.WebviewViewProvider { } // Initialize conversation history if it's the first time - // debugger; - if (this._conversationHistory.length === 0) { - this._conversationHistory.push( + if (this._privateConversationHistory.length === 0) { + this._privateConversationHistory.push( { role: 'user', parts: "You are a flutter/dart development expert who specializes in providing production-ready well-formatted code.\n\n" }, { role: 'model', parts: "I am a flutter/dart development expert who specializes in providing production-ready well-formatted code. How can I help you?\n\n" } ); } - console.debug('conversation history', this._conversationHistory); + let workspacePrompt = ""; - // Append the current user prompt to the conversation history - this._conversationHistory.push({ role: 'user', parts: prompt }); - this._view?.webview.postMessage({ type: 'displayMessages', value: this._conversationHistory }); + // Add a simplified version to the public history + this._publicConversationHistory.push({ role: 'user', parts: prompt }); + this._view?.webview.postMessage({ type: 'displayMessages', value: this._publicConversationHistory }); this._view?.webview.postMessage({ type: 'setPrompt', value: '' }); this._view?.webview.postMessage({ type: 'showLoadingIndicator' }); + // Check if the prompt includes '@workspace' and handle accordingly try { - // Use the stored conversation history for the prompt - const isWorkspacePresent = prompt.includes('@workspace'); - const response = await this.aiRepo.getCompletion(this._conversationHistory, isWorkspacePresent); - this._conversationHistory.push({ role: 'user', parts: prompt }); - this._conversationHistory.push({ role: 'model', parts: response }); - this._view?.webview.postMessage({ type: 'displayMessages', value: this._conversationHistory }); - this._view?.webview.postMessage({ type: 'addResponse', value: '' }); + if (prompt.includes('@workspace')) { + // Add the full prompt to the private history for completion + const dartFiles = await this.aiRepo.findClosestDartFiles(prompt); + workspacePrompt = "You've complete access to the codebase. I'll provide you with top 5 closest files code as context and your job is to read following files code end-to-end and answer the prompt initialised by `@workspace` symbol. If you're unable to find answer for the requested prompt, suggest an alternative solution as a dart expert. Be crisp & crystal clear in your answer. Make sure to provide your thinking process in steps including the file paths, name & code. Here's the code: \n\n" + dartFiles + "\n\n" + prompt; + this._privateConversationHistory.push({ role: 'user', parts: workspacePrompt }); + } else { + // Append the current user prompt to the conversation history + this._privateConversationHistory.push({ role: 'user', parts: prompt }); + } + } catch (error) { + console.error("Error processing workspace prompt: ", error); + } + + // Use the stored conversation history for the prompt + try { + const response = await this.aiRepo.getCompletion(this._privateConversationHistory); + if (prompt.includes('@workspace')) { + this._privateConversationHistory.push({ role: 'user', parts: workspacePrompt }); + } else { + this._privateConversationHistory.push({ role: 'user', parts: prompt }); + } + this._privateConversationHistory.push({ role: 'model', parts: response }); + this._publicConversationHistory.push({ role: 'model', parts: response }); + this._view?.webview.postMessage({ type: 'displayMessages', value: this._publicConversationHistory }); } catch (error) { console.error(error); diff --git a/src/repository/gemini-repository.ts b/src/repository/gemini-repository.ts index c8d0d2d8..8a6e84f0 100644 --- a/src/repository/gemini-repository.ts +++ b/src/repository/gemini-repository.ts @@ -34,16 +34,12 @@ export class GeminiRepository { return text; } - public async getCompletion(prompt: { role: string, parts: string }[], isReferenceAdded?: boolean): Promise { + public async getCompletion(prompt: { role: string, parts: string }[]): Promise { if (!this.apiKey) { throw new Error('API token not set, please go to extension settings to set it (read README.md for more info)'); } let lastMessage = prompt.pop(); - if (lastMessage && isReferenceAdded) { - const dartFiles = await this.findClosestDartFiles(lastMessage.parts); - lastMessage.parts = "You're a vscode extension copilot, you've complete access to the codebase. I'll provide you with top 5 closest files code as context and your job is to read following workspace code end-to-end and answer the prompt initialised by `@workspace` symbol. If you're unable to find answer for the requested prompt, suggest an alternative solution as a dart expert. Be crisp & crystal clear in your answer. Make sure to provide your thinking process in steps. Here's the code: \n\n" + dartFiles + "\n\n" + lastMessage.parts; - } const chat = this.genAI.getGenerativeModel({ model: "gemini-pro", generationConfig: { temperature: 0.0, topP: 0.2 } }).startChat( { history: prompt, From 9bfb4e2d2da07aef45a98c424435db46983a9fa9 Mon Sep 17 00:00:00 2001 From: wadhia-yash Date: Mon, 8 Jan 2024 12:45:59 +0530 Subject: [PATCH 02/12] fix: list style enabled --- media/chat/css/chatpage.css | 14 +++++++++++++- media/chat/scripts/main.js | 6 ++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/media/chat/css/chatpage.css b/media/chat/css/chatpage.css index 0648e989..58f18d02 100644 --- a/media/chat/css/chatpage.css +++ b/media/chat/css/chatpage.css @@ -163,7 +163,19 @@ a { color: var(--vscode-textLink-activeForeground); } +ol li::before, ul li::before { - content: "•"; margin-right: 5px; +} + +ol, ul { + padding-left: 11px; +} + +ol { + list-style-type: decimal; +} + +ul { + list-style-type: disc; } \ No newline at end of file diff --git a/media/chat/scripts/main.js b/media/chat/scripts/main.js index e6f9fd9a..1e37a1cd 100644 --- a/media/chat/scripts/main.js +++ b/media/chat/scripts/main.js @@ -393,8 +393,10 @@ class Mentionify { const modelResponse = document.querySelectorAll("div.user-gemini-pro"); modelResponse.forEach((_modelResponse) => { const pBlocks = _modelResponse.querySelectorAll("p"); - pBlocks.forEach((_pBlock) => { - _pBlock.classList.add("my-3"); + pBlocks.forEach((_pBlock, index) => { + if (index !== 0) { + _pBlock.classList.add("my-3"); + } }); }); } From 40738842a806fa06c6e29f2d2637b9f17fad3703 Mon Sep 17 00:00:00 2001 From: Yatendra Kumar Date: Mon, 8 Jan 2024 19:47:56 +0530 Subject: [PATCH 03/12] fix: ux breakdown + assisted by @wadhia-yash --- media/chat/scripts/main.js | 81 ++++++++++++++++++++++++----- src/providers/chat_view_provider.ts | 2 + 2 files changed, 70 insertions(+), 13 deletions(-) diff --git a/media/chat/scripts/main.js b/media/chat/scripts/main.js index e6addcc6..0e459cde 100644 --- a/media/chat/scripts/main.js +++ b/media/chat/scripts/main.js @@ -3,15 +3,15 @@ // This script will be run within the webview itself // It cannot access the main VS Code APIs directly. -const copyIcon = ` - - +const copyIcon = ` + + `; -const mergeIcon = ` - - - +const mergeIcon = ` + + + `; @@ -265,6 +265,7 @@ class Mentionify { case "displayMessages": { conversationHistory = message.value; displayMessages(); + setResponse(); break; } case 'showLoadingIndicator': @@ -320,7 +321,7 @@ class Mentionify { simpleLineBreaks: true, }); - response = fixCodeBlocks(response); + // response = fixCodeBlocks(response); html = converter.makeHtml(response); document.getElementById("response").innerHTML = html; @@ -429,6 +430,64 @@ class Mentionify { // Scroll the chat container to the most recent message dynamicMessagesContainer.scrollTop = dynamicMessagesContainer.scrollHeight; + const preCodeBlocks = document.querySelectorAll("pre code"); + preCodeBlocks.forEach((_preCodeBlock) => { + _preCodeBlock.classList.add( + "p-1", + "my-2", + "block", + "language-dart" + ); + }); + + const preBlocks = document.querySelectorAll("pre"); + preBlocks.forEach((_preBlock) => { + _preBlock.classList.add("language-dart", "relative", "my-5"); + Prism.highlightElement(_preBlock); + + const iconContainer = document.createElement("div"); + iconContainer.id = "icon-container"; + iconContainer.classList.add("absolute", "top-2", "right-2", "inline-flex", "flex-row", "h-8", "w-16", "z-10", "justify-center", "items-center", "rounded-md", "opacity-0"); + iconContainer.style.backgroundColor = activityBarBackground; + + const _copyIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + _copyIcon.innerHTML = copyIcon; + _copyIcon.id = "copy-icon"; + _copyIcon.classList.add("h-6", "w-6", "inline-flex", "justify-center", "items-center", "cursor-pointer"); + _copyIcon.style.fill = activityBarForeground; + _copyIcon.setAttribute("alt", "Copy"); + iconContainer.appendChild(_copyIcon); + + _copyIcon.addEventListener("click", () => { + const textToCopy = _preBlock.textContent.trim(); + navigator.clipboard.writeText(textToCopy); + }); + + const _mergeIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + _mergeIcon.innerHTML = mergeIcon; + _mergeIcon.id = "merge-icon"; + _mergeIcon.classList.add("h-6", "w-6", "inline-flex", "justify-center", "items-center", "cursor-pointer"); + _mergeIcon.style.fill = activityBarForeground; + _mergeIcon.setAttribute("alt", "Merge"); + iconContainer.appendChild(_mergeIcon); + + _mergeIcon.addEventListener("click", () => { + vscode.postMessage({ + type: "pasteCode", + value: _preBlock.textContent.trim(), + }); + }); + + _preBlock.appendChild(iconContainer); + + _preBlock.addEventListener("mouseenter", () => { + iconContainer.style.opacity = 1; + }); + + _preBlock.addEventListener("mouseleave", () => { + iconContainer.style.opacity = 0; + }); + }); } @@ -439,10 +498,6 @@ class Mentionify { excludeTrailingPunctuationFromURLs: true, literalMidWordUnderscores: true, simpleLineBreaks: true, - openLinksInNewWindow: true, // Add this option to open links in a new window - ghCodeBlocks: true, // Enable GitHub-style code blocks (optional for better styling) - strikethrough: true, // Enable strikethrough syntax (optional) - tasklists: true // Enable task list syntax for checkboxes (optional) }); // response = fixCodeBlocks(input); html = converter.makeHtml(input); @@ -536,4 +591,4 @@ class Mentionify { snackbar.removeChild(errorTextNode); snackbar.style.display = "none"; } -})(); +})(); \ No newline at end of file diff --git a/src/providers/chat_view_provider.ts b/src/providers/chat_view_provider.ts index e4081d35..03268327 100644 --- a/src/providers/chat_view_provider.ts +++ b/src/providers/chat_view_provider.ts @@ -162,6 +162,7 @@ export class FlutterGPTViewProvider implements vscode.WebviewViewProvider { this._privateConversationHistory.push({ role: 'model', parts: response }); this._publicConversationHistory.push({ role: 'model', parts: response }); this._view?.webview.postMessage({ type: 'displayMessages', value: this._publicConversationHistory }); + this._view?.webview.postMessage({ type: 'addResponse', value: '' }); } catch (error) { console.error(error); @@ -175,5 +176,6 @@ export class FlutterGPTViewProvider implements vscode.WebviewViewProvider { private clearConversationHistory() { this._privateConversationHistory = []; + this._publicConversationHistory = []; } } From 0efa43f5053ff1ab764fd2121b623824e932dee9 Mon Sep 17 00:00:00 2001 From: Yatendra Kumar Date: Tue, 9 Jan 2024 10:41:44 +0530 Subject: [PATCH 04/12] fix: ux issues + increase token limit --- media/chat/scripts/main.js | 3 ++- src/repository/gemini-repository.ts | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/media/chat/scripts/main.js b/media/chat/scripts/main.js index 0e459cde..84b0c0cb 100644 --- a/media/chat/scripts/main.js +++ b/media/chat/scripts/main.js @@ -443,7 +443,8 @@ class Mentionify { const preBlocks = document.querySelectorAll("pre"); preBlocks.forEach((_preBlock) => { _preBlock.classList.add("language-dart", "relative", "my-5"); - Prism.highlightElement(_preBlock); + const textToHighlight = _preBlock.textContent.trim(); + Prism.highlightElement(textToHighlight); const iconContainer = document.createElement("div"); iconContainer.id = "icon-container"; diff --git a/src/repository/gemini-repository.ts b/src/repository/gemini-repository.ts index 7a85eceb..d157b1e7 100644 --- a/src/repository/gemini-repository.ts +++ b/src/repository/gemini-repository.ts @@ -42,7 +42,9 @@ export class GeminiRepository { let lastMessage = prompt.pop(); const chat = this.genAI.getGenerativeModel({ model: "gemini-pro", generationConfig: { temperature: 0.0, topP: 0.2 } }).startChat( { - history: prompt, + history: prompt, generationConfig: { + maxOutputTokens: 4096, + }, } ); const result = await chat.sendMessage(lastMessage?.parts ?? ""); From 0ccc7e70e62a96ae920f9a0a725fb12aeba6f66e Mon Sep 17 00:00:00 2001 From: wadhia-yash Date: Tue, 9 Jan 2024 14:17:44 +0530 Subject: [PATCH 05/12] feat(Workspace Loader): Designed UI for loading screen when workspace query is in process --- media/chat/chat.html | 64 ++++++++++++++++++++++++++--- media/chat/css/chatpage.css | 6 +++ media/chat/scripts/main.js | 44 +++++++++++++++++++- src/providers/chat_view_provider.ts | 5 +-- src/repository/gemini-repository.ts | 29 +++++++++++-- 5 files changed, 135 insertions(+), 13 deletions(-) diff --git a/media/chat/chat.html b/media/chat/chat.html index be13dd50..13a317c9 100644 --- a/media/chat/chat.html +++ b/media/chat/chat.html @@ -7,11 +7,8 @@ - - - + @@ -27,16 +24,73 @@
+ +
- + + + \ No newline at end of file diff --git a/media/chat/css/chatpage.css b/media/chat/css/chatpage.css index 02e01d02..7d29e15a 100644 --- a/media/chat/css/chatpage.css +++ b/media/chat/css/chatpage.css @@ -203,6 +203,12 @@ body { word-break: break-word; } +#workspace-loader { + background-color: var(--vscode-editor-background); + color: var(--vscode-editor-foreground); + fill: var(--vscode-editor-foreground); +} + .user-gemini-pro p { margin-left: 0px; white-space: pre-wrap; diff --git a/media/chat/scripts/main.js b/media/chat/scripts/main.js index 66c5c8dd..dbd678c8 100644 --- a/media/chat/scripts/main.js +++ b/media/chat/scripts/main.js @@ -242,11 +242,17 @@ class Mentionify { // Define an empty array, which will be loaded through the displayMessages function let conversationHistory = []; let loadingIndicator = document.getElementById('loader'); + let workspaceLoader = document.getElementById('workspace-loader'); + let accessingWorkspaceLoader = document.getElementById('accessing-workspace-loader'); + let accessingWorkspaceDone = document.getElementById('accessing-workspace-done'); + let fetchingFileLoader = document.getElementById('fetching-file-loader'); + let fetchingFileDone = document.getElementById('fetching-file-done'); + let creatingResultLoader = document.getElementById('creating-result-loader'); + let creatingResultDone = document.getElementById('creating-result-done'); // Handle mexssages sent from the extension to the webview window.addEventListener("message", (event) => { const message = event.data; - debugger; switch (message.type) { case "addResponse": { response = message.value; @@ -293,6 +299,42 @@ class Mentionify { showSnackbar(response); break; } + case 'workspaceLoader': { + debugger; + workspaceLoader.style.display = message.value ? 'flex' : 'none'; + if (message.value) { + workspaceLoader.classList.remove("animate__slideOutDown"); + workspaceLoader.classList.add("animate__slideInUp"); + } else { + workspaceLoader.classList.remove("animate__slideInUp"); + workspaceLoader.classList.add("animate__slideOutDown"); + } + break; + } + case 'stepLoader': { + if (message.value?.accessingWorkspaceLoader) { + accessingWorkspaceLoader.style.display = 'none'; + accessingWorkspaceDone.style.display = 'block'; + } + if (message.value?.fetchingFileLoader) { + fetchingFileLoader.style.display = 'none'; + fetchingFileDone.style.display = 'block'; + } + if (message.value?.creatingResultLoader) { + creatingResultLoader.style.display = 'none'; + creatingResultDone.style.display = 'block'; + } + break; + } + case 'stepLoaderCompleted': { + accessingWorkspaceLoader.style.display = 'block'; + accessingWorkspaceDone.style.display = 'none'; + fetchingFileLoader.style.display = 'block'; + fetchingFileDone.style.display = 'none'; + creatingResultLoader.style.display = 'block'; + creatingResultDone.style.display = 'none'; + break; + } } }); diff --git a/src/providers/chat_view_provider.ts b/src/providers/chat_view_provider.ts index 6eaaf18a..1496c209 100644 --- a/src/providers/chat_view_provider.ts +++ b/src/providers/chat_view_provider.ts @@ -3,7 +3,6 @@ import * as fs from 'fs'; import path = require('path'); import { GeminiRepository } from "../repository/gemini-repository"; - export class FlutterGPTViewProvider implements vscode.WebviewViewProvider { public static readonly viewType = "fluttergpt.chatView"; private _view?: vscode.WebviewView; @@ -127,7 +126,6 @@ export class FlutterGPTViewProvider implements vscode.WebviewViewProvider { { role: 'model', parts: "I am a flutter/dart development expert who specializes in providing production-ready well-formatted code. How can I help you?\n\n" } ); } - console.debug('conversation history', this._conversationHistory); // Append the current user prompt to the conversation history this._conversationHistory.push({ role: 'user', parts: prompt }); @@ -139,7 +137,7 @@ export class FlutterGPTViewProvider implements vscode.WebviewViewProvider { try { // Use the stored conversation history for the prompt const isWorkspacePresent = prompt.includes('@workspace'); - const response = await this.aiRepo.getCompletion(this._conversationHistory, isWorkspacePresent); + const response = await this.aiRepo.getCompletion(this._conversationHistory, isWorkspacePresent, this._view); this._conversationHistory.push({ role: 'user', parts: prompt }); this._conversationHistory.push({ role: 'model', parts: response }); this._view?.webview.postMessage({ type: 'displayMessages', value: this._conversationHistory }); @@ -150,6 +148,7 @@ export class FlutterGPTViewProvider implements vscode.WebviewViewProvider { this._view?.webview.postMessage({ type: 'displaySnackbar', value: response }); } finally { this._view?.webview.postMessage({ type: 'hideLoadingIndicator' }); + this._view?.webview.postMessage({type: 'workspaceLoader', value: false}); } } diff --git a/src/repository/gemini-repository.ts b/src/repository/gemini-repository.ts index 1557a389..6e754358 100644 --- a/src/repository/gemini-repository.ts +++ b/src/repository/gemini-repository.ts @@ -13,6 +13,7 @@ function handleError(error: Error, userFriendlyMessage: string): never { export class GeminiRepository { private apiKey?: string; private genAI: GoogleGenerativeAI; + private _view?: vscode.Webview; constructor(apiKey: string) { this.apiKey = apiKey; @@ -34,15 +35,16 @@ export class GeminiRepository { return text; } - public async getCompletion(prompt: { role: string, parts: string }[], isReferenceAdded?: boolean): Promise { + public async getCompletion(prompt: { role: string, parts: string }[], isReferenceAdded?: boolean, view?: vscode.WebviewView): Promise { if (!this.apiKey) { throw new Error('API token not set, please go to extension settings to set it (read README.md for more info)'); } let lastMessage = prompt.pop(); if (lastMessage && isReferenceAdded) { - const dartFiles = await this.findClosestDartFiles(lastMessage.parts); - lastMessage.parts = "You've complete access to the codebase. I'll provide you with top 5 closest files code as context and your job is to read following workspace code end-to-end and answer the prompt initialised by `@workspace` symbol. If you're unable to find answer for the requested prompt, suggest an alternative solution as a dart expert. Be crisp & crystal clear in your answer. Make sure to provide your thinking process in steps along with file name, path & code. Here's the code: \n\n" + dartFiles + "\n\n" + lastMessage.parts; + this.displayWebViewMessage(view, 'workspaceLoader', true); + const dartFiles = await this.findClosestDartFiles(lastMessage.parts, view); + lastMessage.parts = "You're a vscode extension copilot, you've complete access to the codebase. I'll provide you with top 5 closest files code as context and your job is to read following workspace code end-to-end and answer the prompt initialised by `@workspace` symbol. If you're unable to find answer for the requested prompt, suggest an alternative solution as a dart expert. Be crisp & crystal clear in your answer. Make sure to provide your thinking process in steps. Here's the code: \n\n" + dartFiles + "\n\n" + lastMessage.parts; } const chat = this.genAI.getGenerativeModel({ model: "gemini-pro", generationConfig: { temperature: 0.0, topP: 0.2 } }).startChat( { @@ -55,12 +57,29 @@ export class GeminiRepository { const text = response.text(); // Creating a result for you + if (isReferenceAdded) { + this.displayWebViewMessage(view, 'stepLoader', { creatingResultLoader: true }); + await this.sleep(2000); + this.displayWebViewMessage(view, 'workspaceLoader', false); + this.displayWebViewMessage(view, 'stepLoaderCompleted', ''); + } return text; } // Cache structure private codehashCache: { [filePath: string]: { codehash: string, embedding: ContentEmbedding } } = {}; + private displayWebViewMessage(view?: vscode.WebviewView, type?: string, value?: any) { + view?.webview.postMessage({ + type, + value + }); + } + + private async sleep(msec: number) { + return new Promise(resolve => setTimeout(resolve, msec)); + } + // Modify the get cacheFilePath getter to point to a more secure location private get cacheFilePath() { @@ -122,7 +141,7 @@ export class GeminiRepository { } // Find 5 closest dart files for query - public async findClosestDartFiles(query: string): Promise { + public async findClosestDartFiles(query: string, view?: vscode.WebviewView): Promise { try { if (!this.apiKey) { throw new Error('API token not set, please go to extension settings to set it (read README.md for more info)'); @@ -193,6 +212,7 @@ export class GeminiRepository { await this.saveCache(); //Accessing work structure(it can take a while in first time) + this.displayWebViewMessage(view, 'stepLoader', { accessingWorkspaceLoader: true }); // Generate embedding for the query const queryEmbedding = await embeddingModel.embedContent({ @@ -217,6 +237,7 @@ export class GeminiRepository { } // Fetching most relevant files + this.displayWebViewMessage(view, 'stepLoader', { fetchingFileLoader: true }); return resultString.trim(); } catch (error) { console.error("Error finding closest Dart files: ", error); From 6b5dd8a1da68ca9e44b2d2555199e657a7e6d451 Mon Sep 17 00:00:00 2001 From: Yatendra Kumar Date: Tue, 9 Jan 2024 15:31:28 +0530 Subject: [PATCH 06/12] fix: added token handling + tuned prompt --- src/providers/chat_view_provider.ts | 5 +++-- src/repository/gemini-repository.ts | 17 +++++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/providers/chat_view_provider.ts b/src/providers/chat_view_provider.ts index 03268327..fee85a9d 100644 --- a/src/providers/chat_view_provider.ts +++ b/src/providers/chat_view_provider.ts @@ -123,8 +123,8 @@ export class FlutterGPTViewProvider implements vscode.WebviewViewProvider { // Initialize conversation history if it's the first time if (this._privateConversationHistory.length === 0) { this._privateConversationHistory.push( - { role: 'user', parts: "You are a flutter/dart development expert who specializes in providing production-ready well-formatted code.\n\n" }, - { role: 'model', parts: "I am a flutter/dart development expert who specializes in providing production-ready well-formatted code. How can I help you?\n\n" } + { role: 'user', parts: "You are a flutter/dart development expert who specializes in providing production-ready well-formatted code. Each response from you must be STRICTLY under 8192 characters. DO NOT MENTION EXTRA DETAILS APART FROMT THE QUERY ASKED BY THE USER.\n\n" }, + { role: 'model', parts: "I am a flutter/dart development expert who specializes in providing production-ready well-formatted code. Each response of mine will be STRICTLY under 8192 characters. How can I help you?\n\n" } ); } let workspacePrompt = ""; @@ -168,6 +168,7 @@ export class FlutterGPTViewProvider implements vscode.WebviewViewProvider { console.error(error); const response = error instanceof Error ? error.message : 'An unexpected error occurred.'; this._view?.webview.postMessage({ type: 'displaySnackbar', value: response }); + this._view?.webview.postMessage({ type: 'addResponse', value: '' }); } finally { this._view?.webview.postMessage({ type: 'hideLoadingIndicator' }); } diff --git a/src/repository/gemini-repository.ts b/src/repository/gemini-repository.ts index d157b1e7..c9568d34 100644 --- a/src/repository/gemini-repository.ts +++ b/src/repository/gemini-repository.ts @@ -5,9 +5,8 @@ import * as crypto from 'crypto'; import path = require("path"); function handleError(error: Error, userFriendlyMessage: string): never { - console.error(error); // Log the detailed error for debugging purposes - // Here you could also include logic to log to an external monitoring service - throw new Error(userFriendlyMessage); // Throw a user-friendly message + console.error(error); + throw new Error(userFriendlyMessage); } export class GeminiRepository { @@ -40,10 +39,20 @@ export class GeminiRepository { throw new Error('API token not set, please go to extension settings to set it (read README.md for more info)'); } let lastMessage = prompt.pop(); + + // Count the tokens in the prompt + const model = this.genAI.getGenerativeModel({ model: "gemini-pro" }); + const { totalTokens } = await model.countTokens(lastMessage?.parts ?? ""); + console.log("Total input tokens: " + totalTokens); + // Check if the token count exceeds the limit + if (totalTokens > 30720) { + throw Error('Input prompt exceeds the maximum token limit.'); + } + const chat = this.genAI.getGenerativeModel({ model: "gemini-pro", generationConfig: { temperature: 0.0, topP: 0.2 } }).startChat( { history: prompt, generationConfig: { - maxOutputTokens: 4096, + maxOutputTokens: 2048, }, } ); From e777dda0f38d74a4bd9d80add0769164172faa91 Mon Sep 17 00:00:00 2001 From: Yatendra Kumar Date: Tue, 9 Jan 2024 16:12:04 +0530 Subject: [PATCH 07/12] chore: added file paths --- src/providers/chat_view_provider.ts | 12 ++++-------- src/repository/gemini-repository.ts | 7 +++++-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/providers/chat_view_provider.ts b/src/providers/chat_view_provider.ts index 06adb0e1..ace24063 100644 --- a/src/providers/chat_view_provider.ts +++ b/src/providers/chat_view_provider.ts @@ -3,6 +3,7 @@ import * as fs from 'fs'; import path = require('path'); import { GeminiRepository } from "../repository/gemini-repository"; + export class FlutterGPTViewProvider implements vscode.WebviewViewProvider { public static readonly viewType = "fluttergpt.chatView"; private _view?: vscode.WebviewView; @@ -137,12 +138,6 @@ export class FlutterGPTViewProvider implements vscode.WebviewViewProvider { // Check if the prompt includes '@workspace' and handle accordingly try { - // Use the stored conversation history for the prompt - const isWorkspacePresent = prompt.includes('@workspace'); - const response = await this.aiRepo.getCompletion(this._conversationHistory, isWorkspacePresent, this._view); - this._conversationHistory.push({ role: 'user', parts: prompt }); - this._conversationHistory.push({ role: 'model', parts: response }); - this._view?.webview.postMessage({ type: 'displayMessages', value: this._conversationHistory }); if (prompt.includes('@workspace')) { // Add the full prompt to the private history for completion const dartFiles = await this.aiRepo.findClosestDartFiles(prompt); @@ -158,10 +153,12 @@ export class FlutterGPTViewProvider implements vscode.WebviewViewProvider { // Use the stored conversation history for the prompt try { - const response = await this.aiRepo.getCompletion(this._privateConversationHistory); + let response = ''; if (prompt.includes('@workspace')) { + response = await this.aiRepo.getCompletion(this._privateConversationHistory, true, this._view); this._privateConversationHistory.push({ role: 'user', parts: workspacePrompt }); } else { + response = await this.aiRepo.getCompletion(this._privateConversationHistory); this._privateConversationHistory.push({ role: 'user', parts: prompt }); } this._privateConversationHistory.push({ role: 'model', parts: response }); @@ -176,7 +173,6 @@ export class FlutterGPTViewProvider implements vscode.WebviewViewProvider { this._view?.webview.postMessage({ type: 'addResponse', value: '' }); } finally { this._view?.webview.postMessage({ type: 'hideLoadingIndicator' }); - this._view?.webview.postMessage({type: 'workspaceLoader', value: false}); } } diff --git a/src/repository/gemini-repository.ts b/src/repository/gemini-repository.ts index 192fb14e..d2cbe6c9 100644 --- a/src/repository/gemini-repository.ts +++ b/src/repository/gemini-repository.ts @@ -41,14 +41,13 @@ export class GeminiRepository { let lastMessage = prompt.pop(); if (lastMessage && isReferenceAdded) { this.displayWebViewMessage(view, 'workspaceLoader', true); - const dartFiles = await this.findClosestDartFiles(lastMessage.parts, view); - lastMessage.parts = "You're a vscode extension copilot, you've complete access to the codebase. I'll provide you with top 5 closest files code as context and your job is to read following workspace code end-to-end and answer the prompt initialised by `@workspace` symbol. If you're unable to find answer for the requested prompt, suggest an alternative solution as a dart expert. Be crisp & crystal clear in your answer. Make sure to provide your thinking process in steps. Here's the code: \n\n" + dartFiles + "\n\n" + lastMessage.parts; } // Count the tokens in the prompt const model = this.genAI.getGenerativeModel({ model: "gemini-pro" }); const { totalTokens } = await model.countTokens(lastMessage?.parts ?? ""); console.log("Total input tokens: " + totalTokens); + // Check if the token count exceeds the limit if (totalTokens > 30720) { throw Error('Input prompt exceeds the maximum token limit.'); @@ -246,6 +245,10 @@ export class GeminiRepository { resultString += fileContent; } + // A list of most relevant file paths + const filePaths = distances.slice(0, 5).map(fileEmbedding => fileEmbedding.file.path); + console.log("Most relevant file paths:" + filePaths.join(", ")); + // Fetching most relevant files this.displayWebViewMessage(view, 'stepLoader', { fetchingFileLoader: true }); return resultString.trim(); From 94d53b5ce5ebc5393cb00eade7ef619b326598d8 Mon Sep 17 00:00:00 2001 From: wadhia-yash Date: Tue, 9 Jan 2024 21:11:52 +0530 Subject: [PATCH 08/12] feat(File paths): Displaying files while querying workspace --- media/chat/chat.html | 56 +++--------------------- media/chat/css/chatpage.css | 68 ++++++++++++++++++++++++----- media/chat/scripts/main.js | 42 +++++++----------- src/providers/chat_view_provider.ts | 9 ++-- src/repository/gemini-repository.ts | 23 +++------- 5 files changed, 90 insertions(+), 108 deletions(-) diff --git a/media/chat/chat.html b/media/chat/chat.html index 13a317c9..df925ec0 100644 --- a/media/chat/chat.html +++ b/media/chat/chat.html @@ -25,58 +25,12 @@ -