perf: add insert event (#5052)

* perf: add insert event

perf: receive insert code

perf: add content

perf: update code

perf: save panel to LocalStorage

* perf: only terminal add insert action

---------

Co-authored-by: Eric <xplzv@126.com>
This commit is contained in:
fit2bot 2025-06-17 19:23:19 +08:00 committed by GitHub
parent 8f17fa82a2
commit d72671d2c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 175 additions and 37 deletions

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="container"> <div class="container">
<div class="chat-action"> <div v-if="hasPrompt" class="chat-action">
<Select2 <Select2
v-model="select.value" v-model="select.value"
:disabled="isLoading || isSelectDisabled" :disabled="isLoading || isSelectDisabled"
@ -36,6 +36,10 @@ export default {
expanded: { expanded: {
type: Boolean, type: Boolean,
default: false default: false
},
hasPrompt: {
type: Boolean,
default: true
} }
}, },
data() { data() {

View File

@ -33,7 +33,7 @@
<!-- eslint-disable-next-line --> <!-- eslint-disable-next-line -->
<div class="divider"></div> <div class="divider"></div>
<p> <p>
<MessageText :message="item.reasoning" /> <MessageText :message="item.reasoning" @insert-code="handleInsertCode" />
</p> </p>
</div> </div>
@ -41,8 +41,7 @@
<span v-if="isServerError" class="error"> <span v-if="isServerError" class="error">
{{ isServerError }} {{ isServerError }}
</span> </span>
<MessageText :message="item.result" /> <MessageText :message="item.result" :is-terminal="isTerminal" @insert-code="handleInsertCode" /></div>
</div>
</div> </div>
</div> </div>
<div class="action"> <div class="action">
@ -93,6 +92,10 @@ export default {
type: Object, type: Object,
default: () => { default: () => {
} }
},
isTerminal: {
type: Boolean,
default: false
} }
}, },
data() { data() {
@ -142,6 +145,9 @@ export default {
if (value === 'copy') { if (value === 'copy') {
copy(this.item.result.content) copy(this.item.result.content)
} }
},
handleInsertCode(code) {
this.$emit('insert-code', code)
} }
} }
} }

View File

