支持富文本编辑器,支持在客户端对提词和资源进行选择
This commit is contained in:
parent
cefa6b2af5
commit
96d36c0706
@ -1,9 +1,10 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
## [main] 0.0.7
|
## [main] 0.0.7
|
||||||
- 优化页面布局,使得调试内容更加紧凑
|
- 优化页面布局,使得调试窗口可以显示更多内容
|
||||||
- 扩大默认的上下文长度
|
- 扩大默认的上下文长度 10 -> 20
|
||||||
- 增加「通用选项」,用于设置mcp服务器的最大的等待时间
|
- 增加「通用选项」 -> 「MCP工具最长调用时间 (sec)」
|
||||||
|
- 支持富文本输入框,现在可以将 prompt 和 resource 嵌入到输入框中 进行 大规模 prompt engineering 调试工作了
|
||||||
|
|
||||||
## [main] 0.0.6
|
## [main] 0.0.6
|
||||||
- 修复部分因为服务器名称特殊字符而导致的保存实效的错误
|
- 修复部分因为服务器名称特殊字符而导致的保存实效的错误
|
||||||
|
@ -1,126 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="k-rich-textarea">
|
|
||||||
<div
|
|
||||||
ref="editor"
|
|
||||||
contenteditable="true"
|
|
||||||
class="rich-editor"
|
|
||||||
:placeholder="placeholder"
|
|
||||||
@input="handleInput"
|
|
||||||
@keydown.enter="handleKeydown"
|
|
||||||
@compositionstart="handleCompositionStart"
|
|
||||||
@compositionend="handleCompositionEnd"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, computed, defineProps, defineEmits, watch } from 'vue';
|
|
||||||
import type { RichTextItem } from './textarea.dto';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
modelValue: {
|
|
||||||
type: Array as () => RichTextItem[],
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
placeholder: {
|
|
||||||
type: String,
|
|
||||||
default: '输入消息...'
|
|
||||||
},
|
|
||||||
customClass: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'pressEnter']);
|
|
||||||
|
|
||||||
const editor = ref<HTMLElement | null>(null);
|
|
||||||
|
|
||||||
const renderRichText = (items: RichTextItem[]) => {
|
|
||||||
return items.map(item => {
|
|
||||||
if (item.type === 'prompt' || item.type === 'resource') {
|
|
||||||
return `<span class="rich-item rich-item-${item.type}">${item.text}</span>`;
|
|
||||||
}
|
|
||||||
return item.text;
|
|
||||||
}).join('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInput = (event: Event) => {
|
|
||||||
if (editor.value) {
|
|
||||||
const items: RichTextItem[] = [];
|
|
||||||
const nodes = editor.value.childNodes;
|
|
||||||
nodes.forEach(node => {
|
|
||||||
if (node.nodeType === Node.TEXT_NODE) {
|
|
||||||
items.push({ type: 'text', text: node.textContent || '' });
|
|
||||||
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
const element = node as HTMLElement;
|
|
||||||
if (element.classList.contains('rich-item-prompt')) {
|
|
||||||
items.push({ type: 'prompt', text: element.textContent || '' });
|
|
||||||
} else if (element.classList.contains('rich-item-resource')) {
|
|
||||||
items.push({ type: 'resource', text: element.textContent || '' });
|
|
||||||
} else {
|
|
||||||
items.push({ type: 'text', text: element.textContent || '' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
emit('update:modelValue', items);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(() => props.modelValue, (newValue) => {
|
|
||||||
if (editor.value) {
|
|
||||||
editor.value.innerHTML = renderRichText(newValue);
|
|
||||||
}
|
|
||||||
}, { immediate: true });
|
|
||||||
|
|
||||||
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>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.k-rich-textarea {
|
|
||||||
border-radius: .9em;
|
|
||||||
border: 1px solid #DCDFE6;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rich-editor {
|
|
||||||
min-height: 100px;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rich-editor:empty::before {
|
|
||||||
content: attr(placeholder);
|
|
||||||
color: #C0C4CC;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rich-item {
|
|
||||||
padding: 2px 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin: 0 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rich-item-prompt {
|
|
||||||
background-color: #e8f0fe;
|
|
||||||
color: #1a73e8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rich-item-resource {
|
|
||||||
background-color: #f1f3f4;
|
|
||||||
color: #202124;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,17 +0,0 @@
|
|||||||
|
|
||||||
interface PromptTextItem {
|
|
||||||
type: 'prompt'
|
|
||||||
text: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ResourceTextItem {
|
|
||||||
type: 'resource'
|
|
||||||
text: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TextItem {
|
|
||||||
type: 'text'
|
|
||||||
text: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RichTextItem = PromptTextItem | ResourceTextItem | TextItem;
|
|
@ -76,6 +76,23 @@ export interface ToolCall {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PromptTextItem {
|
||||||
|
type: 'prompt'
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResourceTextItem {
|
||||||
|
type: 'resource'
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TextItem {
|
||||||
|
type: 'text'
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RichTextItem = PromptTextItem | ResourceTextItem | TextItem;
|
||||||
|
|
||||||
export const allTools = ref<ToolItem[]>([]);
|
export const allTools = ref<ToolItem[]>([]);
|
||||||
|
|
||||||
export interface IRenderMessage {
|
export interface IRenderMessage {
|
||||||
@ -105,3 +122,8 @@ export function getToolSchema(enableTools: EnableToolItem[]) {
|
|||||||
}
|
}
|
||||||
return toolsSchema;
|
return toolsSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EditorContext {
|
||||||
|
editor: Ref<HTMLDivElement>;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
204
renderer/src/components/main-panel/chat/chat-box/index.vue
Normal file
204
renderer/src/components/main-panel/chat/chat-box/index.vue
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
<template>
|
||||||
|
<footer class="chat-footer">
|
||||||
|
<div class="input-area">
|
||||||
|
<div class="input-wrapper">
|
||||||
|
|
||||||
|
<KRichTextarea
|
||||||
|
:tabId="tabId"
|
||||||
|
v-model="userInput"
|
||||||
|
:placeholder="t('enter-message-dot')"
|
||||||
|
:customClass="'chat-input'"
|
||||||
|
@press-enter="handleSend()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-button type="primary" @click="isLoading ? handleAbort() : handleSend()" class="send-button">
|
||||||
|
<span v-if="!isLoading" class="iconfont icon-send"></span>
|
||||||
|
<span v-else class="iconfont icon-stop"></span>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { provide, onMounted, onUnmounted, ref, defineEmits, defineProps, PropType, inject, Ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import KRichTextarea from './rich-textarea.vue';
|
||||||
|
import { tabs } from '../../panel';
|
||||||
|
import { ChatMessage, ChatStorage, MessageState, ToolCall, RichTextItem } from './chat';
|
||||||
|
|
||||||
|
import { TaskLoop } from '../core/task-loop';
|
||||||
|
import { llmManager, llms } from '@/views/setting/llm';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
tabId: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits(['update:scrollToBottom']);
|
||||||
|
|
||||||
|
const tab = tabs.content[props.tabId];
|
||||||
|
const tabStorage = tab.storage as ChatStorage;
|
||||||
|
|
||||||
|
// 创建 messages
|
||||||
|
if (!tabStorage.messages) {
|
||||||
|
tabStorage.messages = [] as ChatMessage[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const userInput = ref<string>('');
|
||||||
|
|
||||||
|
let loop: TaskLoop | undefined = undefined;
|
||||||
|
|
||||||
|
const isLoading = inject('isLoading') as Ref<boolean>;
|
||||||
|
const autoScroll = inject('autoScroll') as Ref<boolean>;
|
||||||
|
const streamingContent = inject('streamingContent') as Ref<string>;
|
||||||
|
const streamingToolCalls = inject('streamingToolCalls') as Ref<ToolCall[]>;
|
||||||
|
const scrollToBottom = inject('scrollToBottom') as () => Promise<void>;
|
||||||
|
const updateScrollHeight = inject('updateScrollHeight') as () => void;
|
||||||
|
|
||||||
|
function handleSend(newMessage?: string) {
|
||||||
|
// 将富文本信息转换成纯文本信息
|
||||||
|
const userMessage = newMessage || userInput.value;
|
||||||
|
|
||||||
|
if (!userMessage || isLoading.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
autoScroll.value = true;
|
||||||
|
|
||||||
|
loop = new TaskLoop(streamingContent, streamingToolCalls);
|
||||||
|
|
||||||
|
loop.registerOnError((error) => {
|
||||||
|
|
||||||
|
ElMessage({
|
||||||
|
message: error.msg,
|
||||||
|
type: 'error',
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error.state === MessageState.ReceiveChunkError) {
|
||||||
|
tabStorage.messages.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content: error.msg,
|
||||||
|
extraInfo: {
|
||||||
|
created: Date.now(),
|
||||||
|
state: error.state,
|
||||||
|
serverName: llms[llmManager.currentModelIndex].id || 'unknown'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
loop.registerOnChunk(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
});
|
||||||
|
|
||||||
|
loop.registerOnDone(() => {
|
||||||
|
isLoading.value = false;
|
||||||
|
scrollToBottom();
|
||||||
|
});
|
||||||
|
|
||||||
|
loop.registerOnEpoch(() => {
|
||||||
|
isLoading.value = true;
|
||||||
|
scrollToBottom();
|
||||||
|
});
|
||||||
|
|
||||||
|
loop.start(tabStorage, userMessage);
|
||||||
|
|
||||||
|
userInput.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAbort() {
|
||||||
|
if (loop) {
|
||||||
|
loop.abort();
|
||||||
|
isLoading.value = false;
|
||||||
|
ElMessage.info('请求已中止');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provide('handleSend', handleSend);
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateScrollHeight();
|
||||||
|
window.addEventListener('resize', updateScrollHeight);
|
||||||
|
scrollToBottom();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', updateScrollHeight);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.chat-footer {
|
||||||
|
padding: 16px;
|
||||||
|
border-top: 1px solid var(--el-border-color);
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: absolute;
|
||||||
|
height: fit-content !important;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-area {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input {
|
||||||
|
padding-right: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input textarea {
|
||||||
|
border-radius: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-button {
|
||||||
|
position: absolute !important;
|
||||||
|
right: 8px !important;
|
||||||
|
bottom: 8px !important;
|
||||||
|
height: auto;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 20px;
|
||||||
|
border-radius: 1.2em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.chat-settings) {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-cursor {
|
||||||
|
animation: blink 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,114 @@
|
|||||||
|
<template>
|
||||||
|
<el-tooltip :content="t('prompts')" placement="top">
|
||||||
|
<div class="setting-button" @click="showChoosePrompt = true; saveCursorPosition();">
|
||||||
|
<span class="iconfont icon-chat"></span>
|
||||||
|
</div>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
<!-- 上下文长度设置 - 改为滑块形式 -->
|
||||||
|
<el-dialog v-model="showChoosePrompt" :title="t('prompts')" width="400px">
|
||||||
|
|
||||||
|
<div class="prompt-template-container-scrollbar" v-if="!selectPrompt">
|
||||||
|
<PromptTemplates :tab-id="-1" @prompt-selected="prompt => selectPrompt = prompt" />
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<PromptReader :tab-id="-1" :current-prompt-name="selectPrompt!.name"
|
||||||
|
@prompt-get-response="msg => whenGetPromptResponse(msg)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button v-if="selectPrompt" @click="selectPrompt = undefined;">{{ t('return') }}</el-button>
|
||||||
|
<el-button @click="showChoosePrompt = false; selectPrompt = undefined;">{{ t("cancel") }}</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { createApp, inject, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { ChatStorage, EditorContext } from '../chat';
|
||||||
|
import { PromptsGetResponse, PromptTemplate } from '@/hook/type';
|
||||||
|
|
||||||
|
import PromptTemplates from '@/components/main-panel/prompt/prompt-templates.vue';
|
||||||
|
import PromptReader from '@/components/main-panel/prompt/prompt-reader.vue';
|
||||||
|
import { ElMessage, ElTooltip } from 'element-plus';
|
||||||
|
|
||||||
|
import PromptChatItem from '../prompt-chat-item.vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const tabStorage = inject('tabStorage') as ChatStorage;
|
||||||
|
let selectPrompt = ref<PromptTemplate | undefined>(undefined);
|
||||||
|
const showChoosePrompt = ref(false);
|
||||||
|
|
||||||
|
const editorContext = inject('editorContext') as EditorContext;
|
||||||
|
let savedSelection: Range | null = null;
|
||||||
|
|
||||||
|
function saveCursorPosition() {
|
||||||
|
const editor = editorContext.editor.value;
|
||||||
|
if (editor) {
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (selection && selection.rangeCount > 0) {
|
||||||
|
const range = selection.getRangeAt(0);
|
||||||
|
// 检查 selection 是否在 editor 内部
|
||||||
|
if (editor.contains(range.startContainer) && editor.contains(range.endContainer)) {
|
||||||
|
savedSelection = range;
|
||||||
|
} else {
|
||||||
|
savedSelection = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function whenGetPromptResponse(msg: PromptsGetResponse) {
|
||||||
|
try {
|
||||||
|
const content = msg.messages[0].content;
|
||||||
|
selectPrompt.value = undefined;
|
||||||
|
const editor = editorContext.editor.value;
|
||||||
|
|
||||||
|
if (!content || !editor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const promptChatItem = createApp(PromptChatItem, {
|
||||||
|
messages: msg.messages
|
||||||
|
});
|
||||||
|
promptChatItem.use(ElTooltip);
|
||||||
|
promptChatItem.mount(container);
|
||||||
|
|
||||||
|
const firstElement = container.firstElementChild!;
|
||||||
|
|
||||||
|
if (savedSelection) {
|
||||||
|
|
||||||
|
savedSelection.deleteContents();
|
||||||
|
savedSelection.insertNode(firstElement);
|
||||||
|
} else {
|
||||||
|
editor.appendChild(firstElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置光标到插入元素的后方
|
||||||
|
const newRange = document.createRange();
|
||||||
|
newRange.setStartAfter(firstElement);
|
||||||
|
newRange.collapse(true);
|
||||||
|
const selection = window.getSelection();
|
||||||
|
|
||||||
|
selection?.removeAllRanges();
|
||||||
|
selection?.addRange(newRange);
|
||||||
|
|
||||||
|
editor.dispatchEvent(new Event('input'));
|
||||||
|
editor.focus();
|
||||||
|
|
||||||
|
showChoosePrompt.value = false;
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error((error as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.icon-length {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,116 @@
|
|||||||
|
<template>
|
||||||
|
<el-tooltip :content="t('resources')" placement="top">
|
||||||
|
<div class="setting-button" @click="showChooseResource = true; saveCursorPosition();">
|
||||||
|
<span class="iconfont icon-file"></span>
|
||||||
|
</div>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
<el-dialog v-model="showChooseResource" :title="t('resources')" width="400px">
|
||||||
|
<div class="resource-template-container-scrollbar" v-if="!selectResource">
|
||||||
|
<ResourceList :tab-id="-1" @resource-selected="resource => selectResource = resource" />
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<ResourceReader :tab-id="-1" :current-resource-name="selectResource!.name"
|
||||||
|
@resource-get-response="msg => whenGetResourceResponse(msg)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button v-if="selectResource" @click="selectResource = undefined;">{{ t('return') }}</el-button>
|
||||||
|
<el-button @click="showChooseResource = false; selectResource = undefined;">{{ t("cancel") }}</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { createApp, inject, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { ChatStorage, EditorContext } from '../chat';
|
||||||
|
import { ResourcesReadResponse, ResourceTemplate } from '@/hook/type';
|
||||||
|
|
||||||
|
import ResourceList from '@/components/main-panel/resource/resource-list.vue';
|
||||||
|
import ResourceReader from '@/components/main-panel/resource/resouce-reader.vue';
|
||||||
|
import { ElMessage, ElTooltip, ElProgress, ElPopover } from 'element-plus';
|
||||||
|
|
||||||
|
import ResourceChatItem from '../resource-chat-item.vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const tabStorage = inject('tabStorage') as ChatStorage;
|
||||||
|
let selectResource = ref<ResourceTemplate | undefined>(undefined);
|
||||||
|
const showChooseResource = ref(false);
|
||||||
|
|
||||||
|
const editorContext = inject('editorContext') as EditorContext;
|
||||||
|
let savedSelection: Range | null = null;
|
||||||
|
|
||||||
|
function saveCursorPosition() {
|
||||||
|
const editor = editorContext.editor.value;
|
||||||
|
if (editor) {
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (selection && selection.rangeCount > 0) {
|
||||||
|
const range = selection.getRangeAt(0);
|
||||||
|
if (editor.contains(range.startContainer) && editor.contains(range.endContainer)) {
|
||||||
|
savedSelection = range;
|
||||||
|
} else {
|
||||||
|
savedSelection = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function whenGetResourceResponse(msg: ResourcesReadResponse) {
|
||||||
|
if (!msg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(msg);
|
||||||
|
|
||||||
|
selectResource.value = undefined;
|
||||||
|
const editor = editorContext.editor.value;
|
||||||
|
|
||||||
|
if (msg.contents.length === 0 || !editor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const resourceChatItem = createApp(ResourceChatItem, {
|
||||||
|
contents: msg.contents
|
||||||
|
});
|
||||||
|
resourceChatItem
|
||||||
|
.use(ElTooltip)
|
||||||
|
.use(ElProgress)
|
||||||
|
.use(ElPopover)
|
||||||
|
resourceChatItem.mount(container);
|
||||||
|
|
||||||
|
const firstElement = container.firstElementChild!;
|
||||||
|
|
||||||
|
if (savedSelection) {
|
||||||
|
savedSelection.deleteContents();
|
||||||
|
savedSelection.insertNode(firstElement);
|
||||||
|
} else {
|
||||||
|
editor.appendChild(firstElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRange = document.createRange();
|
||||||
|
newRange.setStartAfter(firstElement);
|
||||||
|
newRange.collapse(true);
|
||||||
|
const selection = window.getSelection();
|
||||||
|
|
||||||
|
selection?.removeAllRanges();
|
||||||
|
selection?.addRange(newRange);
|
||||||
|
|
||||||
|
editor.dispatchEvent(new Event('input'));
|
||||||
|
editor.focus();
|
||||||
|
|
||||||
|
showChooseResource.value = false;
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error((error as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.icon-length {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
@ -3,7 +3,8 @@
|
|||||||
<Model />
|
<Model />
|
||||||
<SystemPrompt />
|
<SystemPrompt />
|
||||||
<ToolUse />
|
<ToolUse />
|
||||||
<Prompt v-model="modelValue" />
|
<Prompt />
|
||||||
|
<Resource />
|
||||||
<Websearch />
|
<Websearch />
|
||||||
<Temperature />
|
<Temperature />
|
||||||
<ContextLength />
|
<ContextLength />
|
||||||
@ -11,15 +12,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineProps, defineEmits, provide, ref, computed } from 'vue';
|
import { defineProps, defineEmits, provide, PropType, computed } from 'vue';
|
||||||
import { llmManager } from '@/views/setting/llm';
|
import { llmManager } from '@/views/setting/llm';
|
||||||
import { tabs } from '../../panel';
|
import { tabs } from '@/components/main-panel/panel';
|
||||||
import type { ChatSetting, ChatStorage } from '../chat';
|
import type { ChatSetting, ChatStorage } from '../chat';
|
||||||
|
|
||||||
import Model from './model.vue';
|
import Model from './model.vue';
|
||||||
import SystemPrompt from './system-prompt.vue';
|
import SystemPrompt from './system-prompt.vue';
|
||||||
import ToolUse from './tool-use.vue';
|
import ToolUse from './tool-use.vue';
|
||||||
import Prompt from './prompt.vue';
|
import Prompt from './prompt.vue';
|
||||||
|
import Resource from './resource.vue';
|
||||||
import Websearch from './websearch.vue';
|
import Websearch from './websearch.vue';
|
||||||
import Temperature from './temperature.vue';
|
import Temperature from './temperature.vue';
|
||||||
import ContextLength from './context-length.vue';
|
import ContextLength from './context-length.vue';
|
||||||
@ -70,9 +72,9 @@ provide('tabStorage', tabStorage);
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
background-color: var(--sidebar);
|
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
border-radius: 99%;
|
border-radius: 99%;
|
||||||
|
left: 5px;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -212,6 +214,7 @@ provide('tabStorage', tabStorage);
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
z-index: 10;
|
||||||
top: -16px;
|
top: -16px;
|
||||||
right: -18px;
|
right: -18px;
|
||||||
box-shadow: 0 0 6px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 0 6px rgba(0, 0, 0, 0.2);
|
@ -38,7 +38,7 @@
|
|||||||
import { ref, computed, inject, onMounted } from 'vue';
|
import { ref, computed, inject, onMounted } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { allTools, ChatStorage, getToolSchema } from '../chat';
|
import { allTools, ChatStorage, getToolSchema } from '../chat';
|
||||||
import { markdownToHtml } from '../markdown/markdown';
|
import { markdownToHtml } from '@/components/main-panel/chat/markdown/markdown';
|
||||||
import { useMessageBridge } from '@/api/message-bridge';
|
import { useMessageBridge } from '@/api/message-bridge';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<el-tooltip :content="props.messages[0].content.text" placement="top">
|
||||||
|
<span class="chat-prompt-item" contenteditable="false">
|
||||||
|
<span class="iconfont icon-chat"></span>
|
||||||
|
<span class="real-text">{{ props.messages[0].content.text }}</span>
|
||||||
|
</span>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { PromptsGetResponse } from '@/hook/type';
|
||||||
|
import { defineProps, PropType } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
messages: {
|
||||||
|
type: Array as PropType<PromptsGetResponse['messages']>,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.chat-prompt-item {
|
||||||
|
max-width: 80px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: inline-flex;
|
||||||
|
border-radius: .3em;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 4px;
|
||||||
|
background-color: #373839;
|
||||||
|
border: 1px solid var(--foreground);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 3px;
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-prompt-item .iconfont {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,168 @@
|
|||||||
|
<template>
|
||||||
|
<el-tooltip placement="top">
|
||||||
|
<template #content>
|
||||||
|
<div class="resource-chat-item-tooltip">
|
||||||
|
<div v-for="(item, index) of toolRenderItems" :key="index">
|
||||||
|
<div v-if="item.mimeType === 'text/plain'">
|
||||||
|
{{ item.text }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="item.mimeType === 'image/jpeg' || item.mimeType === 'image/png'">
|
||||||
|
<img :src="item.imageUrl" alt="screenshot" />
|
||||||
|
<span>{{ item.text }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<span class="chat-prompt-item" contenteditable="false">
|
||||||
|
<span class="iconfont icon-file"></span>
|
||||||
|
<span class="real-text">{{ resourceText }}</span>
|
||||||
|
<el-progress v-if="!finishProcess" class="progress" style="width: 100px;" :percentage="progress"
|
||||||
|
color="var(--main-color)"></el-progress>
|
||||||
|
</span>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useMessageBridge } from '@/api/message-bridge';
|
||||||
|
import { ResourcesReadResponse } from '@/hook/type';
|
||||||
|
import { getImageBlobUrlByBase64 } from '@/hook/util';
|
||||||
|
import { computed, defineProps, PropType, reactive, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
contents: {
|
||||||
|
type: Array as PropType<ResourcesReadResponse['contents']>,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// mcp 协议中,表示图像两种方法
|
||||||
|
// 1. 将图像实现处理成文本
|
||||||
|
// 2. 将图像做成 { image_url: "https://tos.com/xxx.jpeg" },然后上传这段表达的序列化文本
|
||||||
|
|
||||||
|
const toolRenderItems = ref<{
|
||||||
|
mimeType: string;
|
||||||
|
text: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}[]>([]);
|
||||||
|
|
||||||
|
const resourceText = computed(() => {
|
||||||
|
const texts = [];
|
||||||
|
for (const item of toolRenderItems.value) {
|
||||||
|
if (item.mimeType === 'text/plain') {
|
||||||
|
texts.push(item.text);
|
||||||
|
} else if (item.mimeType === 'image/jpeg' || item.mimeType === 'image/png') {
|
||||||
|
texts.push(item.text || '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return texts.join('');
|
||||||
|
});
|
||||||
|
|
||||||
|
const bridge = useMessageBridge();
|
||||||
|
const progress = ref(0);
|
||||||
|
const progressText = ref('OCR');
|
||||||
|
const finishProcess = ref(true);
|
||||||
|
|
||||||
|
props.contents.forEach((content) => {
|
||||||
|
console.log(content);
|
||||||
|
|
||||||
|
if (content.mimeType === 'text/plain') {
|
||||||
|
toolRenderItems.value.push({
|
||||||
|
mimeType: content.mimeType,
|
||||||
|
text: content.text
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.mimeType === 'image/jpeg' || content.mimeType === 'image/png') {
|
||||||
|
finishProcess.value = false;
|
||||||
|
|
||||||
|
const blobUrl = getImageBlobUrlByBase64(content.blob!, content.mimeType);
|
||||||
|
|
||||||
|
toolRenderItems.value.push({
|
||||||
|
mimeType: content.mimeType,
|
||||||
|
imageUrl: blobUrl,
|
||||||
|
text: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderItem = toolRenderItems.value.at(-1)!;
|
||||||
|
|
||||||
|
const makeRequest = async () => {
|
||||||
|
const res = await bridge.commandRequest('ocr/start-ocr', {
|
||||||
|
base64String: content.blob,
|
||||||
|
mimeType: content.mimeType
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.code === 200) {
|
||||||
|
const filename = res.msg.filename;
|
||||||
|
const workerId = res.msg.workerId;
|
||||||
|
|
||||||
|
const cancel = bridge.addCommandListener('ocr/worker/log', data => {
|
||||||
|
|
||||||
|
finishProcess.value = false;
|
||||||
|
const { id, progress: p = 1.0, status = 'finish' } = data;
|
||||||
|
if (id === workerId) {
|
||||||
|
progressText.value = status;
|
||||||
|
progress.value = Math.min(Math.ceil(Math.max(p * 100, 0)), 100);
|
||||||
|
}
|
||||||
|
}, { once: false });
|
||||||
|
|
||||||
|
bridge.addCommandListener('ocr/worker/done', data => {
|
||||||
|
if (data.id !== workerId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.value = 1;
|
||||||
|
finishProcess.value = true;
|
||||||
|
toolRenderItems.value[0].text = data.text;
|
||||||
|
|
||||||
|
cancel();
|
||||||
|
}, { once: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
makeRequest();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.resource-chat-item-tooltip {
|
||||||
|
min-height: 100px;
|
||||||
|
max-width: 420px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-chat-item-tooltip img {
|
||||||
|
max-width: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.chat-resource-item {
|
||||||
|
max-width: 80px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: inline-flex;
|
||||||
|
border-radius: .3em;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 4px;
|
||||||
|
background-color: #373839;
|
||||||
|
border: 1px solid var(--foreground);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 3px;
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-resource-item .iconfont {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,243 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 下侧的设置按钮 -->
|
||||||
|
<Setting :tabId="tabId" v-model="modelValue" />
|
||||||
|
|
||||||
|
<!-- 编辑区 -->
|
||||||
|
<div class="k-rich-textarea">
|
||||||
|
<div
|
||||||
|
:ref="el => editor = el"
|
||||||
|
contenteditable="true"
|
||||||
|
class="rich-editor"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
@input="handleInput"
|
||||||
|
@keydown.backspace="handleBackspace"
|
||||||
|
@keydown.enter="handleKeydown"
|
||||||
|
@compositionstart="handleCompositionStart"
|
||||||
|
@compositionend="handleCompositionEnd"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, PropType, defineProps, defineEmits, computed, provide } from 'vue';
|
||||||
|
import type { RichTextItem } from './chat';
|
||||||
|
|
||||||
|
import Setting from './options/setting.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
tabId: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: '输入消息...'
|
||||||
|
},
|
||||||
|
customClass: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const modelValue = computed({
|
||||||
|
get() {
|
||||||
|
return props.modelValue;
|
||||||
|
},
|
||||||
|
set(value: string) {
|
||||||
|
emit('update:modelValue', value);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'pressEnter']);
|
||||||
|
|
||||||
|
const editor = ref<any>(null);
|
||||||
|
|
||||||
|
provide('editorContext', {
|
||||||
|
editor,
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleBackspace(event: KeyboardEvent) {
|
||||||
|
// 自定义 Backspace 行为
|
||||||
|
const editorElement = editor.value;
|
||||||
|
if (!(editorElement instanceof HTMLDivElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (!selection || !selection.rangeCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const range = selection.getRangeAt(0);
|
||||||
|
const startContainer = range.startContainer;
|
||||||
|
|
||||||
|
// 如果光标在 rich-item 元素中,阻止默认行为并删除整个元素
|
||||||
|
if (startContainer.parentElement?.classList.contains('rich-item')) {
|
||||||
|
event.preventDefault();
|
||||||
|
startContainer.parentElement.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInput(event: Event) {
|
||||||
|
const editorElement = editor.value;
|
||||||
|
if (!(editorElement instanceof HTMLDivElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fragments: string[] = [];
|
||||||
|
|
||||||
|
editorElement.childNodes.forEach(node => {
|
||||||
|
if (node.nodeType === Node.TEXT_NODE) {
|
||||||
|
|
||||||
|
fragments.push(node.textContent || '');
|
||||||
|
} else {
|
||||||
|
const element = node as HTMLElement;
|
||||||
|
|
||||||
|
const collection = element.getElementsByClassName('real-text');
|
||||||
|
const fragmentText = extractTextFromCollection(collection);
|
||||||
|
|
||||||
|
fragments.push(fragmentText || '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(fragments);
|
||||||
|
emit('update:modelValue', fragments.join(' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractTextFromCollection(collection: HTMLCollection) {
|
||||||
|
const texts = [];
|
||||||
|
for (let i = 0; i < collection.length; i++) {
|
||||||
|
texts.push(collection[i].textContent); // 或 .innerText
|
||||||
|
}
|
||||||
|
|
||||||
|
return texts.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isComposing = ref(false);
|
||||||
|
|
||||||
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
|
|
||||||
|
if (event.key === 'Enter' && !event.shiftKey && !isComposing.value) {
|
||||||
|
event.preventDefault();
|
||||||
|
const editorElement = editor.value;
|
||||||
|
if (!(editorElement instanceof HTMLDivElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空
|
||||||
|
editorElement.innerHTML = '';
|
||||||
|
|
||||||
|
emit('pressEnter', event);
|
||||||
|
|
||||||
|
} else if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
|
||||||
|
|
||||||
|
const editorElement = editor.value;
|
||||||
|
if (!(editorElement instanceof HTMLDivElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (!selection || !selection.rangeCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const range = selection.getRangeAt(0);
|
||||||
|
const startContainer = range.startContainer;
|
||||||
|
|
||||||
|
if (event.key === 'ArrowLeft') {
|
||||||
|
// 检查左侧节点
|
||||||
|
const previousSibling = startContainer.previousSibling;
|
||||||
|
if (previousSibling && previousSibling.nodeType !== Node.TEXT_NODE) {
|
||||||
|
event.preventDefault();
|
||||||
|
range.setStartBefore(previousSibling);
|
||||||
|
range.collapse(true);
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
}
|
||||||
|
} else if (event.key === 'ArrowRight') {
|
||||||
|
// 检查右侧节点
|
||||||
|
const nextSibling = startContainer.nextSibling;
|
||||||
|
if (nextSibling && nextSibling.nodeType !== Node.TEXT_NODE) {
|
||||||
|
event.preventDefault();
|
||||||
|
range.setStartAfter(nextSibling);
|
||||||
|
range.collapse(true);
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCompositionStart() {
|
||||||
|
isComposing.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCompositionEnd() {
|
||||||
|
isComposing.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.k-rich-textarea {
|
||||||
|
border: 1px solid var(--main-color);
|
||||||
|
background-color: var(--el-input-bg-color, var(--el-fill-color-blank));
|
||||||
|
background-image: none;
|
||||||
|
border-radius: .5em;
|
||||||
|
box-shadow: 0 0 0 1px var(--el-input-border-color, var(--el-border-color)) inset;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: var(--el-input-text-color, var(--el-text-color-regular));
|
||||||
|
padding: 10px 10px;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: var(--el-font-size-base);
|
||||||
|
position: relative;
|
||||||
|
vertical-align: bottom;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 50px;
|
||||||
|
transition: var(--el-transition-box-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-editor {
|
||||||
|
min-height: 100px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-editor:empty::before {
|
||||||
|
content: attr(placeholder);
|
||||||
|
color: #C0C4CC;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-item {
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-item-prompt {
|
||||||
|
background-color: #e8f0fe;
|
||||||
|
color: #1a73e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-item-resource {
|
||||||
|
background-color: #f1f3f4;
|
||||||
|
color: #202124;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.chat-resource-item {
|
||||||
|
max-width: 100px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
background-color: #373839;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-resource-item .iconfont {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
@ -1,9 +1,9 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import { Ref } from "vue";
|
import { Ref } from "vue";
|
||||||
import { ToolCall, ChatStorage, getToolSchema, MessageState } from "./chat";
|
import { ToolCall, ChatStorage, getToolSchema, MessageState } from "../chat-box/chat";
|
||||||
import { useMessageBridge } from "@/api/message-bridge";
|
import { useMessageBridge } from "@/api/message-bridge";
|
||||||
import type { OpenAI } from 'openai';
|
import type { OpenAI } from 'openai';
|
||||||
import { callTool } from "../tool/tools";
|
import { callTool } from "../../tool/tools";
|
||||||
import { llmManager, llms } from "@/views/setting/llm";
|
import { llmManager, llms } from "@/views/setting/llm";
|
||||||
import { pinkLog, redLog } from "@/views/setting/util";
|
import { pinkLog, redLog } from "@/views/setting/util";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
@ -1,4 +1,4 @@
|
|||||||
import { IExtraInfo } from "./chat";
|
import { IExtraInfo } from "../chat-box/chat";
|
||||||
|
|
||||||
export interface UsageStatistic {
|
export interface UsageStatistic {
|
||||||
input: number;
|
input: number;
|
@ -40,52 +40,29 @@
|
|||||||
<div>
|
<div>
|
||||||
<!-- <span class="iconfont icon-openmcp"></span> -->
|
<!-- <span class="iconfont icon-openmcp"></span> -->
|
||||||
<span>{{ t('press-and-run') }}
|
<span>{{ t('press-and-run') }}
|
||||||
|
|
||||||
<span style="padding: 5px 15px; border-radius: .5em; background-color: var(--background);">
|
<span style="padding: 5px 15px; border-radius: .5em; background-color: var(--background);">
|
||||||
<span class="iconfont icon-send"></span>
|
<span class="iconfont icon-send"></span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="chat-footer" ref="footerRef">
|
<ChatBox
|
||||||
<div class="input-area">
|
:ref="el => footerRef = el"
|
||||||
<div class="input-wrapper">
|
:tab-id="props.tabId"
|
||||||
<Setting :tabId="tabId" v-model="userInput" />
|
/>
|
||||||
|
|
||||||
<KCuteTextarea
|
|
||||||
v-model="userInput"
|
|
||||||
:placeholder="t('enter-message-dot')"
|
|
||||||
:customClass="'chat-input'"
|
|
||||||
@press-enter="handleSend()"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<el-button type="primary" @click="isLoading ? handleAbort() : handleSend()" class="send-button">
|
|
||||||
<span v-if="!isLoading" class="iconfont icon-send"></span>
|
|
||||||
<span v-else class="iconfont icon-stop"></span>
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, defineComponent, defineProps, onUnmounted, computed, nextTick, watch } from 'vue';
|
import { ref, onMounted, defineComponent, defineProps, onUnmounted, computed, nextTick, watch, provide } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { ElMessage, ScrollbarInstance } from 'element-plus';
|
import { ElMessage, ScrollbarInstance } from 'element-plus';
|
||||||
import { tabs } from '../panel';
|
import { tabs } from '../panel';
|
||||||
import { ChatMessage, ChatStorage, IRenderMessage, MessageState, ToolCall } from './chat';
|
import { ChatMessage, ChatStorage, IRenderMessage, MessageState, ToolCall } from './chat-box/chat';
|
||||||
|
|
||||||
import { TaskLoop } from './task-loop';
|
|
||||||
import { llmManager, llms } from '@/views/setting/llm';
|
|
||||||
|
|
||||||
import * as Message from './message';
|
import * as Message from './message';
|
||||||
import Setting from './options/setting.vue';
|
import ChatBox from './chat-box/index.vue';
|
||||||
import KCuteTextarea from '@/components/k-cute-textarea/index.vue';
|
|
||||||
|
|
||||||
import { provide } from 'vue';
|
|
||||||
|
|
||||||
defineComponent({ name: 'chat' });
|
defineComponent({ name: 'chat' });
|
||||||
|
|
||||||
@ -101,8 +78,6 @@ const props = defineProps({
|
|||||||
const tab = tabs.content[props.tabId];
|
const tab = tabs.content[props.tabId];
|
||||||
const tabStorage = tab.storage as ChatStorage;
|
const tabStorage = tab.storage as ChatStorage;
|
||||||
|
|
||||||
const userInput = ref('');
|
|
||||||
|
|
||||||
// 创建 messages
|
// 创建 messages
|
||||||
if (!tabStorage.messages) {
|
if (!tabStorage.messages) {
|
||||||
tabStorage.messages = [] as ChatMessage[];
|
tabStorage.messages = [] as ChatMessage[];
|
||||||
@ -158,18 +133,19 @@ const streamingToolCalls = ref<ToolCall[]>([]);
|
|||||||
|
|
||||||
const chatContainerRef = ref<any>(null);
|
const chatContainerRef = ref<any>(null);
|
||||||
const messageListRef = ref<any>(null);
|
const messageListRef = ref<any>(null);
|
||||||
|
const footerRef = ref<any>(null);
|
||||||
|
|
||||||
const footerRef = ref<HTMLElement>();
|
|
||||||
const scrollHeight = ref('500px');
|
const scrollHeight = ref('500px');
|
||||||
|
|
||||||
const updateScrollHeight = () => {
|
function updateScrollHeight() {
|
||||||
if (chatContainerRef.value && footerRef.value) {
|
if (chatContainerRef.value && footerRef.value) {
|
||||||
const containerHeight = chatContainerRef.value.clientHeight;
|
const containerHeight = chatContainerRef.value.clientHeight;
|
||||||
const footerHeight = footerRef.value.clientHeight;
|
const footerHeight = footerRef.value.clientHeight;
|
||||||
scrollHeight.value = `${containerHeight - footerHeight}px`;
|
scrollHeight.value = `${containerHeight - footerHeight}px`;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
provide('updateScrollHeight', updateScrollHeight);
|
||||||
|
|
||||||
const autoScroll = ref(true);
|
const autoScroll = ref(true);
|
||||||
const scrollbarRef = ref<ScrollbarInstance>();
|
const scrollbarRef = ref<ScrollbarInstance>();
|
||||||
@ -184,8 +160,13 @@ const handleScroll = ({ scrollTop, scrollHeight, clientHeight }: {
|
|||||||
autoScroll.value = scrollTop + clientHeight >= scrollHeight - 10;
|
autoScroll.value = scrollTop + clientHeight >= scrollHeight - 10;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
provide('streamingContent', streamingContent);
|
||||||
|
provide('streamingToolCalls', streamingToolCalls);
|
||||||
|
provide('isLoading', isLoading);
|
||||||
|
provide('autoScroll', autoScroll);
|
||||||
|
|
||||||
// 修改 scrollToBottom 方法
|
// 修改 scrollToBottom 方法
|
||||||
const scrollToBottom = async () => {
|
async function scrollToBottom() {
|
||||||
if (!scrollbarRef.value || !messageListRef.value) return;
|
if (!scrollbarRef.value || !messageListRef.value) return;
|
||||||
|
|
||||||
await nextTick(); // 等待 DOM 更新
|
await nextTick(); // 等待 DOM 更新
|
||||||
@ -198,7 +179,9 @@ const scrollToBottom = async () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Scroll to bottom failed:', error);
|
console.error('Scroll to bottom failed:', error);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
provide('scrollToBottom', scrollToBottom);
|
||||||
|
|
||||||
// 添加对 streamingContent 的监听
|
// 添加对 streamingContent 的监听
|
||||||
watch(streamingContent, () => {
|
watch(streamingContent, () => {
|
||||||
@ -213,78 +196,6 @@ watch(streamingToolCalls, () => {
|
|||||||
}
|
}
|
||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
|
|
||||||
let loop: TaskLoop | undefined = undefined;
|
|
||||||
|
|
||||||
const handleSend = (newMessage?: string) => {
|
|
||||||
const userMessage = newMessage || userInput.value.trim();
|
|
||||||
if (!userMessage || isLoading.value) return;
|
|
||||||
|
|
||||||
autoScroll.value = true;
|
|
||||||
isLoading.value = true;
|
|
||||||
|
|
||||||
loop = new TaskLoop(streamingContent, streamingToolCalls);
|
|
||||||
|
|
||||||
loop.registerOnError((error) => {
|
|
||||||
|
|
||||||
ElMessage({
|
|
||||||
message: error.msg,
|
|
||||||
type: 'error',
|
|
||||||
duration: 3000
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error.state === MessageState.ReceiveChunkError) {
|
|
||||||
tabStorage.messages.push({
|
|
||||||
role: 'assistant',
|
|
||||||
content: error.msg,
|
|
||||||
extraInfo: {
|
|
||||||
created: Date.now(),
|
|
||||||
state: error.state,
|
|
||||||
serverName: llms[llmManager.currentModelIndex].id || 'unknown'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
loop.registerOnChunk(() => {
|
|
||||||
scrollToBottom();
|
|
||||||
});
|
|
||||||
|
|
||||||
loop.registerOnDone(() => {
|
|
||||||
isLoading.value = false;
|
|
||||||
scrollToBottom();
|
|
||||||
});
|
|
||||||
|
|
||||||
loop.registerOnEpoch(() => {
|
|
||||||
isLoading.value = true;
|
|
||||||
scrollToBottom();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
loop.start(tabStorage, userMessage);
|
|
||||||
userInput.value = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAbort = () => {
|
|
||||||
if (loop) {
|
|
||||||
loop.abort();
|
|
||||||
isLoading.value = false;
|
|
||||||
ElMessage.info('请求已中止');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
provide('handleSend', handleSend);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
updateScrollHeight();
|
|
||||||
window.addEventListener('resize', updateScrollHeight);
|
|
||||||
scrollToBottom();
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('resize', updateScrollHeight);
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -387,67 +298,6 @@ onUnmounted(() => {
|
|||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-footer {
|
|
||||||
padding: 16px;
|
|
||||||
border-top: 1px solid var(--el-border-color);
|
|
||||||
flex-shrink: 0;
|
|
||||||
position: absolute;
|
|
||||||
height: fit-content !important;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-area {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-wrapper {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-input {
|
|
||||||
padding-right: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-input textarea {
|
|
||||||
border-radius: .5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.send-button {
|
|
||||||
position: absolute !important;
|
|
||||||
right: 8px !important;
|
|
||||||
bottom: 8px !important;
|
|
||||||
height: auto;
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-size: 20px;
|
|
||||||
border-radius: 1.2em !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.chat-settings) {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0px;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.typing-cursor {
|
|
||||||
animation: blink 1s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes blink {
|
|
||||||
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-text p,
|
.message-text p,
|
||||||
.message-text h3,
|
.message-text h3,
|
||||||
.message-text ol,
|
.message-text ol,
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineProps } from 'vue';
|
import { defineProps } from 'vue';
|
||||||
import { markdownToHtml } from '../markdown/markdown';
|
import { markdownToHtml } from '@/components/main-panel/chat/markdown/markdown';
|
||||||
|
|
||||||
import MessageMeta from './message-meta.vue';
|
import MessageMeta from './message-meta.vue';
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent, defineProps, ref, computed } from 'vue';
|
import { defineComponent, defineProps, ref, computed } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { makeUsageStatistic } from '../usage';
|
import { makeUsageStatistic } from '@/components/main-panel/chat/core/usage';
|
||||||
|
|
||||||
defineComponent({ name: 'message-meta' });
|
defineComponent({ name: 'message-meta' });
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineProps } from 'vue';
|
import { defineProps } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { markdownToHtml } from '../markdown/markdown';
|
import { markdownToHtml } from '@/components/main-panel/chat/markdown/markdown';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ import { getBlobUrlByFilename } from '@/hook/util';
|
|||||||
import { defineComponent, PropType, defineProps, ref, defineEmits } from 'vue';
|
import { defineComponent, PropType, defineProps, ref, defineEmits } from 'vue';
|
||||||
|
|
||||||
defineComponent({ name: 'toolcall-result-item' });
|
defineComponent({ name: 'toolcall-result-item' });
|
||||||
const emit = defineEmits(['update:item', 'update:ocr-done']);
|
const emits = defineEmits(['update:item', 'update:ocr-done']);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
item: {
|
item: {
|
||||||
@ -73,16 +73,20 @@ if (ocr) {
|
|||||||
}
|
}
|
||||||
}, { once: false });
|
}, { once: false });
|
||||||
|
|
||||||
bridge.addCommandListener('ocr/worker/done', () => {
|
bridge.addCommandListener('ocr/worker/done', data => {
|
||||||
|
if (data.id !== workerId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
progress.value = 1;
|
progress.value = 1;
|
||||||
finishProcess.value = true;
|
finishProcess.value = true;
|
||||||
|
|
||||||
if (props.item._meta) {
|
if (props.item._meta) {
|
||||||
const { _meta, ...rest } = props.item;
|
const { _meta, ...rest } = props.item;
|
||||||
emit('update:item', { ...rest });
|
emits('update:item', { ...rest });
|
||||||
}
|
}
|
||||||
|
|
||||||
emit('update:ocr-done');
|
emits('update:ocr-done');
|
||||||
|
|
||||||
cancel();
|
cancel();
|
||||||
}, { once: true });
|
}, { once: true });
|
||||||
|
@ -96,9 +96,9 @@
|
|||||||
import { defineProps, ref, watch, PropType, computed, defineEmits } from 'vue';
|
import { defineProps, ref, watch, PropType, computed, defineEmits } from 'vue';
|
||||||
|
|
||||||
import MessageMeta from './message-meta.vue';
|
import MessageMeta from './message-meta.vue';
|
||||||
import { markdownToHtml } from '../markdown/markdown';
|
import { markdownToHtml } from '@/components/main-panel/chat/markdown/markdown';
|
||||||
import { createTest } from '@/views/setting/llm';
|
import { createTest } from '@/views/setting/llm';
|
||||||
import { IRenderMessage, MessageState } from '../chat';
|
import { IRenderMessage, MessageState } from '../chat-box/chat';
|
||||||
import { ToolCallContent } from '@/hook/type';
|
import { ToolCallContent } from '@/hook/type';
|
||||||
|
|
||||||
import ToolcallResultItem from './toolcall-result-item.vue';
|
import ToolcallResultItem from './toolcall-result-item.vue';
|
||||||
|
@ -1,81 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-tooltip :content="t('prompts')" placement="top">
|
|
||||||
<div class="setting-button" @click="showChoosePrompt = true">
|
|
||||||
<span class="iconfont icon-chat"></span>
|
|
||||||
</div>
|
|
||||||
</el-tooltip>
|
|
||||||
|
|
||||||
<!-- 上下文长度设置 - 改为滑块形式 -->
|
|
||||||
<el-dialog v-model="showChoosePrompt" :title="t('prompts')" width="400px">
|
|
||||||
|
|
||||||
<div class="prompt-template-container-scrollbar" v-if="!selectPrompt">
|
|
||||||
<PromptTemplates
|
|
||||||
:tab-id="-1"
|
|
||||||
@prompt-selected="prompt => selectPrompt = prompt"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<PromptReader
|
|
||||||
:tab-id="-1"
|
|
||||||
:current-prompt-name="selectPrompt!.name"
|
|
||||||
@prompt-get-response="msg => whenGetPromptResponse(msg)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<el-button v-if="selectPrompt" @click="selectPrompt = undefined;">{{ t('return') }}</el-button>
|
|
||||||
<el-button @click="showChoosePrompt = false; selectPrompt = undefined;">{{ t("cancel") }}</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { inject, ref, defineProps, PropType, defineEmits } from 'vue';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import { ChatStorage } from '../chat';
|
|
||||||
import { PromptsGetResponse, PromptTemplate } from '@/hook/type';
|
|
||||||
|
|
||||||
import PromptTemplates from '../../prompt/prompt-templates.vue';
|
|
||||||
import PromptReader from '../../prompt/prompt-reader.vue';
|
|
||||||
import { ElMessage } from 'element-plus';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const tabStorage = inject('tabStorage') as ChatStorage;
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
modelValue: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const emits = defineEmits([ 'update:modelValue' ]);
|
|
||||||
|
|
||||||
let selectPrompt = ref<PromptTemplate | undefined>(undefined);
|
|
||||||
|
|
||||||
const showChoosePrompt = ref(false);
|
|
||||||
|
|
||||||
function whenGetPromptResponse(msg: PromptsGetResponse) {
|
|
||||||
try {
|
|
||||||
const content = msg.messages[0].content;
|
|
||||||
|
|
||||||
selectPrompt.value = undefined;
|
|
||||||
|
|
||||||
if (content) {
|
|
||||||
emits('update:modelValue', props.modelValue + content);
|
|
||||||
}
|
|
||||||
|
|
||||||
showChoosePrompt.value = false;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error((error as Error).message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.icon-length {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,42 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-tooltip :content="t('context-length')" placement="top">
|
|
||||||
<div class="setting-button" @click="showContextLengthDialog = true">
|
|
||||||
<span class="iconfont icon-length"></span>
|
|
||||||
<span class="value-badge">{{ tabStorage.settings.contextLength }}</span>
|
|
||||||
</div>
|
|
||||||
</el-tooltip>
|
|
||||||
|
|
||||||
<!-- 上下文长度设置 - 改为滑块形式 -->
|
|
||||||
<el-dialog v-model="showContextLengthDialog" :title="t('context-length') + ' ' + tabStorage.settings.contextLength"
|
|
||||||
width="400px">
|
|
||||||
<div class="slider-container">
|
|
||||||
<el-slider v-model="tabStorage.settings.contextLength" :min="1" :max="99" :step="1" />
|
|
||||||
<div class="slider-tips">
|
|
||||||
<span> 1: {{ t('single-dialog') }}</span>
|
|
||||||
<span> >1: {{ t('multi-dialog') }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="showContextLengthDialog = false">{{ t("cancel") }}</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { defineComponent, inject, ref } from 'vue';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import { ChatStorage } from '../chat';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const tabStorage = inject('tabStorage') as ChatStorage;
|
|
||||||
|
|
||||||
const showContextLengthDialog = ref(false);
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.icon-length {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -13,7 +13,7 @@
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<el-scrollbar height="350px">
|
<el-scrollbar>
|
||||||
<div
|
<div
|
||||||
class="output-content"
|
class="output-content"
|
||||||
contenteditable="false"
|
contenteditable="false"
|
||||||
|
@ -4,16 +4,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="resource-reader-container">
|
<div class="resource-reader-container">
|
||||||
<el-form :model="tabStorage.formData" :rules="formRules" ref="formRef" label-position="top">
|
<el-form :model="tabStorage.formData" :rules="formRules" ref="formRef" label-position="top">
|
||||||
<el-form-item v-for="param in currentResource?.params" :key="param.name"
|
<el-form-item v-for="param in currentResource?.params" :key="param.name" :label="param.name"
|
||||||
:label="param.name" :prop="param.name">
|
:prop="param.name">
|
||||||
<!-- 根据不同类型渲染不同输入组件 -->
|
<!-- 根据不同类型渲染不同输入组件 -->
|
||||||
<el-input v-if="param.type === 'string'" v-model="tabStorage.formData[param.name]"
|
<el-input v-if="param.type === 'string'" v-model="tabStorage.formData[param.name]"
|
||||||
:placeholder="param.placeholder || `请输入${param.name}`"
|
:placeholder="param.placeholder || `请输入${param.name}`" @keydown.enter.prevent="handleSubmit" />
|
||||||
@keydown.enter.prevent="handleSubmit" />
|
|
||||||
|
|
||||||
<el-input-number v-else-if="param.type === 'number'" v-model="tabStorage.formData[param.name]"
|
<el-input-number v-else-if="param.type === 'number'" v-model="tabStorage.formData[param.name]"
|
||||||
:placeholder="param.placeholder || `请输入${param.name}`"
|
:placeholder="param.placeholder || `请输入${param.name}`" @keydown.enter.prevent="handleSubmit" />
|
||||||
@keydown.enter.prevent="handleSubmit" />
|
|
||||||
|
|
||||||
<el-switch v-else-if="param.type === 'boolean'" v-model="tabStorage.formData[param.name]" />
|
<el-switch v-else-if="param.type === 'boolean'" v-model="tabStorage.formData[param.name]" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -31,7 +29,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent, defineProps, watch, ref, computed } from 'vue';
|
import { defineComponent, defineProps, watch, ref, computed, reactive, defineEmits } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import type { FormInstance, FormRules } from 'element-plus';
|
import type { FormInstance, FormRules } from 'element-plus';
|
||||||
import { tabs } from '../panel';
|
import { tabs } from '../panel';
|
||||||
@ -48,11 +46,28 @@ const props = defineProps({
|
|||||||
tabId: {
|
tabId: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
currentResourceName: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const tab = tabs.content[props.tabId];
|
const emits = defineEmits(['resource-get-response']);
|
||||||
const tabStorage = tab.storage as ResourceStorage;
|
|
||||||
|
let tabStorage: ResourceStorage;
|
||||||
|
|
||||||
|
if (props.tabId >= 0) {
|
||||||
|
const tab = tabs.content[props.tabId];
|
||||||
|
tabStorage = tab.storage as ResourceStorage;
|
||||||
|
} else {
|
||||||
|
tabStorage = reactive({
|
||||||
|
currentType: 'resource',
|
||||||
|
currentResourceName: props.currentResourceName || '',
|
||||||
|
formData: {},
|
||||||
|
lastResourceReadResponse: undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!tabStorage.formData) {
|
if (!tabStorage.formData) {
|
||||||
tabStorage.formData = {};
|
tabStorage.formData = {};
|
||||||
@ -71,7 +86,7 @@ const currentResource = computed(() => {
|
|||||||
const viewParams = params.map(param => ({
|
const viewParams = params.map(param => ({
|
||||||
name: param,
|
name: param,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
placeholder: t('enter') +' ' + param,
|
placeholder: t('enter') + ' ' + param,
|
||||||
required: true
|
required: true
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -97,9 +112,6 @@ const formRules = computed<FormRules>(() => {
|
|||||||
return rules;
|
return rules;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 初始化表单数据
|
// 初始化表单数据
|
||||||
const initFormData = () => {
|
const initFormData = () => {
|
||||||
if (!currentResource.value?.params) return;
|
if (!currentResource.value?.params) return;
|
||||||
@ -110,7 +122,7 @@ const initFormData = () => {
|
|||||||
newSchemaDataForm[param.name] = getDefaultValue(param);
|
newSchemaDataForm[param.name] = getDefaultValue(param);
|
||||||
const originType = normaliseJavascriptType(typeof tabStorage.formData[param.name]);
|
const originType = normaliseJavascriptType(typeof tabStorage.formData[param.name]);
|
||||||
|
|
||||||
if (tabStorage.formData[param.name]!== undefined && originType === param.type) {
|
if (tabStorage.formData[param.name] !== undefined && originType === param.type) {
|
||||||
newSchemaDataForm[param.name] = tabStorage.formData[param.name];
|
newSchemaDataForm[param.name] = tabStorage.formData[param.name];
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -129,28 +141,31 @@ function getUri() {
|
|||||||
return uri;
|
return uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetResource = resourcesManager.resources.find(resources => resources.name === tabStorage.currentResourceName);
|
const currentResourceName = props.tabId >= 0 ? tabStorage.currentResourceName : props.currentResourceName;
|
||||||
|
|
||||||
|
const targetResource = resourcesManager.resources.find(resources => resources.name === currentResourceName);
|
||||||
|
|
||||||
return targetResource?.uri;
|
return targetResource?.uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提交表单
|
// 提交表单
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
const uri = getUri();
|
const uri = getUri();
|
||||||
console.log(uri);
|
|
||||||
|
|
||||||
const bridge = useMessageBridge();
|
const bridge = useMessageBridge();
|
||||||
const { code, msg } = await bridge.commandRequest('resources/read', { resourceUri: uri });
|
const { code, msg } = await bridge.commandRequest('resources/read', { resourceUri: uri });
|
||||||
|
|
||||||
if (code === 200) {
|
tabStorage.lastResourceReadResponse = msg;
|
||||||
tabStorage.lastResourceReadResponse = msg;
|
|
||||||
}
|
emits('resource-get-response', msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听资源变化重置表单
|
if (props.tabId >= 0) {
|
||||||
watch(() => tabStorage.currentResourceName, () => {
|
watch(() => tabStorage.currentResourceName, () => {
|
||||||
initFormData();
|
initFormData();
|
||||||
resetForm();
|
resetForm();
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
<div class="resource-template-container">
|
<div class="resource-template-container">
|
||||||
<div
|
<div
|
||||||
class="item"
|
class="item"
|
||||||
:class="{ 'active': tabStorage.currentType === 'template' && tabStorage.currentResourceName === template.name }"
|
:class="{ 'active': props.tabId >= 0 && tabStorage.currentType === 'template' && tabStorage.currentResourceName === template.name }"
|
||||||
v-for="template of resourcesManager.templates"
|
v-for="template of resourcesManager.templates"
|
||||||
:key="template.name"
|
:key="template.name"
|
||||||
@click="handleClick(template)"
|
@click="handleClick(template)"
|
||||||
@ -31,7 +31,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useMessageBridge } from '@/api/message-bridge';
|
import { useMessageBridge } from '@/api/message-bridge';
|
||||||
import { CasualRestAPI, ResourceTemplate, ResourceTemplatesListResponse } from '@/hook/type';
|
import { CasualRestAPI, ResourceTemplate, ResourceTemplatesListResponse } from '@/hook/type';
|
||||||
import { onMounted, onUnmounted, defineProps, ref } from 'vue';
|
import { onMounted, onUnmounted, defineProps, ref, reactive } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { resourcesManager, ResourceStorage } from './resources';
|
import { resourcesManager, ResourceStorage } from './resources';
|
||||||
import { tabs } from '../panel';
|
import { tabs } from '../panel';
|
||||||
@ -47,8 +47,19 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const tab = tabs.content[props.tabId];
|
let tabStorage: ResourceStorage;
|
||||||
const tabStorage = tab.storage as ResourceStorage;
|
|
||||||
|
if (props.tabId >= 0) {
|
||||||
|
const tab = tabs.content[props.tabId];
|
||||||
|
tabStorage = tab.storage as ResourceStorage;
|
||||||
|
} else {
|
||||||
|
tabStorage = reactive({
|
||||||
|
currentType:'template',
|
||||||
|
currentResourceName: '',
|
||||||
|
formData: {},
|
||||||
|
lastResourceReadResponse: undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function reloadResources(option: { first: boolean }) {
|
function reloadResources(option: { first: boolean }) {
|
||||||
bridge.postMessage({
|
bridge.postMessage({
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
<div class="resource-template-container">
|
<div class="resource-template-container">
|
||||||
<div
|
<div
|
||||||
class="item"
|
class="item"
|
||||||
:class="{ 'active': tabStorage.currentType === 'resource' && tabStorage.currentResourceName === resource.name }"
|
:class="{ 'active': props.tabId >= 0 && tabStorage.currentType === 'resource' && tabStorage.currentResourceName === resource.name }"
|
||||||
v-for="resource of resourcesManager.resources"
|
v-for="resource of resourcesManager.resources"
|
||||||
:key="resource.uri"
|
:key="resource.uri"
|
||||||
@click="handleClick(resource)"
|
@click="handleClick(resource)"
|
||||||
@ -28,7 +28,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useMessageBridge } from '@/api/message-bridge';
|
import { useMessageBridge } from '@/api/message-bridge';
|
||||||
import { CasualRestAPI, Resources, ResourcesListResponse } from '@/hook/type';
|
import { CasualRestAPI, Resources, ResourcesListResponse } from '@/hook/type';
|
||||||
import { onMounted, onUnmounted, defineProps, ref } from 'vue';
|
import { onMounted, onUnmounted, defineProps, defineEmits, reactive } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { resourcesManager, ResourceStorage } from './resources';
|
import { resourcesManager, ResourceStorage } from './resources';
|
||||||
import { tabs } from '../panel';
|
import { tabs } from '../panel';
|
||||||
@ -44,8 +44,21 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const tab = tabs.content[props.tabId];
|
const emits = defineEmits([ 'resource-selected' ]);
|
||||||
const tabStorage = tab.storage as ResourceStorage;
|
|
||||||
|
let tabStorage: ResourceStorage;
|
||||||
|
|
||||||
|
if (props.tabId >= 0) {
|
||||||
|
const tab = tabs.content[props.tabId];
|
||||||
|
tabStorage = tab.storage as ResourceStorage;
|
||||||
|
} else {
|
||||||
|
tabStorage = reactive({
|
||||||
|
currentType:'resource',
|
||||||
|
currentResourceName: '',
|
||||||
|
formData: {},
|
||||||
|
lastResourceReadResponse: undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function reloadResources(option: { first: boolean }) {
|
function reloadResources(option: { first: boolean }) {
|
||||||
bridge.postMessage({
|
bridge.postMessage({
|
||||||
@ -66,6 +79,8 @@ function handleClick(resource: Resources) {
|
|||||||
tabStorage.currentType = 'resource';
|
tabStorage.currentType = 'resource';
|
||||||
tabStorage.currentResourceName = resource.name;
|
tabStorage.currentResourceName = resource.name;
|
||||||
tabStorage.lastResourceReadResponse = undefined;
|
tabStorage.lastResourceReadResponse = undefined;
|
||||||
|
|
||||||
|
emits('resource-selected', resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
let commandCancel: (() => void);
|
let commandCancel: (() => void);
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<el-scrollbar height="350px">
|
<el-scrollbar>
|
||||||
<div
|
<div
|
||||||
class="output-content"
|
class="output-content"
|
||||||
contenteditable="false"
|
contenteditable="false"
|
||||||
@ -104,7 +104,7 @@ const formattedJson = computed(() => {
|
|||||||
.resource-logger .output-content {
|
.resource-logger .output-content {
|
||||||
border-radius: .5em;
|
border-radius: .5em;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
min-height: 300px;
|
min-height: 600px;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
font-family: var(--code-font-family);
|
font-family: var(--code-font-family);
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
@ -167,9 +167,6 @@ watch(() => tabStorage.currentToolName, () => {
|
|||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-executor-container {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-executor-container .el-switch .el-switch__action {
|
.tool-executor-container .el-switch .el-switch__action {
|
||||||
background-color: var(--main-color);
|
background-color: var(--main-color);
|
||||||
|
@ -149,7 +149,9 @@ function toggleConnectionPanel() {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
white-space: wrap;
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
background-color: #f39a6d;
|
background-color: #f39a6d;
|
||||||
padding: 5px 12px;
|
padding: 5px 12px;
|
||||||
border-radius: .5em;
|
border-radius: .5em;
|
||||||
|
@ -65,6 +65,9 @@ export interface Resources {
|
|||||||
uri: string;
|
uri: string;
|
||||||
name: string;
|
name: string;
|
||||||
mimeType: string;
|
mimeType: string;
|
||||||
|
text?: string;
|
||||||
|
blob?: string;
|
||||||
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResourceTemplatesListResponse {
|
export interface ResourceTemplatesListResponse {
|
||||||
|
@ -70,6 +70,12 @@ export async function getBlobUrlByFilename(filename: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getImageBlobUrlByBase64(base64String: string, mimeType: string, cacheKey?: string) {
|
export function getImageBlobUrlByBase64(base64String: string, mimeType: string, cacheKey?: string) {
|
||||||
|
|
||||||
|
// 检查缓存中是否存在该文件
|
||||||
|
if (cacheKey && blobUrlCache.has(cacheKey)) {
|
||||||
|
return blobUrlCache.get(cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
const byteCharacters = atob(base64String);
|
const byteCharacters = atob(base64String);
|
||||||
const byteNumbers = new Array(byteCharacters.length);
|
const byteNumbers = new Array(byteCharacters.length);
|
||||||
for (let i = 0; i < byteCharacters.length; i++) {
|
for (let i = 0; i < byteCharacters.length; i++) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ChatStorage } from '@/components/main-panel/chat/chat';
|
import { ChatStorage } from '@/components/main-panel/chat/chat-box/chat';
|
||||||
import { TaskLoop } from '@/components/main-panel/chat/task-loop';
|
import { TaskLoop } from '@/components/main-panel/chat/core/task-loop';
|
||||||
import { llmManager } from './llm';
|
import { llmManager } from './llm';
|
||||||
import { reactive, ref } from 'vue';
|
import { reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { markRaw, reactive } from 'vue';
|
import { markRaw, reactive } from 'vue';
|
||||||
import { createTab, debugModes, tabs } from '@/components/main-panel/panel';
|
import { createTab, debugModes, tabs } from '@/components/main-panel/panel';
|
||||||
import { ToolStorage } from '@/components/main-panel/tool/tools';
|
import { ToolStorage } from '@/components/main-panel/tool/tools';
|
||||||
import { ToolCall } from '@/components/main-panel/chat/chat';
|
import { ToolCall } from '@/components/main-panel/chat/chat-box/chat';
|
||||||
|
|
||||||
import I18n from '@/i18n';
|
import I18n from '@/i18n';
|
||||||
const { t } = I18n.global;
|
const { t } = I18n.global;
|
||||||
|
@ -9,6 +9,16 @@ mcp = FastMCP('锦恢的 MCP Server', version="11.45.14")
|
|||||||
def add(a: int, b: int) -> int:
|
def add(a: int, b: int) -> int:
|
||||||
return a + b
|
return a + b
|
||||||
|
|
||||||
|
@mcp.resource(
|
||||||
|
uri="network://log",
|
||||||
|
name='network_log',
|
||||||
|
description='用于演示的一个无参数资源协议'
|
||||||
|
)
|
||||||
|
def network_log() -> str:
|
||||||
|
# 访问处理 greeting://{name} 资源访问协议,然后返回
|
||||||
|
# 此处方便起见,直接返回一个 Hello,balabala 了
|
||||||
|
return f"Response from ..."
|
||||||
|
|
||||||
@mcp.resource(
|
@mcp.resource(
|
||||||
uri="greeting://{name}",
|
uri="greeting://{name}",
|
||||||
name='greeting',
|
name='greeting',
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Controller, RequestClientType } from "../common";
|
import { Controller, RequestClientType } from "../common";
|
||||||
import { PostMessageble } from "../hook/adapter";
|
import { PostMessageble } from "../hook/adapter";
|
||||||
import { diskStorage } from "../hook/db";
|
import { diskStorage } from "../hook/db";
|
||||||
|
import { createOcrWorker, saveBase64ImageData } from "./ocr.service";
|
||||||
|
|
||||||
export class OcrController {
|
export class OcrController {
|
||||||
@Controller('ocr/get-ocr-image')
|
@Controller('ocr/get-ocr-image')
|
||||||
@ -15,4 +16,21 @@ export class OcrController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Controller('ocr/start-ocr')
|
||||||
|
async startOcr(client: RequestClientType, data: any, webview: PostMessageble) {
|
||||||
|
const { base64String, mimeType } = data;
|
||||||
|
|
||||||
|
const filename = saveBase64ImageData(base64String, mimeType);
|
||||||
|
const worker = createOcrWorker(filename, webview);
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
msg: {
|
||||||
|
filename,
|
||||||
|
workerId: worker.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -2,47 +2,37 @@
|
|||||||
"currentIndex": 0,
|
"currentIndex": 0,
|
||||||
"tabs": [
|
"tabs": [
|
||||||
{
|
{
|
||||||
"name": "交互测试",
|
"name": "资源",
|
||||||
"icon": "icon-robot",
|
"icon": "icon-file",
|
||||||
"type": "blank",
|
"type": "blank",
|
||||||
"componentIndex": 3,
|
"componentIndex": 0,
|
||||||
"storage": {
|
"storage": {
|
||||||
"messages": [],
|
"formData": {},
|
||||||
"settings": {
|
"currentType": "resource",
|
||||||
"modelIndex": 0,
|
"currentResourceName": "network_log",
|
||||||
"enableTools": [
|
"lastResourceReadResponse": {
|
||||||
|
"contents": [
|
||||||
{
|
{
|
||||||
"name": "add",
|
"uri": "network://log",
|
||||||
"description": "对两个数字进行实数域的加法",
|
"mimeType": "text/plain",
|
||||||
"enabled": true
|
"text": "Response from ..."
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "multiply",
|
|
||||||
"description": "对两个数字进行实数域的乘法运算",
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "is_even",
|
|
||||||
"description": "判断一个整数是否为偶数",
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "capitalize",
|
|
||||||
"description": "将字符串首字母大写",
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "get_weather_by_city_code",
|
|
||||||
"description": "根据城市天气预报的城市编码 (int),获取指定城市的天气信息",
|
|
||||||
"enabled": true
|
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"enableWebSearch": false,
|
|
||||||
"temperature": 0.7,
|
|
||||||
"contextLength": 20,
|
|
||||||
"systemPrompt": ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "工具",
|
||||||
|
"icon": "icon-tool",
|
||||||
|
"type": "blank",
|
||||||
|
"componentIndex": 2,
|
||||||
|
"storage": {
|
||||||
|
"formData": {
|
||||||
|
"a": 0,
|
||||||
|
"b": 0
|
||||||
|
},
|
||||||
|
"currentToolName": "add"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user