优化页面布局

This commit is contained in:
锦恢 2025-04-24 19:03:08 +08:00
parent f484688a4b
commit 8887da8ba9
7 changed files with 132 additions and 278 deletions

View File

@ -10,84 +10,17 @@
<!-- 用户输入的部分 --> <!-- 用户输入的部分 -->
<div class="message-content" v-if="message.role === 'user'"> <div class="message-content" v-if="message.role === 'user'">
<div class="message-role"></div> <Message.User :message="message" />
<div class="message-text">
<span>{{ message.content }}</span>
</div>
</div> </div>
<!-- 助手返回的内容部分 --> <!-- 助手返回的内容部分 -->
<div class="message-content" v-else-if="message.role === 'assistant/content'"> <div class="message-content" v-else-if="message.role === 'assistant/content'">
<div class="message-role">Agent</div> <Message.Assistant :message="message" />
<div class="message-text">
<div v-if="message.content" v-html="markdownToHtml(message.content)"></div>
</div>
<MessageMeta :message="message" />
</div> </div>
<!-- 助手调用的工具部分 --> <!-- 助手调用的工具部分 -->
<div class="message-content" v-else-if="message.role === 'assistant/tool_calls'"> <div class="message-content" v-else-if="message.role === 'assistant/tool_calls'">
<div class="message-role"> <Message.Toolcall :message="message" />
Agent
<span class="message-reminder" v-if="!message.toolResult">
正在使用工具
<span class="tool-loading iconfont icon-double-loading">
</span>
</span>
</div>
<div class="message-text tool_calls">
<div v-if="message.content" v-html="markdownToHtml(message.content)"></div>
<div class="tool-calls">
<div v-for="(call, index) in message.tool_calls" :key="index" class="tool-call-item">
<div class="tool-call-header">
<span class="tool-name">{{ call.function.name }}</span>
<span class="tool-type">{{ 'tool' }}</span>
<el-button @click="createTest(call)">
<span class="iconfont icon-send"></span>
</el-button>
</div>
<div class="tool-arguments">
<div class="inner">
<div v-html="jsonResultToHtml(call.function.arguments)"></div>
</div>
</div>
</div>
</div>
<!-- 工具调用结果 -->
<div v-if="message.toolResult">
<div class="tool-call-header">
<span class="tool-name">{{ "响应" }}</span>
<span style="width: 200px;" class="tools-dialog-container">
<el-switch
v-model="message.showJson!.value"
inline-prompt
active-text="JSON"
inactive-text="Text"
style="margin-left: 10px; width: 200px;"
:inactive-action-style="'backgroundColor: var(--sidebar)'"
/>
</span>
</div>
<div class="tool-result" v-if="isValidJSON(message.toolResult)">
<div v-if="message.showJson!.value" class="tool-result-content">
<div class="inner">
<div v-html="jsonResultToHtml(message.toolResult)"></div>
</div>
</div>
<span v-else>
<div v-for="(item, index) in JSON.parse(message.toolResult)" :key="index">
<el-scrollbar width="100%">
<div v-if="item.type === 'text'" class="tool-text">{{ item.text }}</div>
<div v-else class="tool-other">{{ JSON.stringify(item) }}</div>
</el-scrollbar>
</div>
</span>
</div>
</div>
</div>
<MessageMeta :message="message" />
</div> </div>
</div> </div>
@ -125,7 +58,7 @@
</div> </div>
</div> </div>
<el-footer class="chat-footer" ref="footerRef"> <footer class="chat-footer" ref="footerRef">
<div class="input-area"> <div class="input-area">
<div class="input-wrapper"> <div class="input-wrapper">
<Setting :tabId="tabId" /> <Setting :tabId="tabId" />
@ -139,7 +72,7 @@
</el-button> </el-button>
</div> </div>
</div> </div>
</el-footer> </footer>
</div> </div>
</template> </template>
@ -148,16 +81,16 @@ import { ref, onMounted, defineComponent, defineProps, onUnmounted, computed, ne
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, getToolSchema, IExtraInfo, ToolCall } from './chat'; import { ChatMessage, ChatStorage, IExtraInfo, ToolCall } from './chat';
import Setting from './setting.vue'; import Setting from './setting.vue';
import MessageMeta from './message-meta.vue';
// markdown.ts // markdown.ts
import { markdownToHtml, copyToClipboard } from './markdown'; import { markdownToHtml } from './markdown';
import { ChatCompletionChunk, TaskLoop } from './task-loop'; import { TaskLoop } from './task-loop';
import { createTest, llmManager, llms } from '@/views/setting/llm'; import { llmManager, llms } from '@/views/setting/llm';
import * as Message from './message';
defineComponent({ name: 'chat' }); defineComponent({ name: 'chat' });
@ -377,26 +310,9 @@ onUnmounted(() => {
window.removeEventListener('resize', updateScrollHeight); window.removeEventListener('resize', updateScrollHeight);
}); });
// JSON
const isValidJSON = (str: string) => {
try {
JSON.parse(str);
return true;
} catch {
return false;
}
};
const jsonResultToHtml = (jsonString: string) => {
const formattedJson = JSON.stringify(JSON.parse(jsonString), null, 2);
const html = markdownToHtml('```json\n' + formattedJson + '\n```');
return html;
};
</script> </script>
<style scoped> <style>
.chat-container { .chat-container {
height: 100%; height: 100%;
display: flex; display: flex;
@ -499,7 +415,7 @@ const jsonResultToHtml = (jsonString: string) => {
border-top: 1px solid var(--el-border-color); border-top: 1px solid var(--el-border-color);
flex-shrink: 0; flex-shrink: 0;
position: absolute; position: absolute;
height: fit-content; height: fit-content !important;
bottom: 0; bottom: 0;
width: 100%; width: 100%;
} }
@ -554,72 +470,7 @@ const jsonResultToHtml = (jsonString: string) => {
opacity: 0; opacity: 0;
} }
} }
</style>
<style scoped>
.tool-calls {
margin-top: 10px;
}
.tool-call-item {
margin-bottom: 10px;
}
.tool-call-header {
display: flex;
align-items: center;
margin-bottom: 5px;
}
.tool-name {
font-weight: bold;
color: var(--el-color-primary);
margin-right: 8px;
margin-bottom: 0;
display: flex;
align-items: center;
height: 26px;
}
.tool-type {
font-size: 0.8em;
color: var(--el-text-color-secondary);
background-color: var(--el-fill-color-light);
padding: 2px 6px;
display: flex;
align-items: center;
border-radius: 4px;
}
.tool-arguments {
margin: 0;
padding: 8px;
background-color: var(--el-fill-color-light);
border-radius: 4px;
font-family: monospace;
font-size: 0.9em;
}
.tool-result {
padding: 8px;
background-color: var(--el-fill-color-light);
border-radius: 4px;
}
.tool-text {
white-space: pre-wrap;
line-height: 1.6;
}
.tool-other {
font-family: monospace;
font-size: 0.9em;
color: var(--el-text-color-secondary);
margin-top: 4px;
}
/* 新增样式来减小行距 */
.message-text p, .message-text p,
.message-text h3, .message-text h3,
.message-text ol, .message-text ol,
@ -627,7 +478,6 @@ const jsonResultToHtml = (jsonString: string) => {
margin-top: 0.5em; margin-top: 0.5em;
margin-bottom: 0.5em; margin-bottom: 0.5em;
line-height: 1.4; line-height: 1.4;
/* 可以根据需要调整行高 */
} }
.message-text ol li, .message-text ol li,