@ -25,6 +25,10 @@ export default {
type: Object, type: Object,
default: () => { default: () => {
} }
},
isTerminal: {
type: Boolean,
default: false
} }
}, },
data() { data() {
@ -45,10 +49,10 @@ export default {
this.init() this.init()
}, },
updated() { updated() {
this.addCopyEvents() this.addEvents()
}, },
destroyed() { destroyed() {
this.removeCopyEvents() this.removeEvents()
}, },
methods: { methods: {
init() { init() {
@ -69,26 +73,64 @@ export default {
this.markdown.use(mdKatex, { blockClass: 'katexmath-block rounded-md', errorColor: ' #cc0000' }) this.markdown.use(mdKatex, { blockClass: 'katexmath-block rounded-md', errorColor: ' #cc0000' })
}, },
highlightBlock(str, lang) { highlightBlock(str, lang) {
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">${'Copy'}</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>` let insertSpanHmtl = `<span class="code-block-header__insert">${this.$t('insert')}</span>`
if (!this.isTerminal) {
insertSpanHmtl = ''
}
return `<pre class="code-block-wrapper">
<div class="code-block-header">
<span class="code-block-header__lang">${lang}</span>
<span class="code-block-header__actions">
${insertSpanHmtl}
<span class="code-block-header__copy">${'Copy'}</span>
</span>
</div>
<code class="hljs code-block-body ${lang}">${str}</code></pre>`
}, },
addCopyEvents() { addEvents() {
const copyBtn = document.querySelectorAll('.code-block-header__copy') this.addBtnClickEvents('.code-block-header__copy', this.handlerClickCopy)
copyBtn.forEach((btn) => { this.addBtnClickEvents('.code-block-header__insert', this.handlerClickInsert)
btn.addEventListener('click', () => { },
const code = btn.parentElement?.nextElementSibling?.textContent
if (code) { handlerClickCopy(event) {
copy(code) const wrapper = event.target.closest('.code-block-wrapper')
} if (wrapper) {
// code
const codeElement = wrapper.querySelector('code.code-block-body')
if (codeElement) {
const codeText = codeElement.textContent
copy(codeText)
}
}
},
handlerClickInsert(event) {
const wrapper = event.target.closest('.code-block-wrapper')
if (wrapper) {
// code
const codeElement = wrapper.querySelector('code.code-block-body')
if (codeElement) {
const codeText = codeElement.textContent
this.$emit('insert-code', codeText)
}
}
},
addBtnClickEvents(selector, callback) {
const buttons = this.$refs.textRef.querySelectorAll(selector)
buttons.forEach((btn) => {
btn.addEventListener('click', callback)
})
},
removeBtnClickEvent(selector) {
const buttons = this.$refs.textRef.querySelectorAll(selector)
buttons.forEach((btn) => {
btn.removeEventListener('click', () => {
}) })
}) })
}, },
removeCopyEvents() { removeEvents() {
if (this.$refs.textRef) { if (this.$refs.textRef) {
const copyBtn = this.$refs.textRef.querySelectorAll('.code-block-header__copy') this.removeBtnClickEvent('.code-block-header__copy')
copyBtn.forEach((btn) => { this.addBtnClickEvents('.code-block-header__insert')
btn.removeEventListener('click', () => {
})
})
} }
} }
} }
@ -98,6 +140,7 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.markdown-body { .markdown-body {
font-size: 13px; font-size: 13px;
max-width: 300px;;
&::v-deep p { &::v-deep p {
margin-bottom: 0 !important; margin-bottom: 0 !important;
@ -115,26 +158,46 @@ export default {
&::v-deep .code-block-wrapper { &::v-deep .code-block-wrapper {
background: #1F2329; background: #1F2329;
padding: 2px 6px; padding: 0;
margin: 5px 0; margin: 5px 0;
display: flex;
flex-direction: column;
overflow: hidden;
.code-block-body { .code-block-body {
padding: 5px 10px 0; padding: 5px 10px;
} }
;
.code-block-header { .code-block-header {
margin-bottom: 4px; margin-bottom: 4px;
overflow: hidden; overflow: hidden;
background: #353946; background: #353946;
color: #c2d1e1; color: #c2d1e1;
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 8px;
width: 100%;
box-sizing: border-box;
.code-block-header__copy { .code-block-header__actions {
float: right; display: flex;
cursor: pointer; gap: 8px;
&:hover { .code-block-header__copy {
color: #6e747b; cursor: pointer;
&:hover {
color: #6e747b;
}
}
.code-block-header__insert {
cursor: pointer;
&:hover {
color: #6e747b;
}
} }
} }
} }
@ -178,6 +241,7 @@ export default {
0% { 0% {
opacity: 1; opacity: 1;
} }
100% { 100% {
opacity: 0; opacity: 0;
} }

View File

@ -17,7 +17,7 @@
</div> </div>
</div> </div>
</div> </div>
<ChatMessage v-for="(item, index) in activeChat.chats" :key="index" :item="item" /> <ChatMessage v-for="(item, index) in activeChat.chats" :key="index" :item="item" :is-terminal="isTerminal" @insert-code="insertCode" />
</div> </div>
<div class="input-box"> <div class="input-box">
<el-button <el-button
@ -28,7 +28,7 @@
size="small" size="small"
@click="onStopHandle" @click="onStopHandle"
>{{ $tc('Stop') }}</el-button> >{{ $tc('Stop') }}</el-button>
<ChatInput ref="chatInput" :expanded="expanded" @send="onSendHandle" @select-prompt="onSelectPromptHandle" /> <ChatInput ref="chatInput" :expanded="expanded" :has-prompt="!isTerminal" @send="onSendHandle" @select-prompt="onSelectPromptHandle" />
</div> </div>
</div> </div>
</template> </template>
@ -68,7 +68,10 @@ export default {
prompt: '', prompt: '',
conversationId: '', conversationId: '',
showIntroduction: false, showIntroduction: false,
introduction: [] introduction: [],
terminalContext: null,
isTerminal: false,
sessionChat: {}
} }
}, },
computed: { computed: {
@ -100,6 +103,9 @@ export default {
this.showIntroduction = true this.showIntroduction = true
this.conversationId = '' this.conversationId = ''
this.$refs.chatInput.select.value = '' this.$refs.chatInput.select.value = ''
if (this.terminalContext) {
this.prompt = this.terminalContext.content || ''
}
const chat = { const chat = {
message: { message: {
content: this.$t('ChatHello'), content: this.$t('ChatHello'),
@ -150,6 +156,32 @@ export default {
addMessageToActiveChat(data) addMessageToActiveChat(data)
setLoading(true) setLoading(true)
}, },
onTerminalContext(terminalContext) {
const originSessionId = this.terminalContext?.sessionId
const newSessionId = terminalContext.sessionId || ''
if (originSessionId) {
this.saveSessionChat(originSessionId)
}
this.terminalContext = terminalContext
this.isTerminal = true
this.prompt = terminalContext.content || ''
if (originSessionId !== newSessionId) {
if (this.sessionChat[newSessionId]) {
clearChats()
for (const chat of this.sessionChat[newSessionId]) {
addChatMessageById(chat)
}
} else {
this.onNewChat()
}
}
},
saveSessionChat(sessionId) {
if (this.terminalContext) {
this.sessionChat[sessionId] = JSON.parse(JSON.stringify(this.activeChat.chats))
}
},
onSendHandle(value) { onSendHandle(value) {
this.showIntroduction = false this.showIntroduction = false
this.socket = ws || {} this.socket = ws || {}
@ -204,6 +236,15 @@ export default {
sendIntroduction(item) { sendIntroduction(item) {
this.showIntroduction = false this.showIntroduction = false
this.onSendHandle(item.content) this.onSendHandle(item.content)
},
insertCode(code) {
this.sendPostMessage({
name: 'INSERT_TERMINAL_CODE',
data: code.replace(/^[\s\r\n]+|[\s\r\n]+$/g, '')
})
},
sendPostMessage(data) {
window.parent.postMessage(data)
} }
} }
} }

View File

@ -47,7 +47,9 @@ import Chat from './components/ChitChat/index.vue'
import { getInputFocus } from './useChat.js' import { getInputFocus } from './useChat.js'
import { ws } from '@/utils/socket' import { ws } from '@/utils/socket'
import DrawerPanel from '@/components/Apps/DrawerPanel/index.vue' import DrawerPanel from '@/components/Apps/DrawerPanel/index.vue'
import { ObjectLocalStorage } from '@/utils/common'
const aiPannelLocalStorage = new ObjectLocalStorage('ai_panel_settings')
export default { export default {
components: { components: {
DrawerPanel, DrawerPanel,
@ -76,12 +78,15 @@ export default {
robotUrl: require('@/assets/img/robot-assistant.png'), robotUrl: require('@/assets/img/robot-assistant.png'),
height: '400px', height: '400px',
expanded: false, expanded: false,
clientOffset: {} clientOffset: {},
currentTerminalContent: {}
} }
}, },
watch: { watch: {
}, },
mounted() { mounted() {
const expanded = aiPannelLocalStorage.get('expanded')
this.updateExpandedState(expanded)
this.handlePostMessage() this.handlePostMessage()
}, },
methods: { methods: {
@ -90,6 +95,16 @@ export default {
if (event.data === 'show-chat-panel') { if (event.data === 'show-chat-panel') {
this.$refs.drawer.show = true this.$refs.drawer.show = true
this.initWebSocket() this.initWebSocket()
return
}
const msg = event.data
switch (msg.name) {
case 'current_terminal_content':
// {content: '...', terminalId: '',sessionId: '',viewId: '',viewName: ''}
this.$log.debug('current_terminal_content', msg)
this.currentTerminalContent = msg.data
this.$refs.component?.onTerminalContext(msg.data)
break
} }
}) })
}, },
@ -113,12 +128,20 @@ export default {
this.$refs.drawer.show = false this.$refs.drawer.show = false
}, },
expandFull() { expandFull() {
this.height = '100%' this.updateExpandedState(true)
this.expanded = true this.save_pannel_settings()
}, },
compress() { compress() {
this.height = '400px' this.updateExpandedState(false)
this.expanded = false this.save_pannel_settings()
},
save_pannel_settings() {
aiPannelLocalStorage.set('expanded', this.expanded)
console.log('AI panel settings saved:', this.expanded)
},
updateExpandedState(expanded) {
this.expanded = expanded
this.height = expanded ? '100%' : '400px'
}, },
onNewChat() { onNewChat() {
this.active = 'chat' this.active = 'chat'