支持富文本编辑器,支持在客户端对提词和资源进行选择
This commit is contained in:
parent
cefa6b2af5
commit
96d36c0706
@ -1,9 +1,10 @@
|
||||
# Change Log
|
||||
|
||||
## [main] 0.0.7
|
||||
- 优化页面布局,使得调试内容更加紧凑
|
||||
- 扩大默认的上下文长度
|
||||
- 增加「通用选项」,用于设置mcp服务器的最大的等待时间
|
||||
- 优化页面布局,使得调试窗口可以显示更多内容
|
||||
- 扩大默认的上下文长度 10 -> 20
|
||||
- 增加「通用选项」 -> 「MCP工具最长调用时间 (sec)」
|
||||
- 支持富文本输入框,现在可以将 prompt 和 resource 嵌入到输入框中 进行 大规模 prompt engineering 调试工作了
|
||||
|
||||
## [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 interface IRenderMessage {
|
||||
@ -105,3 +122,8 @@ export function getToolSchema(enableTools: EnableToolItem[]) {
|
||||
}
|
||||
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 />
|
||||
<SystemPrompt />
|
||||
<ToolUse />
|
||||
<Prompt v-model="modelValue" />
|
||||
<Prompt />
|
||||
<Resource />
|
||||
<Websearch />
|
||||
<Temperature />
|
||||
<ContextLength />
|
||||
@ -11,15 +12,16 @@
|
||||
</template>
|
||||
|
||||
<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 { tabs } from '../../panel';
|
||||
import { tabs } from '@/components/main-panel/panel';
|
||||
import type { ChatSetting, ChatStorage } from '../chat';
|
||||
|
||||
import Model from './model.vue';
|
||||
import SystemPrompt from './system-prompt.vue';
|
||||
import ToolUse from './tool-use.vue';
|
||||
import Prompt from './prompt.vue';
|
||||
import Resource from './resource.vue';
|
||||
import Websearch from './websearch.vue';
|
||||
import Temperature from './temperature.vue';
|
||||
import ContextLength from './context-length.vue';
|
||||
@ -70,9 +72,9 @@ provide('tabStorage', tabStorage);
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
padding: 8px 0;
|
||||
background-color: var(--sidebar);
|
||||
width: fit-content;
|
||||
border-radius: 99%;
|
||||
left: 5px;
|
||||
bottom: 0px;
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
@ -212,6 +214,7 @@ provide('tabStorage', tabStorage);
|
||||
border-radius: 50%;
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
z-index: 10;
|
||||
top: -16px;
|
||||
right: -18px;
|
||||
box-shadow: 0 0 6px rgba(0, 0, 0, 0.2);
|
@ -38,7 +38,7 @@
|
||||
import { ref, computed, inject, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
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';
|
||||
|
||||
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 */
|
||||
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 type { OpenAI } from 'openai';
|
||||
import { callTool } from "../tool/tools";
|
||||
import { callTool } from "../../tool/tools";
|
||||
import { llmManager, llms } from "@/views/setting/llm";
|
||||
import { pinkLog, redLog } from "@/views/setting/util";
|
||||
import { ElMessage } from "element-plus";
|
@ -1,4 +1,4 @@
|
||||
import { IExtraInfo } from "./chat";
|
||||
import { IExtraInfo } from "../chat-box/chat";
|
||||
|
||||
export interface UsageStatistic {
|
||||
input: number;
|
@ -40,52 +40,29 @@
|
||||
<div>
|
||||
<!-- <span class="iconfont icon-openmcp"></span> -->
|
||||
<span>{{ t('press-and-run') }}
|
||||
|
||||
<span style="padding: 5px 15px; border-radius: .5em; background-color: var(--background);">
|
||||
<span class="iconfont icon-send"></span>
|
||||
</span>
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="chat-footer" ref="footerRef">
|
||||
<div class="input-area">
|
||||
<div class="input-wrapper">
|
||||
<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>
|
||||
<ChatBox
|
||||
:ref="el => footerRef = el"
|
||||
:tab-id="props.tabId"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 { ElMessage, ScrollbarInstance } from 'element-plus';
|
||||
import { tabs } from '../panel';
|
||||
import { ChatMessage, ChatStorage, IRenderMessage, MessageState, ToolCall } from './chat';
|
||||
|
||||
import { TaskLoop } from './task-loop';
|
||||
import { llmManager, llms } from '@/views/setting/llm';
|
||||
|
||||
import { ChatMessage, ChatStorage, IRenderMessage, MessageState, ToolCall } from './chat-box/chat';
|
||||
import * as Message from './message';
|
||||
import Setting from './options/setting.vue';
|
||||
import KCuteTextarea from '@/components/k-cute-textarea/index.vue';
|
||||
import ChatBox from './chat-box/index.vue';
|
||||
|
||||
import { provide } from 'vue';
|
||||
|
||||
defineComponent({ name: 'chat' });
|
||||
|
||||
@ -101,8 +78,6 @@ const props = defineProps({
|
||||
const tab = tabs.content[props.tabId];
|
||||
const tabStorage = tab.storage as ChatStorage;
|
||||
|
||||
const userInput = ref('');
|
||||
|
||||
// 创建 messages
|
||||
if (!tabStorage.messages) {
|
||||
tabStorage.messages = [] as ChatMessage[];
|
||||
@ -158,18 +133,19 @@ const streamingToolCalls = ref<ToolCall[]>([]);
|
||||
|
||||
const chatContainerRef = ref<any>(null);
|
||||
const messageListRef = ref<any>(null);
|
||||
const footerRef = ref<any>(null);
|
||||
|
||||
const footerRef = ref<HTMLElement>();
|
||||
const scrollHeight = ref('500px');
|
||||
|
||||
const updateScrollHeight = () => {
|
||||
function updateScrollHeight() {
|
||||
if (chatContainerRef.value && footerRef.value) {
|
||||
const containerHeight = chatContainerRef.value.clientHeight;
|
||||
const footerHeight = footerRef.value.clientHeight;
|
||||
scrollHeight.value = `${containerHeight - footerHeight}px`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
provide('updateScrollHeight', updateScrollHeight);
|
||||
|
||||
const autoScroll = ref(true);
|
||||
const scrollbarRef = ref<ScrollbarInstance>();
|
||||
@ -184,8 +160,13 @@ const handleScroll = ({ scrollTop, scrollHeight, clientHeight }: {
|
||||
autoScroll.value = scrollTop + clientHeight >= scrollHeight - 10;
|
||||
};
|
||||
|
||||
provide('streamingContent', streamingContent);
|
||||
provide('streamingToolCalls', streamingToolCalls);
|
||||
provide('isLoading', isLoading);
|
||||
provide('autoScroll', autoScroll);
|
||||
|
||||
// 修改 scrollToBottom 方法
|
||||
const scrollToBottom = async () => {
|
||||
async function scrollToBottom() {
|
||||
if (!scrollbarRef.value || !messageListRef.value) return;
|
||||
|
||||
await nextTick(); // 等待 DOM 更新
|
||||
@ -198,7 +179,9 @@ const scrollToBottom = async () => {
|
||||
} catch (error) {
|
||||
console.error('Scroll to bottom failed:', error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
provide('scrollToBottom', scrollToBottom);
|
||||
|
||||
// 添加对 streamingContent 的监听
|
||||
watch(streamingContent, () => {
|
||||
@ -213,78 +196,6 @@ watch(streamingToolCalls, () => {
|
||||
}
|
||||
}, { 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>
|
||||
|
||||
@ -387,67 +298,6 @@ onUnmounted(() => {
|
||||
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 h3,
|
||||
.message-text ol,
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps } from 'vue';
|
||||
import { markdownToHtml } from '../markdown/markdown';
|
||||
import { markdownToHtml } from '@/components/main-panel/chat/markdown/markdown';
|
||||
|
||||
import MessageMeta from './message-meta.vue';
|
||||
|
||||
|
@ -32,7 +32,7 @@
|
||||
<script setup lang="ts">
|
||||
import { defineComponent, defineProps, ref, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { makeUsageStatistic } from '../usage';
|
||||
import { makeUsageStatistic } from '@/components/main-panel/chat/core/usage';
|
||||
|
||||
defineComponent({ name: 'message-meta' });
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
||||
<script setup lang="ts">
|
||||
import { defineProps } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { markdownToHtml } from '../markdown/markdown';
|
||||
import { markdownToHtml } from '@/components/main-panel/chat/markdown/markdown';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
@ -44,7 +44,7 @@ import { getBlobUrlByFilename } from '@/hook/util';
|
||||
import { defineComponent, PropType, defineProps, ref, defineEmits } from 'vue';
|
||||
|
||||
defineComponent({ name: 'toolcall-result-item' });
|
||||
const emit = defineEmits(['update:item', 'update:ocr-done']);
|
||||
const emits = defineEmits(['update:item', 'update:ocr-done']);
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
@ -73,16 +73,20 @@ if (ocr) {
|
||||
}
|
||||
}, { once: false });
|
||||
|
||||
bridge.addCommandListener('ocr/worker/done', () => {
|
||||
bridge.addCommandListener('ocr/worker/done', data => {
|
||||
if (data.id !== workerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
progress.value = 1;
|
||||
finishProcess.value = true;
|
||||
|
||||
if (props.item._meta) {
|
||||
const { _meta, ...rest } = props.item;
|
||||
emit('update:item', { ...rest });
|
||||
emits('update:item', { ...rest });
|
||||
}
|
||||
|
||||
emit('update:ocr-done');
|
||||
emits('update:ocr-done');
|
||||
|
||||
cancel();
|
||||
}, { once: true });
|
||||
|
@ -96,9 +96,9 @@
|
||||
import { defineProps, ref, watch, PropType, computed, defineEmits } from '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 { IRenderMessage, MessageState } from '../chat';
|
||||
import { IRenderMessage, MessageState } from '../chat-box/chat';
|
||||
import { ToolCallContent } from '@/hook/type';
|
||||
|
||||
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>
|
||||
<el-scrollbar height="350px">
|
||||
<el-scrollbar>
|
||||
<div
|
||||
class="output-content"
|
||||
contenteditable="false"
|
||||
|
@ -4,16 +4,14 @@
|
||||
</div>
|
||||
<div class="resource-reader-container">
|
||||
<el-form :model="tabStorage.formData" :rules="formRules" ref="formRef" label-position="top">
|
||||
<el-form-item v-for="param in currentResource?.params" :key="param.name"
|
||||
:label="param.name" :prop="param.name">
|
||||
<el-form-item v-for="param in currentResource?.params" :key="param.name" :label="param.name"
|
||||
:prop="param.name">
|
||||
<!-- 根据不同类型渲染不同输入组件 -->
|
||||
<el-input v-if="param.type === 'string'" v-model="tabStorage.formData[param.name]"
|
||||
:placeholder="param.placeholder || `请输入${param.name}`"
|
||||
@keydown.enter.prevent="handleSubmit" />
|
||||
:placeholder="param.placeholder || `请输入${param.name}`" @keydown.enter.prevent="handleSubmit" />
|
||||
|
||||
<el-input-number v-else-if="param.type === 'number'" v-model="tabStorage.formData[param.name]"
|
||||
:placeholder="param.placeholder || `请输入${param.name}`"
|
||||
@keydown.enter.prevent="handleSubmit" />
|
||||
:placeholder="param.placeholder || `请输入${param.name}`" @keydown.enter.prevent="handleSubmit" />
|
||||
|
||||
<el-switch v-else-if="param.type === 'boolean'" v-model="tabStorage.formData[param.name]" />
|
||||
</el-form-item>
|
||||
@ -31,7 +29,7 @@
|
||||
</template>
|
||||
|
||||
<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 type { FormInstance, FormRules } from 'element-plus';
|
||||
import { tabs } from '../panel';
|
||||
@ -48,11 +46,28 @@ const props = defineProps({
|
||||
tabId: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
currentResourceName: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
});
|
||||
|
||||
const tab = tabs.content[props.tabId];
|
||||
const tabStorage = tab.storage as ResourceStorage;
|
||||
const emits = defineEmits(['resource-get-response']);
|
||||
|
||||
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) {
|
||||
tabStorage.formData = {};
|
||||
@ -71,7 +86,7 @@ const currentResource = computed(() => {
|
||||
const viewParams = params.map(param => ({
|
||||
name: param,
|
||||
type: 'string',
|
||||
placeholder: t('enter') +' ' + param,
|
||||
placeholder: t('enter') + ' ' + param,
|
||||
required: true
|
||||
}));
|
||||
|
||||
@ -97,9 +112,6 @@ const formRules = computed<FormRules>(() => {
|
||||
return rules;
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
// 初始化表单数据
|
||||
const initFormData = () => {
|
||||
if (!currentResource.value?.params) return;
|
||||
@ -110,7 +122,7 @@ const initFormData = () => {
|
||||
newSchemaDataForm[param.name] = getDefaultValue(param);
|
||||
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];
|
||||
}
|
||||
})
|
||||
@ -129,28 +141,31 @@ function getUri() {
|
||||
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;
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
const uri = getUri();
|
||||
console.log(uri);
|
||||
|
||||
const bridge = useMessageBridge();
|
||||
const { code, msg } = await bridge.commandRequest('resources/read', { resourceUri: uri });
|
||||
|
||||
if (code === 200) {
|
||||
tabStorage.lastResourceReadResponse = msg;
|
||||
}
|
||||
tabStorage.lastResourceReadResponse = msg;
|
||||
|
||||
emits('resource-get-response', msg);
|
||||
}
|
||||
|
||||
// 监听资源变化重置表单
|
||||
watch(() => tabStorage.currentResourceName, () => {
|
||||
initFormData();
|
||||
resetForm();
|
||||
}, { immediate: true });
|
||||
if (props.tabId >= 0) {
|
||||
watch(() => tabStorage.currentResourceName, () => {
|
||||
initFormData();
|
||||
resetForm();
|
||||
}, { immediate: true });
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
@ -12,7 +12,7 @@
|
||||
<div class="resource-template-container">
|
||||
<div
|
||||
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"
|
||||
:key="template.name"
|
||||
@click="handleClick(template)"
|
||||
@ -31,7 +31,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useMessageBridge } from '@/api/message-bridge';
|
||||
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 { resourcesManager, ResourceStorage } from './resources';
|
||||
import { tabs } from '../panel';
|
||||
@ -47,8 +47,19 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
const tab = tabs.content[props.tabId];
|
||||
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:'template',
|
||||
currentResourceName: '',
|
||||
formData: {},
|
||||
lastResourceReadResponse: undefined
|
||||
});
|
||||
}
|
||||
|
||||
function reloadResources(option: { first: boolean }) {
|
||||
bridge.postMessage({
|
||||
|
@ -12,7 +12,7 @@
|
||||
<div class="resource-template-container">
|
||||
<div
|
||||
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"
|
||||
:key="resource.uri"
|
||||
@click="handleClick(resource)"
|
||||
@ -28,7 +28,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useMessageBridge } from '@/api/message-bridge';
|
||||
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 { resourcesManager, ResourceStorage } from './resources';
|
||||
import { tabs } from '../panel';
|
||||
@ -44,8 +44,21 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
const tab = tabs.content[props.tabId];
|
||||
const tabStorage = tab.storage as ResourceStorage;
|
||||
const emits = defineEmits([ 'resource-selected' ]);
|
||||
|
||||
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 }) {
|
||||
bridge.postMessage({
|
||||
@ -66,6 +79,8 @@ function handleClick(resource: Resources) {
|
||||
tabStorage.currentType = 'resource';
|
||||
tabStorage.currentResourceName = resource.name;
|
||||
tabStorage.lastResourceReadResponse = undefined;
|
||||
|
||||
emits('resource-selected', resource);
|
||||
}
|
||||
|
||||
let commandCancel: (() => void);
|
||||
|
@ -13,7 +13,7 @@
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
<el-scrollbar height="350px">
|
||||
<el-scrollbar>
|
||||
<div
|
||||
class="output-content"
|
||||
contenteditable="false"
|
||||
@ -104,7 +104,7 @@ const formattedJson = computed(() => {
|
||||
.resource-logger .output-content {
|
||||
border-radius: .5em;
|
||||
padding: 15px;
|
||||
min-height: 300px;
|
||||
min-height: 600px;
|
||||
height: fit-content;
|
||||
font-family: var(--code-font-family);
|
||||
white-space: pre-wrap;
|
||||
|
@ -167,9 +167,6 @@ watch(() => tabStorage.currentToolName, () => {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.tool-executor-container {
|
||||
|
||||
}
|
||||
|
||||
.tool-executor-container .el-switch .el-switch__action {
|
||||
background-color: var(--main-color);
|
||||
|
@ -149,7 +149,9 @@ function toggleConnectionPanel() {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: wrap;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
background-color: #f39a6d;
|
||||
padding: 5px 12px;
|
||||
border-radius: .5em;
|
||||
|
@ -65,6 +65,9 @@ export interface Resources {
|
||||
uri: string;
|
||||
name: string;
|
||||
mimeType: string;
|
||||
text?: string;
|
||||
blob?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ResourceTemplatesListResponse {
|
||||
|
@ -70,6 +70,12 @@ export async function getBlobUrlByFilename(filename: string) {
|
||||
}
|
||||
|
||||
export function getImageBlobUrlByBase64(base64String: string, mimeType: string, cacheKey?: string) {
|
||||
|
||||
// 检查缓存中是否存在该文件
|
||||
if (cacheKey && blobUrlCache.has(cacheKey)) {
|
||||
return blobUrlCache.get(cacheKey);
|
||||
}
|
||||
|
||||
const byteCharacters = atob(base64String);
|
||||
const byteNumbers = new Array(byteCharacters.length);
|
||||
for (let i = 0; i < byteCharacters.length; i++) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ChatStorage } from '@/components/main-panel/chat/chat';
|
||||
import { TaskLoop } from '@/components/main-panel/chat/task-loop';
|
||||
import { ChatStorage } from '@/components/main-panel/chat/chat-box/chat';
|
||||
import { TaskLoop } from '@/components/main-panel/chat/core/task-loop';
|
||||
import { llmManager } from './llm';
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { markRaw, reactive } from 'vue';
|
||||
import { createTab, debugModes, tabs } from '@/components/main-panel/panel';
|
||||
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';
|
||||
const { t } = I18n.global;
|
||||
|
@ -9,6 +9,16 @@ mcp = FastMCP('锦恢的 MCP Server', version="11.45.14")
|
||||
def add(a: int, b: int) -> int:
|
||||
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(
|
||||
uri="greeting://{name}",
|
||||
name='greeting',
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Controller, RequestClientType } from "../common";
|
||||
import { PostMessageble } from "../hook/adapter";
|
||||
import { diskStorage } from "../hook/db";
|
||||
import { createOcrWorker, saveBase64ImageData } from "./ocr.service";
|
||||
|
||||
export class OcrController {
|
||||
@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,
|
||||
"tabs": [
|
||||
{
|
||||
"name": "交互测试",
|
||||
"icon": "icon-robot",
|
||||
"name": "资源",
|
||||
"icon": "icon-file",
|
||||
"type": "blank",
|
||||
"componentIndex": 3,
|
||||
"componentIndex": 0,
|
||||
"storage": {
|
||||
"messages": [],
|
||||
"settings": {
|
||||
"modelIndex": 0,
|
||||
"enableTools": [
|
||||
"formData": {},
|
||||
"currentType": "resource",
|
||||
"currentResourceName": "network_log",
|
||||
"lastResourceReadResponse": {
|
||||
"contents": [
|
||||
{
|
||||
"name": "add",
|
||||
"description": "对两个数字进行实数域的加法",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"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
|
||||
"uri": "network://log",
|
||||
"mimeType": "text/plain",
|
||||
"text": "Response from ..."
|
||||
}
|
||||
],
|
||||
"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