View File

@ -1,85 +0,0 @@
<template>
<div class="message-meta" @mouseenter="showTime = true" @mouseleave="showTime = false">
<span v-if="usageStatistic" class="message-usage">
<span>
{{ t('input-token') }} {{ usageStatistic.input }}
</span>
<span>
{{ t('output-token') }} {{ usageStatistic.output }}
</span>
<span>
{{ t('total') }} {{ usageStatistic.total }}
</span>
<span>
{{ t('cache-hit-ratio') }} {{ usageStatistic.cacheHitRatio }}%
</span>
</span>
<span v-else class="message-usage">
<span>{{ t('server-not-support-statistic') }}</span>
</span>
<span v-show="showTime" class="message-time">
{{ props.message.extraInfo.serverName }} {{ t('answer-at') }}
{{ new Date(message.extraInfo.created).toLocaleString() }}
</span>
</div>
</template>
<script setup lang="ts">
import { defineComponent, defineProps, ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { makeUsageStatistic } from './usage';
defineComponent({ name: 'message-meta' });
const { t } = useI18n();
const props = defineProps({
message: {
type: Object,
required: true
}
});
const usageStatistic = computed(() => {
return makeUsageStatistic(props.message.extraInfo);
});
console.log(props.message);
console.log(usageStatistic);
const showTime = ref(false);
</script>
<style scoped>
.message-meta {
margin-top: 8px;
font-size: 0.8em;
color: var(--el-text-color-secondary);
display: flex;
}
.message-time {
opacity: 0.7;
padding: 2px 6px 2px 0;
transition: opacity 0.3s ease;
}
.message-usage {
display: flex;
align-items: center;
}
.message-usage > span {
background-color: var(--el-fill-color-light);
padding: 2px 6px;
border-radius: 4px;
margin-right: 3px;
}
</style>

View File

@ -2,11 +2,10 @@
<div class="connection-option"> <div class="connection-option">
<span>{{ t('log') }}</span> <span>{{ t('log') }}</span>
<el-scrollbar height="100%"> <el-scrollbar height="100%">
<div <div class="output-content">
class="output-content" <div v-for="(log, index) in connectionResult.logString" :key="index" :class="log.type">
contenteditable="false" <span class="log-message">{{ log.message }}</span>
> </div>
{{ connectionResult.logString }}
</div> </div>
</el-scrollbar> </el-scrollbar>
</div> </div>
@ -34,7 +33,7 @@ const { t } = useI18n();
.connection-option .output-content { .connection-option .output-content {
border-radius: .5em; border-radius: .5em;
padding: 15px; padding: 12px 16px;
min-height: 300px; min-height: 300px;
height: fit-content; height: fit-content;
font-family: var(--code-font-family); font-family: var(--code-font-family);
@ -42,9 +41,59 @@ const { t } = useI18n();
word-break: break-all; word-break: break-all;
user-select: text; user-select: text;
cursor: text; cursor: text;
font-size: 15px; font-size: 14px;
line-height: 1.5; line-height: 1.6;
background-color: var(--sidebar); background-color: rgba(var(--sidebar), 0.3);
height: 95%; height: 95%;
} }
.output-content .info {
background-color: rgba(103, 194, 58, 0.5);
margin: 8px 0;
margin-bottom: 12px;
padding: 5px 9px;
border-radius: .5em;
}
.output-content .error {
background-color: rgba(245, 108, 108, 0.5);
margin: 8px 0;
margin-bottom: 12px;
padding: 5px 9px;
border-radius: .5em;
}
.output-content .warning {
background-color: rgba(230, 162, 60, 0.5);
margin: 8px 0;
margin-bottom: 12px;
padding: 5px 9px;
border-radius: .5em;
}
.log-icon {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 8px;
vertical-align: middle;
}
.log-icon.info {
background-color: rgba(103, 194, 58, 0.3);
}
.log-icon.error {
background-color: rgba(245, 108, 108, 0.3);
}
.log-icon.warning {
background-color: rgba(230, 162, 60, 0.3);
}
.log-message {
display: inline-block;
vertical-align: middle;
}
</style> </style>

View File

@ -3,7 +3,6 @@ import { reactive } from 'vue';
import { pinkLog } from '../setting/util'; import { pinkLog } from '../setting/util';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { ILaunchSigature } from '@/hook/type'; import { ILaunchSigature } from '@/hook/type';
import { url } from 'inspector';
export const connectionMethods = reactive({ export const connectionMethods = reactive({
current: 'STDIO', current: 'STDIO',
@ -80,17 +79,25 @@ export function doConnect() {
bridge.addCommandListener('connect', async data => { bridge.addCommandListener('connect', async data => {
const { code, msg } = data; const { code, msg } = data;
connectionResult.success = (code === 200); connectionResult.success = (code === 200);
connectionResult.logString = msg;
if (code === 200) { if (code === 200) {
const res = await getServerVersion() as { name: string, version: string }; const res = await getServerVersion() as { name: string, version: string };
connectionResult.serverInfo.name = res.name || ''; connectionResult.serverInfo.name = res.name || '';
connectionResult.serverInfo.version = res.version || ''; connectionResult.serverInfo.version = res.version || '';
connectionResult.logString.push({
type: 'info',
message: msg
});
} else { } else {
ElMessage({ ElMessage({
type: 'error', type: 'error',
message: msg message: msg
}); });
connectionResult.logString.push({
type: 'error',
message: msg
});
} }
resolve(void 0); resolve(void 0);
@ -189,9 +196,13 @@ async function launchStdio() {
bridge.addCommandListener('connect', async data => { bridge.addCommandListener('connect', async data => {
const { code, msg } = data; const { code, msg } = data;
connectionResult.success = (code === 200); connectionResult.success = (code === 200);
connectionResult.logString = msg;
if (code === 200) { if (code === 200) {
connectionResult.logString.push({
type: 'info',
message: msg
});
const res = await getServerVersion() as { name: string, version: string }; const res = await getServerVersion() as { name: string, version: string };
connectionResult.serverInfo.name = res.name || ''; connectionResult.serverInfo.name = res.name || '';
connectionResult.serverInfo.version = res.version || ''; connectionResult.serverInfo.version = res.version || '';
@ -217,6 +228,11 @@ async function launchStdio() {
}); });
} else { } else {
connectionResult.logString.push({
type: 'error',
message: msg
});
ElMessage({ ElMessage({
type: 'error', type: 'error',
message: msg message: msg
@ -257,9 +273,13 @@ async function launchSSE() {
bridge.addCommandListener('connect', async data => { bridge.addCommandListener('connect', async data => {
const { code, msg } = data; const { code, msg } = data;
connectionResult.success = (code === 200); connectionResult.success = (code === 200);
connectionResult.logString = msg;
if (code === 200) { if (code === 200) {
connectionResult.logString.push({
type: 'info',
message: msg
});
const res = await getServerVersion() as { name: string, version: string }; const res = await getServerVersion() as { name: string, version: string };
connectionResult.serverInfo.name = res.name || ''; connectionResult.serverInfo.name = res.name || '';
connectionResult.serverInfo.version = res.version || ''; connectionResult.serverInfo.version = res.version || '';
@ -279,6 +299,11 @@ async function launchSSE() {
}); });
} else { } else {
connectionResult.logString.push({
type: 'error',
message: msg
});
ElMessage({ ElMessage({
type: 'error', type: 'error',
message: msg message: msg
@ -328,9 +353,16 @@ export function doReconnect() {
console.log(); console.log();
} }
export const connectionResult = reactive({ export const connectionResult = reactive<{
success: boolean,
logString: { type: 'info' | 'error' | 'warning', message: string }[],
serverInfo: {
name: string,
version: string
}
}>({
success: false, success: false,
logString: '', logString: [],
serverInfo: { serverInfo: {
name: '', name: '',
version: '' version: ''
@ -353,3 +385,7 @@ export function getServerVersion() {
}); });
}); });
} }
export const envVarStatus = {
launched: false
};

View File

@ -44,7 +44,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent, onMounted, ref } from 'vue'; import { defineComponent, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { connectionEnv, connectionResult, EnvItem } from './connection'; import { connectionEnv, connectionResult, EnvItem, envVarStatus } from './connection';
import { useMessageBridge } from '@/api/message-bridge'; import { useMessageBridge } from '@/api/message-bridge';
defineComponent({ name: 'env-var' }); defineComponent({ name: 'env-var' });
@ -60,9 +60,18 @@ function lookupEnvVar(varNames: string[]) {
const { code, msg } = data; const { code, msg } = data;
if (code === 200) { if (code === 200) {
connectionResult.logString.push({
type: 'info',
message: '预设环境变量同步完成'
});
resolve(msg); resolve(msg);
} else { } else {
connectionResult.logString += '\n' + msg; connectionResult.logString.push({
type: 'error',
message: '预设环境变量同步失败: ' + msg
});
resolve(undefined); resolve(undefined);
} }
}, { once: true }); }, { once: true });
@ -149,7 +158,11 @@ async function handleEnvSwitch(enabled: boolean) {
onMounted(() => { onMounted(() => {
setTimeout(() => { setTimeout(() => {
if (envVarStatus.launched) {
return;
}
handleEnvSwitch(envEnabled.value); handleEnvSwitch(envEnabled.value);
envVarStatus.launched = true;
}, 200); }, 200);
}); });

View File

@ -27,7 +27,7 @@ import { useI18n } from 'vue-i18n';
const { t } = useI18n(); const { t } = useI18n();
import { connectionResult, doConnect, doReconnect, launchConnect } from './connection'; import { connectionResult, doConnect, launchConnect } from './connection';
import ConnectionMethod from './connection-method.vue'; import ConnectionMethod from './connection-method.vue';
import ConnectionArgs from './connection-args.vue'; import ConnectionArgs from './connection-args.vue';
@ -35,23 +35,14 @@ import EnvVar from './env-var.vue';
import ConnectionLog from './connection-log.vue'; import ConnectionLog from './connection-log.vue';
import { acquireVsCodeApi, useMessageBridge } from '@/api/message-bridge'; import { acquireVsCodeApi } from '@/api/message-bridge';
defineComponent({ name: 'connect' }); defineComponent({ name: 'connect' });
const bridge = useMessageBridge();
bridge.addCommandListener('connect', data => {
const { code, msg } = data;
connectionResult.success = (code === 200);
connectionResult.logString = msg;
}, { once: false });
const isLoading = ref(false); const isLoading = ref(false);
async function suitableConnect() { async function suitableConnect() {
isLoading.value = true; isLoading.value = true;
connectionResult.logString = '';
if (acquireVsCodeApi === undefined) { if (acquireVsCodeApi === undefined) {
await doConnect(); await doConnect();

View File

@ -44,7 +44,7 @@ async function connectHandler(option: MCPOptions, webview: PostMessageble) {
client = await connect(option); client = await connect(option);
const connectResult = { const connectResult = {
code: 200, code: 200,
msg: 'connect success\nHello from OpenMCP | virtual client version: 0.0.1' msg: 'Connect to OpenMCP successfully\nWelcome back, Kirigaya'
}; };
webview.postMessage({ command: 'connect', data: connectResult }); webview.postMessage({ command: 'connect', data: connectResult });
} catch (error) { } catch (error) {