mirror of
https://github.com/fofolee/uTools-quickcommand.git
synced 2025-10-10 07:23:23 +08:00
代码编辑器添加AI助手,支持AI代码编写
This commit is contained in:
@@ -21,7 +21,6 @@
|
||||
v-if="!isRunCodePage"
|
||||
v-model="commandManager.state.currentCommand"
|
||||
from="quickcommand"
|
||||
@update:is-expanded="isConfigExpanded = $event"
|
||||
:expand-on-focus="true"
|
||||
class="command-config"
|
||||
/>
|
||||
@@ -97,7 +96,6 @@ export default {
|
||||
programLanguages: Object.keys(programs),
|
||||
showComposer: false,
|
||||
listener: null,
|
||||
isConfigExpanded: false,
|
||||
composerInfo: {
|
||||
program: "quickcomposer",
|
||||
},
|
||||
|
90
src/components/ai/AISelector.vue
Normal file
90
src/components/ai/AISelector.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div>
|
||||
<q-select
|
||||
v-if="apiOptions.length > 0"
|
||||
:model-value="modelValue"
|
||||
@update:model-value="updateModelValue($event)"
|
||||
:options="apiOptions"
|
||||
map-options
|
||||
emit-value
|
||||
dense
|
||||
options-dense
|
||||
filled
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-badge color="primary" text-color="white" class="q-mr-sm q-pa-xs">
|
||||
模型
|
||||
</q-badge>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<q-btn icon="settings" dense flat @click.stop="showAIConfig = true" />
|
||||
</template>
|
||||
</q-select>
|
||||
<q-btn
|
||||
dense
|
||||
color="primary"
|
||||
class="full-width q-px-sm"
|
||||
icon="settings"
|
||||
label="配置AI接口"
|
||||
unelevated
|
||||
v-else
|
||||
@click="showAIConfig = true"
|
||||
/>
|
||||
<q-dialog v-model="showAIConfig">
|
||||
<AIConfig @save="onAIConfigSave" />
|
||||
</q-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AIConfig from "components/popup/AIConfig.vue";
|
||||
import { dbManager } from "js/utools.js";
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: { AIConfig },
|
||||
emits: ["update:modelValue"],
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showAIConfig: false,
|
||||
apiOptions: [],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onAIConfigSave() {
|
||||
this.apiOptions = this.getApiOptions();
|
||||
const newApiConfig = this.apiOptions.find(
|
||||
(option) => option.value.id === this.modelValue.id
|
||||
);
|
||||
const newModelValue =
|
||||
newApiConfig?.value || this.apiOptions[0].value || {};
|
||||
this.updateModelValue({ ...newModelValue });
|
||||
},
|
||||
updateModelValue(value) {
|
||||
this.$emit("update:modelValue", value);
|
||||
},
|
||||
getApiOptions() {
|
||||
const apiConfigs = dbManager.getStorage("cfg_aiConfigs");
|
||||
if (!apiConfigs) return [];
|
||||
return apiConfigs.map((config) => {
|
||||
return {
|
||||
label: config.name,
|
||||
value: config,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.apiOptions = this.getApiOptions();
|
||||
if (!this.modelValue.id) {
|
||||
this.updateModelValue(this.apiOptions[0]?.value || {});
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
@@ -1,42 +1,26 @@
|
||||
<template>
|
||||
<div>
|
||||
<q-select
|
||||
v-if="apiOptions.length > 0"
|
||||
:model-value="argvs.apiConfig"
|
||||
@update:model-value="updateArgvs('apiConfig', $event)"
|
||||
:options="apiOptions"
|
||||
map-options
|
||||
emit-value
|
||||
dense
|
||||
options-dense
|
||||
filled
|
||||
label="API模型"
|
||||
class="q-mb-sm"
|
||||
/>
|
||||
<q-field filled dense v-else class="q-mb-sm">
|
||||
<template #control>
|
||||
<div class="flex items-center justify-center full-width text-warning">
|
||||
<q-icon name="warning" class="q-mr-sm" />
|
||||
<div>
|
||||
未配置API模型,配置方法:命令配置界面-右下角菜单按钮-API配置
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</q-field>
|
||||
<ButtonGroup
|
||||
:model-value="argvs.content.presetPrompt"
|
||||
@update:modelValue="updateArgvs('content.presetPrompt', $event)"
|
||||
:options="presetPromptOptions"
|
||||
height="26px"
|
||||
class="q-mb-sm"
|
||||
/>
|
||||
<VariableInput
|
||||
:model-value="argvs.content.prompt"
|
||||
@update:modelValue="updateArgvs('content.prompt', $event)"
|
||||
label="提示词"
|
||||
type="textarea"
|
||||
autogrow
|
||||
/>
|
||||
<div class="q-pt-sm">
|
||||
<AISelector
|
||||
:model-value="argvs.apiConfig"
|
||||
@update:modelValue="updateArgvs('apiConfig', $event)"
|
||||
class="q-mb-sm"
|
||||
/>
|
||||
<ButtonGroup
|
||||
:model-value="argvs.content.role"
|
||||
@update:modelValue="updateArgvs('content.role', $event)"
|
||||
:options="roleOptions"
|
||||
height="26px"
|
||||
class="q-mb-sm"
|
||||
/>
|
||||
<VariableInput
|
||||
:model-value="argvs.content.prompt"
|
||||
@update:modelValue="updateArgvs('content.prompt', $event)"
|
||||
label="提示词"
|
||||
type="textarea"
|
||||
autogrow
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -46,8 +30,7 @@ import ButtonGroup from "components/composer/common/ButtonGroup.vue";
|
||||
import { newVarInputVal } from "js/composer/varInputValManager";
|
||||
import VariableInput from "components/composer/common/VariableInput.vue";
|
||||
import { parseFunction, stringifyArgv } from "js/composer/formatString";
|
||||
import { dbManager } from "js/utools.js";
|
||||
|
||||
import AISelector from "components/ai/AISelector.vue";
|
||||
export default defineComponent({
|
||||
name: "AskAIEditor",
|
||||
props: {
|
||||
@@ -56,25 +39,26 @@ export default defineComponent({
|
||||
components: {
|
||||
VariableInput,
|
||||
ButtonGroup,
|
||||
AISelector,
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
data() {
|
||||
return {
|
||||
showAIConfig: false,
|
||||
defaultArgvs: {
|
||||
content: {
|
||||
prompt: newVarInputVal("str"),
|
||||
presetPrompt: "",
|
||||
role: "",
|
||||
},
|
||||
apiConfig: {},
|
||||
},
|
||||
apiOptions: [],
|
||||
presetPromptOptions: [
|
||||
{ label: "自由问答", value: "" },
|
||||
roleOptions: [
|
||||
{ label: "无", value: "" },
|
||||
{ label: "翻译", value: "translate" },
|
||||
{ label: "总结", value: "summarize" },
|
||||
{ label: "执行shell命令", value: "shell" },
|
||||
{ label: "生成shell命令", value: "shell" },
|
||||
],
|
||||
modelTypeOptions: [
|
||||
apiTypeOptions: [
|
||||
{ label: "OpenAI", value: "openai" },
|
||||
{ label: "Ollama", value: "ollama" },
|
||||
],
|
||||
@@ -125,23 +109,6 @@ export default defineComponent({
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const apiConfigs = dbManager.getStorage("cfg_aiConfigs");
|
||||
this.apiOptions = apiConfigs
|
||||
? apiConfigs.map((config) => {
|
||||
return {
|
||||
label: config.name,
|
||||
value: config,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
this.defaultArgvs.apiConfig = apiConfigs?.[0] || {};
|
||||
|
||||
const argvs = this.modelValue.argvs || this.defaultArgvs;
|
||||
if (!this.modelValue.code) {
|
||||
this.updateModelValue(argvs);
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
404
src/components/editor/AIAssistantDialog.vue
Normal file
404
src/components/editor/AIAssistantDialog.vue
Normal file
@@ -0,0 +1,404 @@
|
||||
<template>
|
||||
<q-card class="ai-dialog">
|
||||
<div class="header q-px-md q-py-sm">
|
||||
<q-icon name="smart_toy" size="24px" />
|
||||
<div class="text-h6">AI 助手</div>
|
||||
<AISelector v-model="selectedApi" />
|
||||
<q-space />
|
||||
<q-btn icon="close" flat round dense v-close-popup size="md" />
|
||||
</div>
|
||||
|
||||
<!-- 聊天记录区域 -->
|
||||
<q-scroll-area
|
||||
ref="scrollArea"
|
||||
class="chat-container"
|
||||
:vertical-thumb-style="{
|
||||
width: '5px',
|
||||
}"
|
||||
>
|
||||
<div class="chat-history q-px-md">
|
||||
<div
|
||||
v-for="(message, index) in chatHistory"
|
||||
:key="index"
|
||||
class="chat-message-wrapper"
|
||||
>
|
||||
<div :class="['chat-message', message.role]">
|
||||
<div class="avatar">
|
||||
<q-avatar size="28px">
|
||||
<q-icon
|
||||
:name="message.role === 'user' ? 'person' : 'smart_toy'"
|
||||
:color="message.role === 'user' ? 'white' : 'primary'"
|
||||
size="20px"
|
||||
/>
|
||||
</q-avatar>
|
||||
</div>
|
||||
<div class="message-bubble">
|
||||
<div
|
||||
v-if="message.role === 'assistant'"
|
||||
class="message-content markdown"
|
||||
v-html="getTrimContent(message.content)"
|
||||
/>
|
||||
<div v-else class="message-content" v-text="message.content" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-scroll-area>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<div class="input-container q-px-md q-py-sm">
|
||||
<q-input
|
||||
v-model="prompt"
|
||||
type="textarea"
|
||||
filled
|
||||
dense
|
||||
autogrow
|
||||
autofocus
|
||||
:max-rows="3"
|
||||
placeholder="请描述你的需求,Enter 发送,Shift+Enter 换行"
|
||||
@keydown.enter.exact.prevent="handleSubmit"
|
||||
@keydown.shift.enter.prevent="prompt += '\n'"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<div class="row items-center q-gutter-x-md">
|
||||
<q-btn
|
||||
flat
|
||||
icon="delete_sweep"
|
||||
size="sm"
|
||||
dense
|
||||
:disable="chatHistory.length === 0"
|
||||
@click="clearHistory"
|
||||
>
|
||||
<q-tooltip>清空对话</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
@click="autoUpdateCode = !autoUpdateCode"
|
||||
:color="autoUpdateCode ? 'primary' : 'grey'"
|
||||
icon="auto_fix_high"
|
||||
size="sm"
|
||||
dense
|
||||
flat
|
||||
>
|
||||
<q-tooltip>
|
||||
{{
|
||||
autoUpdateCode
|
||||
? "自动更新代码(已开启)"
|
||||
: "自动更新代码(已关闭)"
|
||||
}}
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
:color="streamingResponse ? 'negative' : 'primary'"
|
||||
:icon="streamingResponse ? 'stop' : 'send'"
|
||||
size="sm"
|
||||
dense
|
||||
flat
|
||||
@click="handleSubmit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "vue";
|
||||
import AISelector from "components/ai/AISelector.vue";
|
||||
import { marked } from "marked";
|
||||
import DOMPurify from "dompurify";
|
||||
|
||||
const quickcommandApi =
|
||||
require(`!raw-loader!plugins/monaco/types/quickcommand.api.d.ts`)
|
||||
.default.replace(/\/\*[\s\S]*?\*\//g, "")
|
||||
.replace(/\n/g, "");
|
||||
const uToolsApi = require(`!raw-loader!plugins/monaco/types/utools.api.d.ts`)
|
||||
.default.replace(/\/\*[\s\S]*?\*\//g, "")
|
||||
.replace(/\n/g, "");
|
||||
|
||||
export default defineComponent({
|
||||
name: "AIAssistantDialog",
|
||||
components: {
|
||||
AISelector,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
prompt: "",
|
||||
selectedApi: {},
|
||||
streamingResponse: false,
|
||||
chatHistory: [],
|
||||
currentRequest: null,
|
||||
autoUpdateCode: true,
|
||||
scrollToBottomDebounce: null,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
code: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
emits: ["update-code"],
|
||||
methods: {
|
||||
scrollToBottom() {
|
||||
// 清除之前的定时器
|
||||
if (this.scrollToBottomDebounce) {
|
||||
clearTimeout(this.scrollToBottomDebounce);
|
||||
}
|
||||
|
||||
// 设置新的定时器,延迟执行滚动
|
||||
this.scrollToBottomDebounce = setTimeout(() => {
|
||||
const scrollArea = this.$refs.scrollArea;
|
||||
if (scrollArea) {
|
||||
const scrollTarget = scrollArea.getScrollTarget();
|
||||
scrollArea.setScrollPosition(
|
||||
"vertical",
|
||||
scrollTarget.scrollHeight,
|
||||
300
|
||||
);
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
async handleSubmit() {
|
||||
if (this.streamingResponse) {
|
||||
this.stopStreaming();
|
||||
return;
|
||||
}
|
||||
|
||||
const promptText = this.prompt.trim();
|
||||
if (!promptText || !this.selectedApi) return;
|
||||
|
||||
// 添加用户消息到历史记录
|
||||
this.chatHistory.push(
|
||||
{
|
||||
role: "user",
|
||||
content: promptText,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: "",
|
||||
}
|
||||
);
|
||||
|
||||
// 添加消息后立即滚动到底部
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom();
|
||||
});
|
||||
|
||||
this.streamingResponse = true;
|
||||
this.prompt = ""; // 清空输入框
|
||||
|
||||
try {
|
||||
const response = await window.quickcommand.askAI(
|
||||
{
|
||||
prompt: promptText,
|
||||
role: this.getRolePrompt(this.language),
|
||||
context: this.chatHistory.slice(0, -2),
|
||||
},
|
||||
this.selectedApi,
|
||||
{
|
||||
showLoadingBar: false,
|
||||
stream: true,
|
||||
onStream: (text, controller, done) => {
|
||||
this.currentRequest = controller;
|
||||
if (text) {
|
||||
this.chatHistory[this.chatHistory.length - 1].content += text;
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom();
|
||||
});
|
||||
}
|
||||
|
||||
if (done) {
|
||||
this.streamingResponse = false;
|
||||
if (this.autoUpdateCode) {
|
||||
const response =
|
||||
this.chatHistory[this.chatHistory.length - 1].content;
|
||||
const code = response.match(
|
||||
/```[a-z]*\n([\s\S]*?)\n```/
|
||||
)?.[1];
|
||||
|
||||
if (!code) return;
|
||||
|
||||
this.$emit("update-code", code);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.success && !response.cancelled) {
|
||||
window.quickcommand.showMessageBox(response.error, "error");
|
||||
}
|
||||
} catch (error) {
|
||||
window.quickcommand.showMessageBox(error.message, "error");
|
||||
this.streamingResponse = false;
|
||||
}
|
||||
},
|
||||
stopStreaming() {
|
||||
this.streamingResponse = false;
|
||||
if (this.currentRequest) {
|
||||
this.currentRequest.abort();
|
||||
this.currentRequest = null;
|
||||
}
|
||||
},
|
||||
clearHistory() {
|
||||
this.chatHistory = [];
|
||||
},
|
||||
getTrimContent(content) {
|
||||
const markedContent = marked(content.trim());
|
||||
// 解决think标签被错误地包裹在<p>标签中
|
||||
const processedContent = markedContent
|
||||
.replace("<p><think>", "<think><p>")
|
||||
.replace("</think></p>", "</p></think>")
|
||||
// 去除空的think标签
|
||||
.replace("<think>\n\n</think>", "");
|
||||
const purifiedContent = DOMPurify.sanitize(processedContent, {
|
||||
ADD_TAGS: ["think"],
|
||||
});
|
||||
return purifiedContent;
|
||||
},
|
||||
getRolePrompt(language) {
|
||||
const languageMap = {
|
||||
quickcommand: "NodeJS",
|
||||
javascript: "NodeJS",
|
||||
};
|
||||
const commonInstructions = `请作为一名专业的开发专家,根据我的需求编写${languageMap[language]}代码,并请遵循以下原则:
|
||||
- 编写简洁、可读性强的代码
|
||||
- 遵循${language}最佳实践和设计模式
|
||||
- 使用恰当的命名规范和代码组织
|
||||
- 添加必要的错误处理和边界检查
|
||||
- 保持中文注释的准确性和专业性
|
||||
- 提供必要的使用说明
|
||||
`;
|
||||
|
||||
// 针对不同语言的特殊提示
|
||||
let languageSpecific = {
|
||||
javascript: `- 优先使用现代ES6+特性
|
||||
- 使用NodeJS原生API和模块`,
|
||||
python: `- 遵循PEP8规范`,
|
||||
};
|
||||
languageSpecific.quickcommand = `${languageSpecific.javascript}
|
||||
- 支持使用以下uTools接口: ${uToolsApi}
|
||||
- 支持使用以下quickcommand接口: ${quickcommandApi}`;
|
||||
|
||||
// 获取语言特定的提示,如果没有则使用空字符串
|
||||
const specificInstructions =
|
||||
languageSpecific[language.toLowerCase()] || "";
|
||||
|
||||
const lastInstructions =
|
||||
"\n请直接生成代码,任何情况下都不需要做解释和说明";
|
||||
|
||||
return commonInstructions + specificInstructions + lastInstructions;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ai-dialog {
|
||||
width: 800px;
|
||||
max-width: 90vw;
|
||||
height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.chat-message-wrapper {
|
||||
margin-bottom: 1rem;
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.chat-message {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
.chat-message.user {
|
||||
margin-left: auto;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.chat-message .avatar {
|
||||
background: var(--q-primary);
|
||||
border-radius: 50%;
|
||||
padding: 2px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chat-message.assistant .avatar {
|
||||
background: var(--transparent-bg-color);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.chat-message.user .avatar {
|
||||
background: var(--q-primary);
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.message-content :deep(think) {
|
||||
color: #8b8b8b;
|
||||
display: block;
|
||||
border-left: 4px solid #8b8b8b;
|
||||
padding-left: 10px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.chat-message.user .message-bubble {
|
||||
background-color: var(--q-primary);
|
||||
color: white;
|
||||
border-top-right-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.chat-message.assistant .message-bubble {
|
||||
background-color: var(--transparent-bg-color);
|
||||
border-top-left-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.message-content {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 暗色模式适配 */
|
||||
.body--dark .chat-message.assistant .message-bubble {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
</style>
|
@@ -6,6 +6,26 @@
|
||||
{{ placeholder }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- AI助手按钮 -->
|
||||
<div class="ai-button-wrapper">
|
||||
<q-btn
|
||||
round
|
||||
dense
|
||||
color="primary"
|
||||
icon="smart_toy"
|
||||
@click="showAIDialog = true"
|
||||
>
|
||||
<q-tooltip>AI 助手</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
<!-- AI对话框 -->
|
||||
<q-dialog v-model="showAIDialog" position="right" seamless>
|
||||
<AIAssistantDialog
|
||||
:code="modelValue"
|
||||
:language="language"
|
||||
@update-code="setEditorValue"
|
||||
/>
|
||||
</q-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -13,6 +33,7 @@
|
||||
import * as monaco from "monaco-editor";
|
||||
import importAll from "js/common/importAll.js";
|
||||
import { defineComponent } from "vue";
|
||||
import AIAssistantDialog from "./AIAssistantDialog.vue";
|
||||
|
||||
// 批量导入关键字补全
|
||||
let languageCompletions = importAll(
|
||||
@@ -39,6 +60,9 @@ const typeDefinitions = {
|
||||
|
||||
export default defineComponent({
|
||||
name: "CodeEditor",
|
||||
components: {
|
||||
AIAssistantDialog,
|
||||
},
|
||||
props: {
|
||||
// v-model 绑定值
|
||||
modelValue: {
|
||||
@@ -134,6 +158,7 @@ export default defineComponent({
|
||||
// 光标样式
|
||||
cursorStyle: "line",
|
||||
},
|
||||
showAIDialog: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
@@ -394,6 +419,9 @@ export default defineComponent({
|
||||
formatDocument() {
|
||||
editor.getAction("editor.action.formatDocument").run();
|
||||
},
|
||||
setEditorValue(value) {
|
||||
editor.setValue(value);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
showPlaceholder() {
|
||||
@@ -431,4 +459,11 @@ export default defineComponent({
|
||||
user-select: none;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.ai-button-wrapper {
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
bottom: 30px;
|
||||
z-index: 500;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<q-expansion-item
|
||||
v-model="isExpanded"
|
||||
@update:model-value="$emit('update:is-expanded', $event)"
|
||||
class="command-config"
|
||||
expand-icon-toggle
|
||||
>
|
||||
@@ -188,7 +187,7 @@ export default defineComponent({
|
||||
default: "quickcommand",
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue", "update:is-expanded"],
|
||||
emits: ["update:modelValue"],
|
||||
data() {
|
||||
return {
|
||||
commandManager: useCommandManager(),
|
||||
@@ -303,7 +302,6 @@ export default defineComponent({
|
||||
},
|
||||
updateExpanded(value) {
|
||||
this.isExpanded = value;
|
||||
this.$emit("update:is-expanded", value);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<div>
|
||||
<div class="flex q-mb-md q-px-sm" style="height: 26px">
|
||||
<ButtonGroup
|
||||
v-model="modelToAdd"
|
||||
v-model="apiToAdd"
|
||||
class="col"
|
||||
:options="[
|
||||
{ label: 'OPENAI', value: 'openai' },
|
||||
@@ -27,93 +27,122 @@
|
||||
width: '2px',
|
||||
}"
|
||||
>
|
||||
<div class="config-list">
|
||||
<div
|
||||
v-for="(aiConfig, index) in aiConfigs"
|
||||
:key="index"
|
||||
class="config-item"
|
||||
>
|
||||
<div class="row q-col-gutter-sm">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model="aiConfig.name"
|
||||
class="col"
|
||||
placeholder="请输入名称"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-badge
|
||||
color="primary"
|
||||
text-color="black"
|
||||
label="名称"
|
||||
class="q-pa-xs"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<q-icon
|
||||
color="grey"
|
||||
name="remove_circle"
|
||||
@click="deleteModel(index)"
|
||||
size="16px"
|
||||
class="cursor-pointer"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model="aiConfig.apiUrl"
|
||||
class="col-8"
|
||||
:placeholder="`${aiConfig.modelType} API地址`"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-badge
|
||||
color="primary"
|
||||
text-color="black"
|
||||
label="接口"
|
||||
class="q-pa-xs"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
<draggable
|
||||
v-model="aiConfigs"
|
||||
item-key="name"
|
||||
handle=".drag-handle"
|
||||
:animation="200"
|
||||
class="config-list"
|
||||
>
|
||||
<template #item="{ element: aiConfig, index }">
|
||||
<div class="config-item">
|
||||
<div class="config-item-side-bar">
|
||||
<q-icon
|
||||
name="drag_indicator"
|
||||
class="drag-handle cursor-move"
|
||||
size="20px"
|
||||
/>
|
||||
</div>
|
||||
<div class="config-item-content">
|
||||
<div class="row q-col-gutter-sm">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model="aiConfig.name"
|
||||
class="col"
|
||||
placeholder="请输入名称"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-badge
|
||||
color="primary"
|
||||
text-color="white"
|
||||
label="名称"
|
||||
class="q-pa-xs"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<q-icon
|
||||
color="grey"
|
||||
name="remove_circle"
|
||||
@click="deleteModel(index)"
|
||||
size="16px"
|
||||
class="cursor-pointer"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model="aiConfig.apiUrl"
|
||||
class="col-7"
|
||||
:placeholder="`${aiConfig.apiType} API地址`"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-badge
|
||||
color="primary"
|
||||
text-color="white"
|
||||
label="接口"
|
||||
class="q-pa-xs"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="row q-col-gutter-sm">
|
||||
<q-input filled dense v-model="aiConfig.model" class="col">
|
||||
<template v-slot:prepend>
|
||||
<q-badge
|
||||
color="primary"
|
||||
text-color="white"
|
||||
label="模型"
|
||||
class="q-pa-xs"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<q-btn-dropdown
|
||||
flat
|
||||
@click="getModels(aiConfig)"
|
||||
dense
|
||||
dropdown-icon="refresh"
|
||||
>
|
||||
<q-list>
|
||||
<q-item
|
||||
v-for="model in models"
|
||||
:key="model"
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="aiConfig.model = model"
|
||||
>
|
||||
<q-item-section>
|
||||
{{ model }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
<q-tooltip>获取模型</q-tooltip>
|
||||
</template>
|
||||
</q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model="aiConfig.apiToken"
|
||||
v-if="aiConfig.apiType === 'openai'"
|
||||
type="password"
|
||||
class="col-7"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-badge
|
||||
color="primary"
|
||||
text-color="white"
|
||||
label="令牌"
|
||||
class="q-pa-xs"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-col-gutter-sm">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
v-model="aiConfig.model"
|
||||
:options="models"
|
||||
@focus="getModels(aiConfig)"
|
||||
class="col"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-badge
|
||||
color="primary"
|
||||
text-color="black"
|
||||
label="模型"
|
||||
class="q-pa-xs"
|
||||
/>
|
||||
</template>
|
||||
</q-select>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model="aiConfig.apiToken"
|
||||
v-if="aiConfig.modelType === 'openai'"
|
||||
type="password"
|
||||
class="col-8"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-badge
|
||||
color="primary"
|
||||
text-color="black"
|
||||
label="令牌"
|
||||
class="q-pa-xs"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</q-scroll-area>
|
||||
</div>
|
||||
<div class="flex justify-end q-gutter-sm q-px-sm">
|
||||
@@ -133,19 +162,23 @@
|
||||
import { defineComponent } from "vue";
|
||||
import { dbManager } from "js/utools.js";
|
||||
import ButtonGroup from "components/composer/common/ButtonGroup.vue";
|
||||
import draggable from "vuedraggable";
|
||||
import { getUniqueId } from "js/common/uuid.js";
|
||||
|
||||
export default defineComponent({
|
||||
name: "AIConfig",
|
||||
components: {
|
||||
ButtonGroup,
|
||||
draggable,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modelToAdd: "openai",
|
||||
apiToAdd: "openai",
|
||||
aiConfigs: [],
|
||||
models: [],
|
||||
};
|
||||
},
|
||||
emits: ["save"],
|
||||
methods: {
|
||||
async getModels(aiConfig) {
|
||||
const { success, result, error } = await window.getModelsFromAiApi(
|
||||
@@ -153,6 +186,7 @@ export default defineComponent({
|
||||
);
|
||||
if (!success) {
|
||||
quickcommand.showMessageBox(error, "error");
|
||||
this.models = [];
|
||||
return;
|
||||
}
|
||||
this.models = result;
|
||||
@@ -162,17 +196,19 @@ export default defineComponent({
|
||||
"cfg_aiConfigs",
|
||||
window.lodashM.cloneDeep(this.aiConfigs)
|
||||
);
|
||||
this.$emit("save");
|
||||
},
|
||||
deleteModel(index) {
|
||||
this.aiConfigs.splice(index, 1);
|
||||
},
|
||||
addModel() {
|
||||
this.aiConfigs.push({
|
||||
modelType: this.modelToAdd,
|
||||
apiType: this.apiToAdd,
|
||||
apiUrl: "",
|
||||
apiToken: "",
|
||||
model: "",
|
||||
name: "",
|
||||
id: getUniqueId(),
|
||||
});
|
||||
},
|
||||
getConfigListHeight() {
|
||||
@@ -188,7 +224,7 @@ export default defineComponent({
|
||||
|
||||
<style scoped>
|
||||
.config-list,
|
||||
.config-item {
|
||||
.config-item-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
@@ -198,5 +234,21 @@ export default defineComponent({
|
||||
border: 1px solid var(--q-primary);
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.config-item-side-bar {
|
||||
width: 20px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.config-item-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
cursor: move;
|
||||
color: var(--q-primary);
|
||||
margin-right: 4px;
|
||||
}
|
||||
</style>
|
||||
|
@@ -9,6 +9,8 @@
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
margin: 2px 0;
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.markdown code {
|
||||
@@ -74,3 +76,12 @@
|
||||
border-left-color: #444;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.markdown code ::-webkit-scrollbar {
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
.markdown a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
@@ -1,5 +1,3 @@
|
||||
import { newVarInputVal } from "js/composer/varInputValManager";
|
||||
|
||||
export const aiCommands = {
|
||||
label: "AI操作",
|
||||
icon: "smart_toy",
|
||||
|
25
src/plugins/monaco/types/quickcommand.api.d.ts
vendored
25
src/plugins/monaco/types/quickcommand.api.d.ts
vendored
@@ -892,16 +892,23 @@ interface quickcommandApi {
|
||||
/**
|
||||
* 与 AI 进行问答
|
||||
* @param content 对话内容
|
||||
* @param content.prompt 提示词
|
||||
* @param content.role 预设角色
|
||||
* @param apiConfig API配置
|
||||
* @param apiConfig.apiType 模型类型:openai/ollama
|
||||
* @param apiConfig.apiUrl API地址
|
||||
* @param apiConfig.apiToken API令牌(仅 OpenAI 需要)
|
||||
* @param apiConfig.model 模型名称
|
||||
* @param options 其他选项
|
||||
* @param options.showLoadingBar 是否显示加载条
|
||||
* @example
|
||||
* // OpenAI 示例
|
||||
* const response = await quickcommand.askAI(
|
||||
* {
|
||||
* prompt: "你好",
|
||||
* presetPrompt: "" // 使用预设提示词:translate/shell/summarize
|
||||
* },
|
||||
* {
|
||||
* modelType: "openai",
|
||||
* apiType: "openai",
|
||||
* apiUrl: "https://api.openai.com/v1/chat/completions",
|
||||
* apiToken: "your-api-token",
|
||||
* model: "gpt-3.5-turbo"
|
||||
@@ -912,10 +919,10 @@ interface quickcommandApi {
|
||||
* const response = await quickcommand.askAI(
|
||||
* {
|
||||
* prompt: "查找进程名为chrome的进程并关闭",
|
||||
* presetPrompt: "shell"
|
||||
* role: "shell"
|
||||
* },
|
||||
* {
|
||||
* modelType: "ollama",
|
||||
* apiType: "ollama",
|
||||
* apiUrl: "http://localhost:11434/api/generate",
|
||||
* model: "qwen2.5:32b"
|
||||
* }
|
||||
@@ -925,18 +932,22 @@ interface quickcommandApi {
|
||||
content: {
|
||||
/** 提示词 */
|
||||
prompt: string;
|
||||
/** 预设提示词类型 */
|
||||
presetPrompt?: "" | "translate" | "shell" | "summarize";
|
||||
/** 预设角色 */
|
||||
role?: "translate" | "shell" | "summarize";
|
||||
},
|
||||
apiConfig: {
|
||||
/** 模型类型:openai/ollama */
|
||||
modelType: "openai" | "ollama";
|
||||
apiType: "openai" | "ollama";
|
||||
/** API地址 */
|
||||
apiUrl: string;
|
||||
/** API令牌(仅 OpenAI 需要) */
|
||||
apiToken?: string;
|
||||
/** 模型名称 */
|
||||
model: string;
|
||||
},
|
||||
options?: {
|
||||
/** 是否显示加载条, 默认 true */
|
||||
showLoadingBar?: boolean;
|
||||
}
|
||||
): Promise<{
|
||||
/** 是否成功 */
|
||||
|
Reference in New Issue
Block a user