实现中间对话的修改
This commit is contained in:
parent
cf3a3a57ce
commit
beaf4f5ba1
@ -1,8 +1,8 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: "iconfont"; /* Project id 4870215 */
|
font-family: "iconfont"; /* Project id 4870215 */
|
||||||
src: url('iconfont.woff2?t=1745313248329') format('woff2'),
|
src: url('iconfont.woff2?t=1745654620708') format('woff2'),
|
||||||
url('iconfont.woff?t=1745313248329') format('woff'),
|
url('iconfont.woff?t=1745654620708') format('woff'),
|
||||||
url('iconfont.ttf?t=1745313248329') format('truetype');
|
url('iconfont.ttf?t=1745654620708') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconfont {
|
.iconfont {
|
||||||
@ -13,6 +13,18 @@
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-copy:before {
|
||||||
|
content: "\e77c";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-restart:before {
|
||||||
|
content: "\e86b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-edit2:before {
|
||||||
|
content: "\e848";
|
||||||
|
}
|
||||||
|
|
||||||
.icon-star:before {
|
.icon-star:before {
|
||||||
content: "\e80f";
|
content: "\e80f";
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
70
renderer/src/components/k-cute-textarea/index.vue
Normal file
70
renderer/src/components/k-cute-textarea/index.vue
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<el-input
|
||||||
|
type="textarea"
|
||||||
|
v-model="model"
|
||||||
|
:rows="inputHeightLines"
|
||||||
|
:maxlength="2000"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:resize="resize"
|
||||||
|
:class="customClass"
|
||||||
|
@keydown.enter="handleKeydown"
|
||||||
|
@compositionstart="handleCompositionStart"
|
||||||
|
@compositionend="handleCompositionEnd"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, defineProps, defineEmits } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: '输入消息...'
|
||||||
|
},
|
||||||
|
resize: {
|
||||||
|
type: String,
|
||||||
|
default: 'none'
|
||||||
|
},
|
||||||
|
customClass: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'pressEnter']);
|
||||||
|
|
||||||
|
const model = computed({
|
||||||
|
get() {
|
||||||
|
return props.modelValue;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
emit('update:modelValue', value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputHeightLines = computed(() => {
|
||||||
|
const currentLines = props.modelValue.split('\n').length;
|
||||||
|
return Math.min(12, Math.max(5, currentLines));
|
||||||
|
});
|
||||||
|
|
||||||
|
const isComposing = ref(false);
|
||||||
|
|
||||||
|
const handleKeydown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Enter' && !event.shiftKey && !isComposing.value) {
|
||||||
|
event.preventDefault();
|
||||||
|
emit('pressEnter', event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCompositionStart = () => {
|
||||||
|
isComposing.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCompositionEnd = () => {
|
||||||
|
isComposing.value = false;
|
||||||
|
};
|
||||||
|
</script>
|
@ -104,3 +104,4 @@ export function getToolSchema(enableTools: EnableToolItem[]) {
|
|||||||
}
|
}
|
||||||
return toolsSchema;
|
return toolsSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,23 +14,23 @@
|
|||||||
|
|
||||||
<!-- 用户输入的部分 -->
|
<!-- 用户输入的部分 -->
|
||||||
<div class="message-content" v-if="message.role === 'user'">
|
<div class="message-content" v-if="message.role === 'user'">
|
||||||
<Message.User :message="message" />
|
<Message.User :message="message" :tab-id="props.tabId" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 助手返回的内容部分 -->
|
<!-- 助手返回的内容部分 -->
|
||||||
<div class="message-content" v-else-if="message.role === 'assistant/content'">
|
<div class="message-content" v-else-if="message.role === 'assistant/content'">
|
||||||
<Message.Assistant :message="message" />
|
<Message.Assistant :message="message" :tab-id="props.tabId" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 助手调用的工具部分 -->
|
<!-- 助手调用的工具部分 -->
|
||||||
<div class="message-content" v-else-if="message.role === 'assistant/tool_calls'">
|
<div class="message-content" v-else-if="message.role === 'assistant/tool_calls'">
|
||||||
<Message.Toolcall :message="message" />
|
<Message.Toolcall :message="message" :tab-id="props.tabId" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 正在加载的部分实时解析 markdown -->
|
<!-- 正在加载的部分实时解析 markdown -->
|
||||||
<div v-if="isLoading" class="message-item assistant">
|
<div v-if="isLoading" class="message-item assistant">
|
||||||
<Message.StreamingBox :streaming-content="streamingContent" />
|
<Message.StreamingBox :streaming-content="streamingContent" :tab-id="props.tabId" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
@ -52,8 +52,12 @@
|
|||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
<Setting :tabId="tabId" />
|
<Setting :tabId="tabId" />
|
||||||
|
|
||||||
<el-input v-model="userInput" type="textarea" :rows="inputHeightLines" :maxlength="2000"
|
<KCuteTextarea
|
||||||
placeholder="输入消息..." @keydown.enter="handleKeydown" resize="none" class="chat-input" />
|
v-model="userInput"
|
||||||
|
placeholder="输入消息..."
|
||||||
|
:customClass="'chat-input'"
|
||||||
|
@press-enter="handleSend()"
|
||||||
|
/>
|
||||||
|
|
||||||
<el-button type="primary" @click="isLoading ? handleAbort() : handleSend()" class="send-button">
|
<el-button type="primary" @click="isLoading ? handleAbort() : handleSend()" class="send-button">
|
||||||
<span v-if="!isLoading" class="iconfont icon-send"></span>
|
<span v-if="!isLoading" class="iconfont icon-send"></span>
|
||||||
@ -77,6 +81,9 @@ import { llmManager, llms } from '@/views/setting/llm';
|
|||||||
|
|
||||||
import * as Message from './message';
|
import * as Message from './message';
|
||||||
import Setting from './setting.vue';
|
import Setting from './setting.vue';
|
||||||
|
import KCuteTextarea from '@/components/k-cute-textarea/index.vue';
|
||||||
|
|
||||||
|
import { provide } from 'vue';
|
||||||
|
|
||||||
defineComponent({ name: 'chat' });
|
defineComponent({ name: 'chat' });
|
||||||
|
|
||||||
@ -93,10 +100,6 @@ const tab = tabs.content[props.tabId];
|
|||||||
const tabStorage = tab.storage as ChatStorage;
|
const tabStorage = tab.storage as ChatStorage;
|
||||||
|
|
||||||
const userInput = ref('');
|
const userInput = ref('');
|
||||||
const inputHeightLines = computed(() => {
|
|
||||||
const currentLines = userInput.value.split('\n').length;
|
|
||||||
return Math.min(12, Math.max(5, currentLines));
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建 messages
|
// 创建 messages
|
||||||
if (!tabStorage.messages) {
|
if (!tabStorage.messages) {
|
||||||
@ -165,14 +168,6 @@ const updateScrollHeight = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeydown = (event: KeyboardEvent) => {
|
|
||||||
if (event.key === 'Enter' && !event.shiftKey) {
|
|
||||||
event.preventDefault();
|
|
||||||
handleSend();
|
|
||||||
}
|
|
||||||
// Shift+Enter 允许自然换行
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const autoScroll = ref(true);
|
const autoScroll = ref(true);
|
||||||
const scrollbarRef = ref<ScrollbarInstance>();
|
const scrollbarRef = ref<ScrollbarInstance>();
|
||||||
@ -218,14 +213,13 @@ watch(streamingToolCalls, () => {
|
|||||||
|
|
||||||
let loop: TaskLoop | undefined = undefined;
|
let loop: TaskLoop | undefined = undefined;
|
||||||
|
|
||||||
const handleSend = () => {
|
const handleSend = (newMessage?: string) => {
|
||||||
if (!userInput.value.trim() || isLoading.value) return;
|
const userMessage = newMessage || userInput.value.trim();
|
||||||
|
if (!userMessage || isLoading.value) return;
|
||||||
|
|
||||||
autoScroll.value = true;
|
autoScroll.value = true;
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
const userMessage = userInput.value.trim();
|
|
||||||
|
|
||||||
loop = new TaskLoop(streamingContent, streamingToolCalls);
|
loop = new TaskLoop(streamingContent, streamingToolCalls);
|
||||||
|
|
||||||
loop.registerOnError((error) => {
|
loop.registerOnError((error) => {
|
||||||
@ -278,6 +272,8 @@ const handleAbort = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
provide('handleSend', handleSend);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
updateScrollHeight();
|
updateScrollHeight();
|
||||||
window.addEventListener('resize', updateScrollHeight);
|
window.addEventListener('resize', updateScrollHeight);
|
||||||
@ -357,9 +353,10 @@ onUnmounted(() => {
|
|||||||
.user .message-text {
|
.user .message-text {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user .message-text>span {
|
.user .message-text > span {
|
||||||
border-radius: .9em;
|
border-radius: .9em;
|
||||||
background-color: var(--main-light-color);
|
background-color: var(--main-light-color);
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
@ -419,7 +416,7 @@ onUnmounted(() => {
|
|||||||
height: auto;
|
height: auto;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
border-radius: .5em;
|
border-radius: 1.2em !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.chat-settings) {
|
:deep(.chat-settings) {
|
||||||
|
@ -16,6 +16,10 @@ const props = defineProps({
|
|||||||
message: {
|
message: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
tabId: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -25,6 +25,10 @@ const props = defineProps({
|
|||||||
streamingContent: {
|
streamingContent: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
tabId: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -104,6 +104,10 @@ const props = defineProps({
|
|||||||
message: {
|
message: {
|
||||||
type: Object as PropType<IRenderMessage>,
|
type: Object as PropType<IRenderMessage>,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
tabId: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,22 +1,140 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="message-role"></div>
|
<div class="message-role"></div>
|
||||||
<div class="message-text">
|
<div class="message-text">
|
||||||
<span>{{ props.message.content }}</span>
|
<div v-if="!isEditing" class="message-content">
|
||||||
|
<span>
|
||||||
|
{{ props.message.content.trim() }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<KCuteTextarea v-else
|
||||||
|
v-model="userInput"
|
||||||
|
placeholder="输入消息..."
|
||||||
|
@press-enter="handleKeydown"
|
||||||
|
/>
|
||||||
|
<div class="message-actions" v-if="!isEditing">
|
||||||
|
<el-button @click="copy">
|
||||||
|
<span class="iconfont icon-copy"></span>
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="toggleEdit">
|
||||||
|
<span class="iconfont icon-edit2"></span>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="message-actions" v-else>
|
||||||
|
<el-button @click="toggleEdit">
|
||||||
|
{{ '取消' }}
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleKeydown" type="primary">
|
||||||
|
{{ '发送' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { defineProps, ref, PropType, inject } from 'vue';
|
||||||
|
import { tabs } from '../../panel';
|
||||||
|
import { ChatStorage, IRenderMessage } from '../chat';
|
||||||
|
|
||||||
import { defineProps } from 'vue';
|
import KCuteTextarea from '@/components/k-cute-textarea/index.vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { markdownToHtml } from '../markdown';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
message: {
|
message: {
|
||||||
type: Object,
|
type: Object as PropType<IRenderMessage>,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
tabId: {
|
||||||
|
type: Number,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const tab = tabs.content[props.tabId];
|
||||||
|
const tabStorage = tab.storage as ChatStorage;
|
||||||
|
const isEditing = ref(false);
|
||||||
|
const userInput = ref('');
|
||||||
|
|
||||||
|
const handleSend = inject<(newMessage: string | undefined) => void>('handleSend');
|
||||||
|
|
||||||
|
const toggleEdit = () => {
|
||||||
|
isEditing.value = !isEditing.value;
|
||||||
|
if (isEditing.value) {
|
||||||
|
userInput.value = props.message.content;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeydown = (event: KeyboardEvent) => {
|
||||||
|
const index = tabStorage.messages.findIndex(msg => msg.extraInfo === props.message.extraInfo);
|
||||||
|
|
||||||
|
if (index !== -1 && handleSend) {
|
||||||
|
// 把 index 之后的全部删除(包括 index)
|
||||||
|
tabStorage.messages.splice(index);
|
||||||
|
handleSend(userInput.value);
|
||||||
|
|
||||||
|
isEditing.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copy = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(userInput.value);
|
||||||
|
ElMessage.success('内容已复制到剪贴板');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('无法复制内容: ', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
|
.message-text {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-text:hover .message-actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-actions {
|
||||||
|
opacity: 0;
|
||||||
|
transition: var(--animation-3s);
|
||||||
|
position: absolute;
|
||||||
|
bottom: -34px;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-actions .el-button {
|
||||||
|
border-radius: .5em;
|
||||||
|
padding: 5px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-actions .el-button:hover {
|
||||||
|
background-color: var(--main-light-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-actions .el-button+.el-button {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user .message-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user .message-content > span {
|
||||||
|
max-width: calc(100% - 48px);
|
||||||
|
border-radius: .9em;
|
||||||
|
background-color: var(--main-light-color);
|
||||||
|
padding: 10px 15px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
@ -83,7 +83,7 @@ export function messageController(command: string, data: any, webview: PostMessa
|
|||||||
chatCompletionService(client, data, webview);
|
chatCompletionService(client, data, webview);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'llm/chat/completions/cancel':
|
case 'llm/chat/completions/abort':
|
||||||
abortMessageService(client, data, webview);
|
abortMessageService(client, data, webview);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -104,15 +104,15 @@ export function abortMessageService(client: MCPClient | undefined, data: any, we
|
|||||||
if (currentStream) {
|
if (currentStream) {
|
||||||
// 标记流已中止
|
// 标记流已中止
|
||||||
currentStream = null;
|
currentStream = null;
|
||||||
// 发送中止消息给前端
|
|
||||||
webview.postMessage({
|
|
||||||
command: 'llm/chat/completions/abort',
|
|
||||||
data: {
|
|
||||||
code: 200,
|
|
||||||
msg: {
|
|
||||||
success: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
webview.postMessage({
|
||||||
|
command: 'llm/chat/completions/abort',
|
||||||
|
data: {
|
||||||
|
code: 200,
|
||||||
|
msg: {
|
||||||
|
success: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user