Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mqttx-cli",
"version": "1.11.1",
"version": "1.12.0-beta.1",
"description": "MQTTX Command Line Tools",
"keywords": [
"mqtt",
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "MQTTX",
"version": "1.11.1",
"version": "1.12.0-beta.1",
"description": "MQTT desktop client",
"author": "EMQX Team <yusf@emqx.io>",
"scripts": {
Expand Down Expand Up @@ -42,7 +42,7 @@
"@electron/remote": "^2.1.2",
"@modelcontextprotocol/sdk": "^1.7.0",
"abort-controller": "^3.0.0",
"ai": "^4.1.46",
"ai": "^4.2.0",
"appdata-path": "^1.0.0",
"avsc": "^5.7.7",
"axios": "^0.21.2",
Expand Down
17 changes: 12 additions & 5 deletions src/components/ai/Copilot.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
:messages="messages"
:is-sending="isSending"
:response-stream-text="responseStreamText"
:reasoning="reasoning"
@load-more-messages="loadMoreMessages"
/>
<copilot-input
Expand Down Expand Up @@ -63,6 +64,7 @@ export default class Copilot extends Vue {
private responseStreamText = ''
private currPresetPrompt = ''
private abortController: AbortController | null = null
private reasoning = ''

private sessionManager = new SessionManager()
private aiAgent: AIAgent | null = null
Expand Down Expand Up @@ -151,6 +153,9 @@ export default class Copilot extends Vue {
onAbort: () => {
this.resetResponseState()
},
onReasoningChunk: (reasoning) => {
this.reasoning = reasoning
},
})
this.$log.info(`[Copilot] AI Agent initialized with Model: "${this.model}" from "${this.openAIAPIHost}"`)
}
Expand Down Expand Up @@ -247,6 +252,7 @@ export default class Copilot extends Vue {
this.isSending = false
this.isResponseStream = false
this.currPresetPrompt = ''
this.reasoning = ''
this.scrollToBottom()
}

Expand Down Expand Up @@ -367,7 +373,7 @@ export default class Copilot extends Vue {
*/
private async processMCPResult(): Promise<void> {
if (this.messages.length === 0 || !this.aiAgent) return
const lastMessage = this.messages[this.messages.length - 1]
const [lastUserMessage, lastAssistantMessage] = this.messages.slice(-2)

this.isSending = true
this.isResponseStream = true
Expand All @@ -379,12 +385,13 @@ export default class Copilot extends Vue {
this.responseStreamText = ''
this.isResponseStream = false
this.isSending = false
this.updateMessageInUI(lastMessage.id, lastMessage.content + '\n\n' + text)
this.updateMessageInUI(lastAssistantMessage.id, lastAssistantMessage.content + '\n\n' + text)
}

const initialHistory: CopilotMessage[] = buildMCPAnalysisMessages(
this.currentLang,
lastMessage.content,
lastUserMessage.content,
lastAssistantMessage.content,
this.$tc('copilot.mcpResultAnalysisPrompts'),
)

Expand All @@ -398,13 +405,13 @@ export default class Copilot extends Vue {
content: cleanedContent,
}
await copilotService.update(cleanedMessage)
this.updateMessageInUI(lastMessage.id, cleanedContent)
this.updateMessageInUI(lastAssistantMessage.id, cleanedContent)
this.isResponseStream = false
this.isSending = false
}
try {
await this.aiAgent.chatLoop(initialHistory, {
existingMessage: lastMessage,
existingMessage: lastAssistantMessage,
shouldContinue,
stopCondition,
updateCallback: updateCallbackHandler,
Expand Down
2 changes: 1 addition & 1 deletion src/components/ai/CopilotHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div class="copilot-header clearfix">
<span>MQTTX Copilot <el-tag size="mini" type="info">Beta</el-tag></span>
<div>
<el-button type="text" @click="clearAllMessages"><i class="el-icon-delete"></i></el-button>
<el-button type="text" @click="clearAllMessages"><i class="el-icon-plus"></i></el-button>
<el-button type="text" @click="toggleWindow"><i class="el-icon-close"></i></el-button>
</div>
</div>
Expand Down
7 changes: 6 additions & 1 deletion src/components/ai/CopilotMessages.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<i :class="[message.role === 'user' ? 'el-icon-user' : 'el-icon-magic-stick']"></i>
{{ roleMap[message.role] }}
</span>
<copilot-thinking v-if="message.role === 'assistant' && message.reasoning" :reasoning="message.reasoning" />
<message-render
class="chat-content"
:content="message.content"
Expand All @@ -29,11 +30,12 @@
</div>

<!-- Response Stream Text -->
<div v-if="responseStreamText">
<div v-if="responseStreamText || reasoning">
<span class="chat-title">
<i class="el-icon-magic-stick"></i>
<span>MQTTX Copilot</span>
</span>
<copilot-thinking v-if="reasoning" :reasoning="reasoning" />
<message-render
class="chat-content"
:content="responseStreamText"
Expand All @@ -57,12 +59,14 @@ import { ipcRenderer } from 'electron'
import { CopilotMessage } from '@/types/copilot'
import CopilotWelcome from './CopilotWelcome.vue'
import MessageRender from './MessageRender.vue'
import CopilotThinking from './CopilotThinking.vue'

@Component({
components: {
VueMarkdown,
CopilotWelcome,
MessageRender,
CopilotThinking,
},
})
export default class CopilotMessages extends Vue {
Expand All @@ -72,6 +76,7 @@ export default class CopilotMessages extends Vue {
@Prop({ default: () => [] }) readonly messages!: CopilotMessage[]
@Prop({ default: false }) readonly isSending!: boolean
@Prop({ default: '' }) readonly responseStreamText!: string
@Prop({ default: '' }) readonly reasoning!: string

private copyText = String(this.$t('common.copy'))
private copyFailedText = String(this.$t('common.copyFailed'))
Expand Down
116 changes: 116 additions & 0 deletions src/components/ai/CopilotThinking.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<template>
<div v-if="reasoning" class="thinking-container">
<div class="thinking-header" @click="toggleThinking" :class="{ 'is-updating': isUpdating }">
<i :class="[isExpanded ? 'el-icon-arrow-down' : 'el-icon-arrow-right']"></i>
<span>{{ $t('copilot.showThinking') }}</span>
<i v-if="isUpdating" class="el-icon-loading"></i>
</div>
<div v-show="isExpanded" class="thinking-content">
<message-render :content="reasoning" />
</div>
</div>
</template>

<script lang="ts">
import { Component, Vue, Prop, Watch } from 'vue-property-decorator'
import MessageRender from './MessageRender.vue'

@Component({
components: {
MessageRender,
},
})
export default class CopilotThinking extends Vue {
@Prop({ default: '' }) readonly reasoning!: string

private isExpanded: boolean = false
private isUpdating: boolean = false
private updateTimeout: number | null = null
private copyText = String(this.$t('common.copy'))
private copyFailedText = String(this.$t('common.copyFailed'))
private copiedText = String(this.$t('common.copied'))
private lastReasoning = ''

private toggleThinking(): void {
this.isExpanded = !this.isExpanded
this.$nextTick(() => {
this.$emit('thinking-toggled', this.isExpanded)
})
}

@Watch('reasoning')
onReasoningChange() {
this.isUpdating = true

if (this.updateTimeout !== null) {
window.clearTimeout(this.updateTimeout)
}

this.updateTimeout = window.setTimeout(() => {
if (this.reasoning === this.lastReasoning) {
this.isUpdating = false
}
}, 1000)

this.lastReasoning = this.reasoning

if (this.isExpanded) {
this.$nextTick(() => {
const content = this.$el.querySelector('.thinking-content')
if (content) {
content.scrollTop = content.scrollHeight
}
})
}
}

private formatReasoning(reasoning: string): string {
if (!reasoning) return ''
return reasoning
}

beforeDestroy() {
if (this.updateTimeout !== null) {
window.clearTimeout(this.updateTimeout)
}
}
}
</script>

<style lang="scss">
.thinking-container {
margin: 12px 0;

.thinking-header {
display: flex;
align-items: center;
cursor: pointer;
color: var(--color-text-tips);
margin-bottom: 8px;
font-size: 14px;
position: relative;
transition: color 0.3s ease;
&.is-updating {
i.el-icon-loading {
margin-left: 12px;
}
}
i {
margin-right: 6px;
}
}

.thinking-content {
padding: 0 16px;
background-color: var(--color-bg-primary);
border-radius: 8px;
overflow-x: scroll;
max-height: 320px;
overflow-y: auto;
font-size: 12px;
font-style: italic;
color: var(--color-text-tips);
line-height: 1.5;
}
}
</style>
2 changes: 2 additions & 0 deletions src/database/database.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { supportOpenAIAPIHost1716044120271 } from './migration/1716044120271-sup
import { ignoreQoS0Message1724839386732 } from './migration/1724839386732-ignoreQoS0Message'
import { changeDefaultLLMModel1727111519962 } from './migration/1727111519962-changeDefaultLLMModel'
import { topicNodeTables1729246737362 } from './migration/1729246737362-topicNodeTables'
import { reasonModelSupport1742835643809 } from './migration/1742835643809-reasonModelSupport'

const STORE_PATH = getAppDataPath('MQTTX')
try {
Expand Down Expand Up @@ -103,6 +104,7 @@ const ORMConfig = {
ignoreQoS0Message1724839386732,
changeDefaultLLMModel1727111519962,
topicNodeTables1729246737362,
reasonModelSupport1742835643809,
],
migrationsTableName: 'temp_migration_table',
entities: [
Expand Down
58 changes: 58 additions & 0 deletions src/database/migration/1742835643809-reasonModelSupport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { MigrationInterface, QueryRunner } from 'typeorm'

export class reasonModelSupport1742835643809 implements MigrationInterface {
name = 'reasonModelSupport1742835643809'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE "temporary_CopilotEntity" (
"id" varchar PRIMARY KEY NOT NULL,
"role" varchar NOT NULL,
"content" text NOT NULL,
"createAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
"reasoning" text
)
`)
await queryRunner.query(`
INSERT INTO "temporary_CopilotEntity"("id", "role", "content", "createAt")
SELECT "id",
"role",
"content",
"createAt"
FROM "CopilotEntity"
`)
await queryRunner.query(`
DROP TABLE "CopilotEntity"
`)
await queryRunner.query(`
ALTER TABLE "temporary_CopilotEntity"
RENAME TO "CopilotEntity"
`)
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "CopilotEntity"
RENAME TO "temporary_CopilotEntity"
`)
await queryRunner.query(`
CREATE TABLE "CopilotEntity" (
"id" varchar PRIMARY KEY NOT NULL,
"role" varchar NOT NULL,
"content" text NOT NULL,
"createAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
)
`)
await queryRunner.query(`
INSERT INTO "CopilotEntity"("id", "role", "content", "createAt")
SELECT "id",
"role",
"content",
"createAt"
FROM "temporary_CopilotEntity"
`)
await queryRunner.query(`
DROP TABLE "temporary_CopilotEntity"
`)
}
}
3 changes: 3 additions & 0 deletions src/database/models/CopilotEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@ export default class CopilotEntity {

@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
createAt!: string

@Column({ type: 'text', nullable: true })
reasoning?: string
}
7 changes: 7 additions & 0 deletions src/lang/copilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ export default {
ja: 'MCP(モデルコンテキストプロトコル)機能を有効にしてみてください!MCPはCopilotにさまざまな外部ツールやサービスを使用することを可能にし、より効率的なテスト、自動化、開発タスクをサポートするためにその機能を大幅に拡張します。',
hu: 'Próbálja ki az MCP (Model Context Protocol) funkció engedélyezését! Az MCP lehetővé teszi a Copilot számára különféle külső eszközök és szolgáltatások használatát, jelentősen bővítve képességeit, hogy segítsen a hatékonyabb tesztelésben, automatizálásban és fejlesztési feladatokban.',
},
showThinking: {
zh: '查看思考过程',
en: 'View Thinking Process',
tr: 'Düşünme Sürecini Görüntüle',
ja: '思考プロセスを表示',
hu: 'Gondolkodási folyamat megtekintése',
},
copiltePubMsgPlacehoder: {
zh: '向 MQTTX Copilot 发送消息...',
en: 'Message MQTTX Copilot...',
Expand Down
2 changes: 2 additions & 0 deletions src/types/copilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface CopilotMessage {
id: string
role: CopilotRole
content: string
reasoning?: string
}

/**
Expand Down Expand Up @@ -205,6 +206,7 @@ export interface AIStreamOptions {
topP?: number
frequencyPenalty?: number
presencePenalty?: number
disabledReasoningShow?: boolean
}

export interface ChatLoopOptions {
Expand Down
Loading