Loading scripts/utils/browser_jobs.mjs +2 −2 Original line number Diff line number Diff line Loading @@ -5,8 +5,8 @@ import { prepareWebviews, generateAssets, writePackageJson, commonJobs } from '. import { ENVIRONMENTS } from '../constants.mjs'; const browserWebviews = { vue: ['gitlab_duo_chat'], vue2: [], vue: [], vue2: ['gitlab_duo_chat'], }; function typecheck(signal) { Loading scripts/utils/desktop_jobs.mjs +2 −2 Original line number Diff line number Diff line Loading @@ -7,8 +7,8 @@ import { prepareWebviews, generateAssets, writePackageJson, commonJobs } from '. import { ENVIRONMENTS } from '../constants.mjs'; const desktopWebviews = { vue: ['gitlab_duo_chat', 'issuable'], vue2: [], vue: ['issuable'], vue2: ['gitlab_duo_chat'], }; export async function copyPendingJobAssets() { Loading webviews/vue2/gitlab_duo_chat/index.html 0 → 100644 +16 −0 Original line number Diff line number Diff line <!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="Content-Security-Policy" content="img-src vscode-resource: https:; script-src 'nonce-{{nonce}}';" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>GitLab Duo Chat</title> </head> <body> <div id="app"></div> <script crossorigin type="module" src="src/main.js"></script> </body> </html> webviews/vue2/gitlab_duo_chat/src/App.vue 0 → 100644 +252 −0 Original line number Diff line number Diff line <script> import DuoChatMessage from './components/chat_message.vue'; import PaperAirplaneIcon from './components/icons/PaperAirplaneIcon.vue'; const vscode = acquireVsCodeApi(); export default { name: 'GitLabDuoChat', components: { DuoChatMessage, PaperAirplaneIcon, }, data() { return { chatMessages: [], promptDraft: null, }; }, created() { window.vsCodeApi = vscode; window.addEventListener('message', this.handleExtensionMessageEvent); window.vsCodeApi.postMessage({ command: 'appReady' }); }, onBeforeDestroy() { window.removeEventListener('message', this.handleExtensionMessageEvent); }, mounted() { this.attachToVsCodeState(); }, watch: { promptDraft(newVal) { window.vsCodeApi.setState({ promptDraft: newVal, }); }, }, methods: { handleExtensionMessageEvent(event) { const message = event.data; console.log('received', message); switch (message.eventType) { case 'focus': { this.focusInput(); break; } case 'newRecord': { this.newRecord(message.record); break; } case 'updateRecord': { this.updateRecord(message.record); break; } default: console.log('Chat view received unexpected message type.'); break; } }, newRecord(record) { this.chatMessages.push(record); this.scrollBottom(); }, updateRecord(record) { const index = this.chatMessages.findIndex(r => r.id === record.id); this.chatMessages.splice(index, 1, record); this.scrollBottom(); }, scrollBottom() { setTimeout(() => { window.scrollTo(0, document.body.scrollHeight); }, 20); // TODO: replace this hack with proper CSS. }, attachToVsCodeState() { const currentState = window.vsCodeApi.getState(); if (currentState && currentState.promptDraft) { this.promptDraft = currentState.promptDraft; } }, // form related stuff. submitPrompt() { window.vsCodeApi.postMessage({ eventType: 'newPrompt', record: { content: this.promptDraft, }, }); this.promptDraft = null; }, handleKeyPress(event) { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); this.submitPrompt(); } }, focusInput() { this.$refs.promptTextarea.focus(); }, }, }; </script> <template> <section> <div class="header">Responses generated by AI.</div> <div id="chat-history"> <duo-chat-message v-for="message in chatMessages" :key="message.id" :message="message" /> </div> <footer> <form @submit.stop.prevent="submitPrompt"> <div class="input-group"> <div class="input"> <textarea rows="1" placeholder="Ask a question" ref="promptTextarea" v-model="promptDraft" @keypress="handleKeyPress" ></textarea> </div> <div class="input-group-append"> <button type="submit"> <paper-airplane-icon /> </button> </div> </div> <small> May provide inappropriate responses not representative of GitLab's views. Do not input personal data. </small> </form> </footer> </section> </template> <style lang="scss"> :root { --font-family: var(--vscode-font-family); } * { box-sizing: border-box; } body { padding: 0; } section { font-family: var(--font-family); display: flex; flex-direction: column; height: 100vh; } .header { text-align: center; width: 100%; position: sticky; top: 0; color: var(--vscode-foreground); background: var(--vscode-widget-shadow); padding: 0.5rem; } #chat-history { display: flex; flex-direction: column; flex-grow: 1; padding: 0.5rem; } footer { position: sticky; bottom: 0; border-top: 1px solid var(--vscode-widget-border); padding: 0.5rem; background-color: var(--vscode-sideBar-background); .input-group { position: relative; display: flex; flex-wrap: wrap; align-items: stretch; } .input { display: inline-grid; max-width: 100%; min-height: 2.5rem; flex-grow: 1; box-shadow: inset 0 0 0 1px var(--vscode-settings-textInputBorder); background-color: var(--vscode-input-background); border-radius: 0.25rem; } textarea { width: calc(100% - 40px); max-height: 240px; border: none; outline: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; resize: none; height: 100%; padding: 0.75rem 0.5rem; box-shadow: none; border-radius: 0.25rem; border-bottom-right-radius: 0; border-top-right-radius: 0; color: var(--vscode-input-foreground); background-color: transparent; font-family: var(--font-family); &::placeholder { color: var(--vscode-disabledForeground); } &:focus { box-shadow: none; outline: none; } } .input-group-append { display: flex; button { padding: 0.5rem; background-color: var(--vscode-button-background); box-shadow: inset 0 0 0 1px var(--vscode-button-border); color: var(--vscode-button-foreground); cursor: pointer; border-width: 0; flex-shrink: 0; border: none; border-radius: 0.25rem; position: absolute; bottom: 0.25rem; right: 0.25rem; svg { width: 16px; height: 16px; vertical-align: -3px; fill: currentColor; } } } form small { margin-top: 1rem; color: var(--vscode-disabledForeground); } } </style> webviews/vue2/gitlab_duo_chat/src/components/chat_message.vue 0 → 100644 +157 −0 Original line number Diff line number Diff line <script> import { renderMarkdown } from '../utils'; import { CHAT_MODEL_ROLES } from '../constants'; import vSafeHtmlDirective from '../directives/SafeHtmlDirective'; export default { name: 'DuoChatMessage', props: { message: { type: Object, required: true, }, }, directives: { SafeHtml: vSafeHtmlDirective, }, computed: { messageContent() { return renderMarkdown(this.message.errors[0] || this.message.content); }, isPending() { return this.message.state === 'pending'; }, isAssistantMessage() { return this.message.role.toLowerCase() === CHAT_MODEL_ROLES.assistant; }, isUserMessage() { return this.message.role.toLowerCase() === CHAT_MODEL_ROLES.user; }, isSystemMessage() { return this.message.role.toLowerCase() === CHAT_MODEL_ROLES.system; }, hasErrors() { return this.message.errors.length > 0; }, isNewConversationCommand() { return this.message.type === 'newConversation'; }, }, }; </script> <template> <div> <template v-if="isNewConversationCommand"> <div class="conversation-delimiter"> <hr /> <span>New chat</span> <hr /> </div> </template> <template v-else> <div class="message" :class="{ prompt: isUserMessage, response: isAssistantMessage, system: isSystemMessage, 'with-errors': hasErrors, }" > <template v-if="isPending"> <div class="ai-genie-loader"> <div class="ai-genie-loader__dot ai-genie-loader__dot--1"></div> <div class="ai-genie-loader__dot ai-genie-loader__dot--2"></div> <div class="ai-genie-loader__dot ai-genie-loader__dot--3"></div> </div> </template> <template v-else> <p v-safe-html="messageContent"></p> </template> </div> </template> </div> </template> <style lang="scss"> @keyframes AIGenieLoading { 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1); } } .ai-genie-loader { max-width: unset; &__dot { display: inline-block; width: 0.5rem; height: 0.5rem; background-color: var(--vscode-editor-foreground); border-radius: 100%; animation: AIGenieLoading 1400ms ease-in-out infinite; animation-fill-mode: both; } &__dot--1 { animation-delay: -0.3s; } &__dot--2 { animation-delay: -0.15s; } } .conversation-delimiter { display: flex; align-items: center; gap: 0.75rem; color: var(--vscode-disabledForeground); hr { flex-grow: 1; margin: 1.5rem 0; border: 0; border-top: 1px solid var(--vscode-widget-border); overflow: hidden; } } .message { max-width: 90%; line-height: 1.25rem; word-break: break-word; margin-bottom: 0.75rem; padding: 0.5rem 0.75rem 0.5rem 0.75rem; border-radius: 0.5rem; color: var(--vscode-foreground); pre { overflow-x: auto; border: 1px dotted var(--vscode-editorWidget-border); border-radius: 0.25em; padding: 0.25em; background-color: rgba(255, 255, 255, 0.1); } p { margin-bottom: 0; margin-top: 0; } } .prompt { margin-left: auto; border-bottom-right-radius: 0; background-color: var(--vscode-editor-selectionBackground); } .response { margin-right: auto; border-bottom-left-radius: 0; background-color: var(--vscode-editor-background); } .with-errors { background-color: var(--vscode-inputValidation-errorBackground); } </style> Loading
scripts/utils/browser_jobs.mjs +2 −2 Original line number Diff line number Diff line Loading @@ -5,8 +5,8 @@ import { prepareWebviews, generateAssets, writePackageJson, commonJobs } from '. import { ENVIRONMENTS } from '../constants.mjs'; const browserWebviews = { vue: ['gitlab_duo_chat'], vue2: [], vue: [], vue2: ['gitlab_duo_chat'], }; function typecheck(signal) { Loading
scripts/utils/desktop_jobs.mjs +2 −2 Original line number Diff line number Diff line Loading @@ -7,8 +7,8 @@ import { prepareWebviews, generateAssets, writePackageJson, commonJobs } from '. import { ENVIRONMENTS } from '../constants.mjs'; const desktopWebviews = { vue: ['gitlab_duo_chat', 'issuable'], vue2: [], vue: ['issuable'], vue2: ['gitlab_duo_chat'], }; export async function copyPendingJobAssets() { Loading
webviews/vue2/gitlab_duo_chat/index.html 0 → 100644 +16 −0 Original line number Diff line number Diff line <!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="Content-Security-Policy" content="img-src vscode-resource: https:; script-src 'nonce-{{nonce}}';" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>GitLab Duo Chat</title> </head> <body> <div id="app"></div> <script crossorigin type="module" src="src/main.js"></script> </body> </html>
webviews/vue2/gitlab_duo_chat/src/App.vue 0 → 100644 +252 −0 Original line number Diff line number Diff line <script> import DuoChatMessage from './components/chat_message.vue'; import PaperAirplaneIcon from './components/icons/PaperAirplaneIcon.vue'; const vscode = acquireVsCodeApi(); export default { name: 'GitLabDuoChat', components: { DuoChatMessage, PaperAirplaneIcon, }, data() { return { chatMessages: [], promptDraft: null, }; }, created() { window.vsCodeApi = vscode; window.addEventListener('message', this.handleExtensionMessageEvent); window.vsCodeApi.postMessage({ command: 'appReady' }); }, onBeforeDestroy() { window.removeEventListener('message', this.handleExtensionMessageEvent); }, mounted() { this.attachToVsCodeState(); }, watch: { promptDraft(newVal) { window.vsCodeApi.setState({ promptDraft: newVal, }); }, }, methods: { handleExtensionMessageEvent(event) { const message = event.data; console.log('received', message); switch (message.eventType) { case 'focus': { this.focusInput(); break; } case 'newRecord': { this.newRecord(message.record); break; } case 'updateRecord': { this.updateRecord(message.record); break; } default: console.log('Chat view received unexpected message type.'); break; } }, newRecord(record) { this.chatMessages.push(record); this.scrollBottom(); }, updateRecord(record) { const index = this.chatMessages.findIndex(r => r.id === record.id); this.chatMessages.splice(index, 1, record); this.scrollBottom(); }, scrollBottom() { setTimeout(() => { window.scrollTo(0, document.body.scrollHeight); }, 20); // TODO: replace this hack with proper CSS. }, attachToVsCodeState() { const currentState = window.vsCodeApi.getState(); if (currentState && currentState.promptDraft) { this.promptDraft = currentState.promptDraft; } }, // form related stuff. submitPrompt() { window.vsCodeApi.postMessage({ eventType: 'newPrompt', record: { content: this.promptDraft, }, }); this.promptDraft = null; }, handleKeyPress(event) { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); this.submitPrompt(); } }, focusInput() { this.$refs.promptTextarea.focus(); }, }, }; </script> <template> <section> <div class="header">Responses generated by AI.</div> <div id="chat-history"> <duo-chat-message v-for="message in chatMessages" :key="message.id" :message="message" /> </div> <footer> <form @submit.stop.prevent="submitPrompt"> <div class="input-group"> <div class="input"> <textarea rows="1" placeholder="Ask a question" ref="promptTextarea" v-model="promptDraft" @keypress="handleKeyPress" ></textarea> </div> <div class="input-group-append"> <button type="submit"> <paper-airplane-icon /> </button> </div> </div> <small> May provide inappropriate responses not representative of GitLab's views. Do not input personal data. </small> </form> </footer> </section> </template> <style lang="scss"> :root { --font-family: var(--vscode-font-family); } * { box-sizing: border-box; } body { padding: 0; } section { font-family: var(--font-family); display: flex; flex-direction: column; height: 100vh; } .header { text-align: center; width: 100%; position: sticky; top: 0; color: var(--vscode-foreground); background: var(--vscode-widget-shadow); padding: 0.5rem; } #chat-history { display: flex; flex-direction: column; flex-grow: 1; padding: 0.5rem; } footer { position: sticky; bottom: 0; border-top: 1px solid var(--vscode-widget-border); padding: 0.5rem; background-color: var(--vscode-sideBar-background); .input-group { position: relative; display: flex; flex-wrap: wrap; align-items: stretch; } .input { display: inline-grid; max-width: 100%; min-height: 2.5rem; flex-grow: 1; box-shadow: inset 0 0 0 1px var(--vscode-settings-textInputBorder); background-color: var(--vscode-input-background); border-radius: 0.25rem; } textarea { width: calc(100% - 40px); max-height: 240px; border: none; outline: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; resize: none; height: 100%; padding: 0.75rem 0.5rem; box-shadow: none; border-radius: 0.25rem; border-bottom-right-radius: 0; border-top-right-radius: 0; color: var(--vscode-input-foreground); background-color: transparent; font-family: var(--font-family); &::placeholder { color: var(--vscode-disabledForeground); } &:focus { box-shadow: none; outline: none; } } .input-group-append { display: flex; button { padding: 0.5rem; background-color: var(--vscode-button-background); box-shadow: inset 0 0 0 1px var(--vscode-button-border); color: var(--vscode-button-foreground); cursor: pointer; border-width: 0; flex-shrink: 0; border: none; border-radius: 0.25rem; position: absolute; bottom: 0.25rem; right: 0.25rem; svg { width: 16px; height: 16px; vertical-align: -3px; fill: currentColor; } } } form small { margin-top: 1rem; color: var(--vscode-disabledForeground); } } </style>
webviews/vue2/gitlab_duo_chat/src/components/chat_message.vue 0 → 100644 +157 −0 Original line number Diff line number Diff line <script> import { renderMarkdown } from '../utils'; import { CHAT_MODEL_ROLES } from '../constants'; import vSafeHtmlDirective from '../directives/SafeHtmlDirective'; export default { name: 'DuoChatMessage', props: { message: { type: Object, required: true, }, }, directives: { SafeHtml: vSafeHtmlDirective, }, computed: { messageContent() { return renderMarkdown(this.message.errors[0] || this.message.content); }, isPending() { return this.message.state === 'pending'; }, isAssistantMessage() { return this.message.role.toLowerCase() === CHAT_MODEL_ROLES.assistant; }, isUserMessage() { return this.message.role.toLowerCase() === CHAT_MODEL_ROLES.user; }, isSystemMessage() { return this.message.role.toLowerCase() === CHAT_MODEL_ROLES.system; }, hasErrors() { return this.message.errors.length > 0; }, isNewConversationCommand() { return this.message.type === 'newConversation'; }, }, }; </script> <template> <div> <template v-if="isNewConversationCommand"> <div class="conversation-delimiter"> <hr /> <span>New chat</span> <hr /> </div> </template> <template v-else> <div class="message" :class="{ prompt: isUserMessage, response: isAssistantMessage, system: isSystemMessage, 'with-errors': hasErrors, }" > <template v-if="isPending"> <div class="ai-genie-loader"> <div class="ai-genie-loader__dot ai-genie-loader__dot--1"></div> <div class="ai-genie-loader__dot ai-genie-loader__dot--2"></div> <div class="ai-genie-loader__dot ai-genie-loader__dot--3"></div> </div> </template> <template v-else> <p v-safe-html="messageContent"></p> </template> </div> </template> </div> </template> <style lang="scss"> @keyframes AIGenieLoading { 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1); } } .ai-genie-loader { max-width: unset; &__dot { display: inline-block; width: 0.5rem; height: 0.5rem; background-color: var(--vscode-editor-foreground); border-radius: 100%; animation: AIGenieLoading 1400ms ease-in-out infinite; animation-fill-mode: both; } &__dot--1 { animation-delay: -0.3s; } &__dot--2 { animation-delay: -0.15s; } } .conversation-delimiter { display: flex; align-items: center; gap: 0.75rem; color: var(--vscode-disabledForeground); hr { flex-grow: 1; margin: 1.5rem 0; border: 0; border-top: 1px solid var(--vscode-widget-border); overflow: hidden; } } .message { max-width: 90%; line-height: 1.25rem; word-break: break-word; margin-bottom: 0.75rem; padding: 0.5rem 0.75rem 0.5rem 0.75rem; border-radius: 0.5rem; color: var(--vscode-foreground); pre { overflow-x: auto; border: 1px dotted var(--vscode-editorWidget-border); border-radius: 0.25em; padding: 0.25em; background-color: rgba(255, 255, 255, 0.1); } p { margin-bottom: 0; margin-top: 0; } } .prompt { margin-left: auto; border-bottom-right-radius: 0; background-color: var(--vscode-editor-selectionBackground); } .response { margin-right: auto; border-bottom-left-radius: 0; background-color: var(--vscode-editor-background); } .with-errors { background-color: var(--vscode-inputValidation-errorBackground); } </style>