feat: add KaTeX and Mermaid and computation-friendly renderer support (#4118)
* feat: add KaTeX and Mermaid support for enhanced markdown rendering in MessageList.vue closes: #3747 - Integrated @mdit/plugin-katex and katex for LaTeX rendering. - Added markstream-vue for improved markdown rendering capabilities. - Updated MessageList.vue to utilize MarkdownRender component for rendering markdown content. - Enhanced UI for dark mode compatibility across various components. - Introduced new styles for file links, reasoning blocks, and tool call cards to improve visual consistency. * refactor: replace markdown-it with markstream-vue for improved markdown rendering - Removed markdown-it and related configurations from ReadmeDialog.vue, VerticalHeader.vue, and ConversationPage.vue. - Integrated markstream-vue for enhanced markdown rendering capabilities, including support for KaTeX and Mermaid. - Updated components to utilize MarkdownRender for rendering markdown content, improving consistency and performance. * chore: remove deprecated markdown-it and marked dependencies from pnpm-lock.yaml - Cleaned up pnpm-lock.yaml by removing markdown-it and marked entries, streamlining the dependency list. - This change follows the recent integration of markstream-vue for improved markdown rendering capabilities. * chore: remove d3 dependency and update MessageList.vue for dark mode support - Removed d3 from package.json and commented out its import in LongTermMemory.vue to clean up unused dependencies. - Updated MessageList.vue to ensure consistent dark mode styling by passing the isDark prop to MarkdownRender components. * feat: add loading indicator for message retrieval in Chat and MessageList components - Introduced a loading overlay in Chat.vue and MessageList.vue to indicate when messages are being loaded. - Added a new `isLoadingMessages` prop to manage loading state and enhance user experience during message retrieval. - Updated styles to ensure the loading indicator is visually integrated with the existing UI. * feat: add provider configuration dialog to chat sidebar - Introduced a new `ProviderConfigDialog` component for managing provider settings. - Added a menu item in the `ConversationSidebar` to open the provider configuration dialog. - Updated English and Chinese localization files to include translations for the new provider configuration feature. * feat: update dashboard components and styles for improved chat experience - Replaced font in index.html to use 'Outfit' for a fresh look. - Changed icon in ConversationSidebar.vue to 'mdi-creation' for better representation. - Refactored MessageList.vue to streamline loading indicators and enhance styling consistency. - Updated localization files to change 'Provider Configuration' to 'AI Configuration' for clarity. - Introduced new styles for loading indicators and chat mode adjustments in FullLayout.vue. - Added functionality for toggling between bot and chat modes in the header. - Removed deprecated sidebar item for chat navigation. * feat: xmas easter egg * chore: remove pnpm lock file
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
<meta name="description" content="AstrBot Dashboard" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Poppins:wght@400;500;600;700&family=Roboto:wght@400;500;700&display=swap"
|
||||
href="https://fonts.googleapis.com/css2?family=Outfit&family=Poppins:wght@400;500;600;700&family=Roboto:wght@400;500;700&display=swap"
|
||||
/>
|
||||
<title>AstrBot - 仪表盘</title>
|
||||
</head>
|
||||
|
||||
@@ -14,22 +14,26 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@guolao/vue-monaco-editor": "^1.5.4",
|
||||
"@mdit/plugin-katex": "^0.24.1",
|
||||
"@tiptap/starter-kit": "2.1.7",
|
||||
"@tiptap/vue-3": "2.1.7",
|
||||
"apexcharts": "3.42.0",
|
||||
"axios": ">=1.6.2 <1.10.0 || >1.10.0 <2.0.0",
|
||||
"axios-mock-adapter": "^1.22.0",
|
||||
"chance": "1.1.11",
|
||||
"d3": "^7.9.0",
|
||||
"date-fns": "2.30.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"js-md5": "^0.8.3",
|
||||
"katex": "^0.16.27",
|
||||
"lodash": "4.17.21",
|
||||
"marked": "^15.0.7",
|
||||
"markdown-it": "^14.1.0",
|
||||
"pinyin-pro": "^3.26.0",
|
||||
"markstream-vue": "0.0.3-beta.7",
|
||||
"mermaid": "^11.12.2",
|
||||
"pinia": "2.1.6",
|
||||
"pinyin-pro": "^3.26.0",
|
||||
"remixicon": "3.5.0",
|
||||
"shiki": "^3.20.0",
|
||||
"stream-markdown": "^0.0.11",
|
||||
"stream-monaco": "^0.0.8",
|
||||
"vee-validate": "4.11.3",
|
||||
"vite-plugin-vuetify": "1.0.2",
|
||||
"vue": "3.3.4",
|
||||
@@ -44,7 +48,6 @@
|
||||
"@mdi/font": "7.2.96",
|
||||
"@rushstack/eslint-patch": "1.3.3",
|
||||
"@types/chance": "1.1.3",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/node": "^20.5.7",
|
||||
"@vitejs/plugin-vue": "4.3.3",
|
||||
"@vue/eslint-config-prettier": "8.0.0",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
@@ -34,13 +34,23 @@
|
||||
|
||||
<div class="message-list-wrapper" v-if="messages && messages.length > 0">
|
||||
<MessageList :messages="messages" :isDark="isDark"
|
||||
:isStreaming="isStreaming || isConvRunning" @openImagePreview="openImagePreview"
|
||||
:isStreaming="isStreaming || isConvRunning"
|
||||
:isLoadingMessages="isLoadingMessages"
|
||||
@openImagePreview="openImagePreview"
|
||||
@replyMessage="handleReplyMessage"
|
||||
ref="messageList" />
|
||||
<div class="message-list-fade" :class="{ 'fade-dark': isDark }"></div>
|
||||
</div>
|
||||
<div class="welcome-container fade-in" v-else>
|
||||
<div class="welcome-title">
|
||||
<div v-if="isLoadingMessages" class="loading-overlay-welcome">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
size="48"
|
||||
width="4"
|
||||
color="primary"
|
||||
></v-progress-circular>
|
||||
</div>
|
||||
<div v-else class="welcome-title">
|
||||
<span>Hello, I'm</span>
|
||||
<span class="bot-name">AstrBot ⭐</span>
|
||||
</div>
|
||||
@@ -139,6 +149,7 @@ const isMobile = ref(false);
|
||||
const mobileMenuOpen = ref(false);
|
||||
const imagePreviewDialog = ref(false);
|
||||
const previewImageUrl = ref('');
|
||||
const isLoadingMessages = ref(false);
|
||||
|
||||
// 使用 composables
|
||||
const {
|
||||
@@ -295,7 +306,14 @@ async function handleSelectConversation(sessionIds: string[]) {
|
||||
// 清除引用状态
|
||||
clearReply();
|
||||
|
||||
await getSessionMsg(sessionIds[0], router);
|
||||
// 开始加载消息
|
||||
isLoadingMessages.value = true;
|
||||
|
||||
try {
|
||||
await getSessionMsg(sessionIds[0], router);
|
||||
} finally {
|
||||
isLoadingMessages.value = false;
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
messageList.value?.scrollToBottom();
|
||||
@@ -540,6 +558,7 @@ onBeforeUnmount(() => {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
@@ -547,6 +566,12 @@ onBeforeUnmount(() => {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.loading-overlay-welcome {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bot-name {
|
||||
font-weight: 700;
|
||||
margin-left: 8px;
|
||||
|
||||
@@ -113,8 +113,19 @@
|
||||
</template>
|
||||
<v-list-item-title>{{ chatboxMode ? tm('actions.exitFullscreen') : tm('actions.fullscreen') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<!-- 提供商配置 -->
|
||||
<v-list-item class="styled-menu-item" @click="showProviderConfigDialog = true">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-creation</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ tm('actions.providerConfig') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</StyledMenu>
|
||||
</div>
|
||||
|
||||
<!-- 提供商配置对话框 -->
|
||||
<ProviderConfigDialog v-model="showProviderConfigDialog" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -124,6 +135,7 @@ import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
import type { Session } from '@/composables/useSessions';
|
||||
import LanguageSwitcher from '@/components/shared/LanguageSwitcher.vue';
|
||||
import StyledMenu from '@/components/shared/StyledMenu.vue';
|
||||
import ProviderConfigDialog from '@/components/chat/ProviderConfigDialog.vue';
|
||||
|
||||
interface Props {
|
||||
sessions: Session[];
|
||||
@@ -151,6 +163,7 @@ const { t } = useI18n();
|
||||
const { tm } = useModuleI18n('features/chat');
|
||||
|
||||
const sidebarCollapsed = ref(true);
|
||||
const showProviderConfigDialog = ref(false);
|
||||
|
||||
// 从 localStorage 读取侧边栏折叠状态
|
||||
const savedCollapsedState = localStorage.getItem('sidebarCollapsed');
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<div class="messages-container" ref="messageContainer">
|
||||
<!-- 加载指示器 -->
|
||||
<div v-if="isLoadingMessages" class="loading-overlay" :class="{ 'is-dark': isDark }">
|
||||
<v-progress-circular indeterminate size="48" width="4" color="primary"></v-progress-circular>
|
||||
</div>
|
||||
<!-- 聊天消息列表 -->
|
||||
<div class="message-list">
|
||||
<div class="message-list" :class="{ 'loading-blur': isLoadingMessages }">
|
||||
<div class="message-item fade-in" v-for="(msg, index) in messages" :key="index">
|
||||
<!-- 用户消息 -->
|
||||
<div v-if="msg.content.type == 'user'" class="user-message">
|
||||
@@ -40,13 +44,24 @@
|
||||
<div v-else-if="part.type === 'file' && part.embedded_file" class="file-attachments">
|
||||
<div class="file-attachment">
|
||||
<a v-if="part.embedded_file.url" :href="part.embedded_file.url"
|
||||
:download="part.embedded_file.filename" class="file-link">
|
||||
<v-icon size="small" class="file-icon">mdi-file-document-outline</v-icon>
|
||||
:download="part.embedded_file.filename" class="file-link"
|
||||
:class="{ 'is-dark': isDark }" :style="isDark ? {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||
color: 'var(--v-theme-secondary)'
|
||||
} : {}">
|
||||
<v-icon size="small" class="file-icon"
|
||||
:style="isDark ? { color: 'var(--v-theme-secondary)' } : {}">mdi-file-document-outline</v-icon>
|
||||
<span class="file-name">{{ part.embedded_file.filename }}</span>
|
||||
</a>
|
||||
<a v-else @click="downloadFile(part.embedded_file)"
|
||||
class="file-link file-link-download">
|
||||
<v-icon size="small" class="file-icon">mdi-file-document-outline</v-icon>
|
||||
class="file-link file-link-download" :class="{ 'is-dark': isDark }" :style="isDark ? {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||
color: 'var(--v-theme-secondary)'
|
||||
} : {}">
|
||||
<v-icon size="small" class="file-icon"
|
||||
:style="isDark ? { color: 'var(--v-theme-secondary)' } : {}">mdi-file-document-outline</v-icon>
|
||||
<span class="file-name">{{ part.embedded_file.filename }}</span>
|
||||
<v-icon v-if="downloadingFiles.has(part.embedded_file.attachment_id)"
|
||||
size="small" class="download-icon">mdi-loading mdi-spin</v-icon>
|
||||
@@ -76,16 +91,19 @@
|
||||
<template v-else>
|
||||
<!-- Reasoning Block (Collapsible) - 放在最前面 -->
|
||||
<div v-if="msg.content.reasoning && msg.content.reasoning.trim()"
|
||||
class="reasoning-container">
|
||||
<div class="reasoning-header" @click="toggleReasoning(index)">
|
||||
class="reasoning-container" :class="{ 'is-dark': isDark }"
|
||||
:style="isDark ? { backgroundColor: 'rgba(103, 58, 183, 0.08)' } : {}">
|
||||
<div class="reasoning-header" :class="{ 'is-dark': isDark }"
|
||||
@click="toggleReasoning(index)">
|
||||
<v-icon size="small" class="reasoning-icon">
|
||||
{{ isReasoningExpanded(index) ? 'mdi-chevron-down' : 'mdi-chevron-right' }}
|
||||
</v-icon>
|
||||
<span class="reasoning-label">{{ tm('reasoning.thinking') }}</span>
|
||||
</div>
|
||||
<div v-if="isReasoningExpanded(index)" class="reasoning-content">
|
||||
<div v-html="md.render(msg.content.reasoning)"
|
||||
class="markdown-content reasoning-text"></div>
|
||||
<MarkdownRender :content="msg.content.reasoning"
|
||||
class="reasoning-text markdown-content" :typewriter="false"
|
||||
:style="isDark ? { opacity: '0.85' } : {}" :is-dark="isDark" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -95,12 +113,15 @@
|
||||
<div v-if="part.type === 'tool_call' && part.tool_calls && part.tool_calls.length > 0"
|
||||
class="tool-calls-container">
|
||||
<div v-for="(toolCall, tcIndex) in part.tool_calls" :key="toolCall.id"
|
||||
class="tool-call-card">
|
||||
<div class="tool-call-header"
|
||||
class="tool-call-card" :class="{ 'is-dark': isDark }" :style="isDark ? {
|
||||
backgroundColor: 'rgba(40, 60, 100, 0.4)',
|
||||
borderColor: 'rgba(100, 140, 200, 0.4)'
|
||||
} : {}">
|
||||
<div class="tool-call-header" :class="{ 'is-dark': isDark }"
|
||||
@click="toggleToolCall(index, partIndex, tcIndex)">
|
||||
<v-icon size="small" class="tool-call-expand-icon">
|
||||
{{ isToolCallExpanded(index, partIndex, tcIndex) ?
|
||||
'mdi-chevron-down' : 'mdi-chevron-right' }}
|
||||
'mdi-chevron-down' : 'mdi-chevron-right' }}
|
||||
</v-icon>
|
||||
<v-icon size="small" class="tool-call-icon">mdi-wrench-outline</v-icon>
|
||||
<div class="tool-call-info">
|
||||
@@ -121,28 +142,36 @@
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="isToolCallExpanded(index, partIndex, tcIndex)"
|
||||
class="tool-call-details">
|
||||
class="tool-call-details" :style="isDark ? {
|
||||
borderTopColor: 'rgba(100, 140, 200, 0.3)',
|
||||
backgroundColor: 'rgba(30, 45, 70, 0.5)'
|
||||
} : {}">
|
||||
<div class="tool-call-detail-row">
|
||||
<span class="detail-label">ID:</span>
|
||||
<code class="detail-value">{{ toolCall.id }}</code>
|
||||
<code class="detail-value"
|
||||
:style="isDark ? { backgroundColor: 'transparent' } : {}">{{ toolCall.id
|
||||
}}</code>
|
||||
</div>
|
||||
<div class="tool-call-detail-row">
|
||||
<span class="detail-label">Args:</span>
|
||||
<pre
|
||||
class="detail-value detail-json">{{ JSON.stringify(toolCall.args, null, 2) }}</pre>
|
||||
<pre class="detail-value detail-json"
|
||||
:style="isDark ? { backgroundColor: 'transparent' } : {}">{{
|
||||
JSON.stringify(toolCall.args, null, 2) }}</pre>
|
||||
</div>
|
||||
<div v-if="toolCall.result" class="tool-call-detail-row">
|
||||
<span class="detail-label">Result:</span>
|
||||
<pre
|
||||
class="detail-value detail-json detail-result">{{ formatToolResult(toolCall.result) }}</pre>
|
||||
<pre class="detail-value detail-json detail-result"
|
||||
:style="isDark ? { backgroundColor: 'transparent' } : {}">{{ formatToolResult(toolCall.result) }}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Text (Markdown) -->
|
||||
<div v-else-if="part.type === 'plain' && part.text && part.text.trim()"
|
||||
v-html="md.render(part.text)" class="markdown-content"></div>
|
||||
<MarkdownRender v-else-if="part.type === 'plain' && part.text && part.text.trim()"
|
||||
:content="part.text" :typewriter="false" class="markdown-content"
|
||||
:is-dark="isDark" :monacoOptions="{ theme: isDark ? 'vs-dark' : 'vs-light' }" />
|
||||
|
||||
<!-- Image -->
|
||||
<div v-else-if="part.type === 'image' && part.embedded_url" class="embedded-images">
|
||||
@@ -164,15 +193,25 @@
|
||||
<div v-else-if="part.type === 'file' && part.embedded_file" class="embedded-files">
|
||||
<div class="embedded-file">
|
||||
<a v-if="part.embedded_file.url" :href="part.embedded_file.url"
|
||||
:download="part.embedded_file.filename" class="file-link">
|
||||
<v-icon size="small"
|
||||
class="file-icon">mdi-file-document-outline</v-icon>
|
||||
:download="part.embedded_file.filename" class="file-link"
|
||||
:class="{ 'is-dark': isDark }" :style="isDark ? {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||
color: 'var(--v-theme-secondary)'
|
||||
} : {}">
|
||||
<v-icon size="small" class="file-icon"
|
||||
:style="isDark ? { color: 'var(--v-theme-secondary)' } : {}">mdi-file-document-outline</v-icon>
|
||||
<span class="file-name">{{ part.embedded_file.filename }}</span>
|
||||
</a>
|
||||
<a v-else @click="downloadFile(part.embedded_file)"
|
||||
class="file-link file-link-download">
|
||||
<v-icon size="small"
|
||||
class="file-icon">mdi-file-document-outline</v-icon>
|
||||
class="file-link file-link-download" :class="{ 'is-dark': isDark }"
|
||||
:style="isDark ? {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||
color: 'var(--v-theme-secondary)'
|
||||
} : {}">
|
||||
<v-icon size="small" class="file-icon"
|
||||
:style="isDark ? { color: 'var(--v-theme-secondary)' } : {}">mdi-file-document-outline</v-icon>
|
||||
<span class="file-name">{{ part.embedded_file.filename }}</span>
|
||||
<v-icon v-if="downloadingFiles.has(part.embedded_file.attachment_id)"
|
||||
size="small" class="download-icon">mdi-loading mdi-spin</v-icon>
|
||||
@@ -185,33 +224,42 @@
|
||||
</div>
|
||||
<div class="message-actions" v-if="!msg.content.isLoading || index === messages.length - 1">
|
||||
<span class="message-time" v-if="msg.created_at">{{ formatMessageTime(msg.created_at)
|
||||
}}</span>
|
||||
}}</span>
|
||||
<!-- Agent Stats Menu -->
|
||||
<v-menu v-if="msg.content.agentStats" location="bottom" open-on-hover :close-on-content-click="false">
|
||||
<v-menu v-if="msg.content.agentStats" location="bottom" open-on-hover
|
||||
:close-on-content-click="false">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon v-bind="props" size="x-small" class="stats-info-icon">mdi-information-outline</v-icon>
|
||||
<v-icon v-bind="props" size="x-small"
|
||||
class="stats-info-icon">mdi-information-outline</v-icon>
|
||||
</template>
|
||||
<v-card class="stats-menu-card" variant="elevated" elevation="3">
|
||||
<v-card-text class="stats-menu-content">
|
||||
<div class="stats-menu-row">
|
||||
<span class="stats-menu-label">{{ tm('stats.inputTokens') }}</span>
|
||||
<span class="stats-menu-value">{{ getInputTokens(msg.content.agentStats.token_usage) }}</span>
|
||||
<span class="stats-menu-value">{{
|
||||
getInputTokens(msg.content.agentStats.token_usage) }}</span>
|
||||
</div>
|
||||
<div class="stats-menu-row">
|
||||
<span class="stats-menu-label">{{ tm('stats.outputTokens') }}</span>
|
||||
<span class="stats-menu-value">{{ msg.content.agentStats.token_usage.output || 0 }}</span>
|
||||
<span class="stats-menu-value">{{ msg.content.agentStats.token_usage.output
|
||||
|| 0 }}</span>
|
||||
</div>
|
||||
<div class="stats-menu-row" v-if="msg.content.agentStats.token_usage.input_cached > 0">
|
||||
<div class="stats-menu-row"
|
||||
v-if="msg.content.agentStats.token_usage.input_cached > 0">
|
||||
<span class="stats-menu-label">{{ tm('stats.cachedTokens') }}</span>
|
||||
<span class="stats-menu-value">{{ msg.content.agentStats.token_usage.input_cached }}</span>
|
||||
<span class="stats-menu-value">{{
|
||||
msg.content.agentStats.token_usage.input_cached }}</span>
|
||||
</div>
|
||||
<div class="stats-menu-row" v-if="msg.content.agentStats.time_to_first_token > 0">
|
||||
<div class="stats-menu-row"
|
||||
v-if="msg.content.agentStats.time_to_first_token > 0">
|
||||
<span class="stats-menu-label">{{ tm('stats.ttft') }}</span>
|
||||
<span class="stats-menu-value">{{ formatTTFT(msg.content.agentStats.time_to_first_token) }}</span>
|
||||
<span class="stats-menu-value">{{
|
||||
formatTTFT(msg.content.agentStats.time_to_first_token) }}</span>
|
||||
</div>
|
||||
<div class="stats-menu-row">
|
||||
<span class="stats-menu-label">{{ tm('stats.duration') }}</span>
|
||||
<span class="stats-menu-value">{{ formatAgentDuration(msg.content.agentStats) }}</span>
|
||||
<span class="stats-menu-value">{{
|
||||
formatAgentDuration(msg.content.agentStats) }}</span>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@@ -231,29 +279,20 @@
|
||||
|
||||
<script>
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import hljs from 'highlight.js';
|
||||
import { MarkdownRender, enableKatex, enableMermaid } from 'markstream-vue'
|
||||
import 'markstream-vue/index.css'
|
||||
import 'katex/dist/katex.min.css'
|
||||
import 'highlight.js/styles/github.css';
|
||||
import axios from 'axios';
|
||||
|
||||
const md = new MarkdownIt({
|
||||
html: false,
|
||||
breaks: true,
|
||||
linkify: true,
|
||||
highlight: function (code, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return hljs.highlight(code, { language: lang }).value;
|
||||
} catch (err) {
|
||||
console.error('Highlight error:', err);
|
||||
}
|
||||
}
|
||||
return hljs.highlightAuto(code).value;
|
||||
}
|
||||
});
|
||||
enableKatex();
|
||||
enableMermaid();
|
||||
|
||||
export default {
|
||||
name: 'MessageList',
|
||||
components: {
|
||||
MarkdownRender
|
||||
},
|
||||
props: {
|
||||
messages: {
|
||||
type: Array,
|
||||
@@ -266,6 +305,10 @@ export default {
|
||||
isStreaming: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isLoadingMessages: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['openImagePreview', 'replyMessage'],
|
||||
@@ -275,8 +318,7 @@ export default {
|
||||
|
||||
return {
|
||||
t,
|
||||
tm,
|
||||
md
|
||||
tm
|
||||
};
|
||||
},
|
||||
data() {
|
||||
@@ -741,6 +783,29 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.hr-node) {
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 1.25rem;
|
||||
opacity: 0.5;
|
||||
border-top-width: .3px;
|
||||
}
|
||||
|
||||
:deep(.paragraph-node) {
|
||||
margin: .5rem 0;
|
||||
line-height: 1.7;
|
||||
margin-block: 1rem;
|
||||
}
|
||||
|
||||
:deep(.list-node) {
|
||||
margin-top: .5rem;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
:deep(.mermaid-block-header) {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
|
||||
/* 基础动画 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
@@ -763,6 +828,31 @@ export default {
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.loading-overlay.is-dark {
|
||||
background-color: rgba(30, 30, 30, 0.7);
|
||||
}
|
||||
|
||||
.message-list.loading-blur {
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
@@ -770,6 +860,33 @@ export default {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: var(--v-theme-secondaryText);
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.messages-container {
|
||||
@@ -972,7 +1089,7 @@ export default {
|
||||
.bot-bubble {
|
||||
border: 1px solid var(--v-theme-border);
|
||||
color: var(--v-theme-primaryText);
|
||||
font-size: 15px;
|
||||
font-size: 16px;
|
||||
max-width: 100%;
|
||||
padding-left: 12px;
|
||||
}
|
||||
@@ -980,7 +1097,7 @@ export default {
|
||||
.user-avatar,
|
||||
.bot-avatar {
|
||||
align-self: flex-start;
|
||||
margin-top: 6px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* 附件样式 */
|
||||
@@ -1102,19 +1219,9 @@ export default {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.v-theme--dark .file-link {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
color: var(--v-theme-secondary);
|
||||
}
|
||||
|
||||
.v-theme--dark .file-link:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.v-theme--dark .file-icon {
|
||||
color: var(--v-theme-secondary);
|
||||
.file-link.is-dark:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
border-color: rgba(255, 255, 255, 0.2) !important;
|
||||
}
|
||||
|
||||
/* 动画类 */
|
||||
@@ -1132,10 +1239,6 @@ export default {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.v-theme--dark .reasoning-container {
|
||||
background-color: rgba(103, 58, 183, 0.08);
|
||||
}
|
||||
|
||||
.reasoning-header {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -1150,7 +1253,7 @@ export default {
|
||||
background-color: rgba(103, 58, 183, 0.08);
|
||||
}
|
||||
|
||||
.v-theme--dark .reasoning-header:hover {
|
||||
.reasoning-header.is-dark:hover {
|
||||
background-color: rgba(103, 58, 183, 0.15);
|
||||
}
|
||||
|
||||
@@ -1181,10 +1284,6 @@ export default {
|
||||
color: var(--v-theme-secondaryText);
|
||||
}
|
||||
|
||||
.v-theme--dark .reasoning-text {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* Tool Call Card Styles */
|
||||
.tool-calls-container {
|
||||
display: flex;
|
||||
@@ -1201,11 +1300,6 @@ export default {
|
||||
margin: 8px 0px;
|
||||
}
|
||||
|
||||
.v-theme--dark .tool-call-card {
|
||||
background-color: rgba(40, 60, 100, 0.4);
|
||||
border-color: rgba(100, 140, 200, 0.4);
|
||||
}
|
||||
|
||||
.tool-call-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1220,7 +1314,7 @@ export default {
|
||||
background-color: rgba(169, 194, 219, 0.15);
|
||||
}
|
||||
|
||||
.v-theme--dark .tool-call-header:hover {
|
||||
.tool-call-header.is-dark:hover {
|
||||
background-color: rgba(100, 150, 200, 0.2);
|
||||
}
|
||||
|
||||
@@ -1300,11 +1394,6 @@ export default {
|
||||
animation: fadeIn 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.v-theme--dark .tool-call-details {
|
||||
border-top-color: rgba(100, 140, 200, 0.3);
|
||||
background-color: rgba(30, 45, 70, 0.5);
|
||||
}
|
||||
|
||||
.tool-call-detail-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1345,278 +1434,14 @@ export default {
|
||||
max-height: 300px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.v-theme--dark .detail-value {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.v-theme--dark .detail-result {
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* Markdown内容样式 - 需要全局样式 */
|
||||
.markdown-content {
|
||||
font-family: inherit;
|
||||
max-width: 100%;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.markdown-content h1,
|
||||
.markdown-content h2,
|
||||
.markdown-content h3,
|
||||
.markdown-content h4,
|
||||
.markdown-content h5,
|
||||
.markdown-content h6 {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--v-theme-primaryText);
|
||||
}
|
||||
|
||||
.markdown-content h1 {
|
||||
font-size: 1.8em;
|
||||
border-bottom: 1px solid var(--v-theme-border);
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.markdown-content h2 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.markdown-content h3 {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.markdown-content li {
|
||||
margin-left: 16px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.markdown-content p {
|
||||
margin-top: .5rem;
|
||||
margin-bottom: .5rem;
|
||||
font-size: 15.5px;
|
||||
}
|
||||
|
||||
.markdown-content pre {
|
||||
background-color: var(--v-theme-surface);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
margin: 12px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.markdown-content hr {
|
||||
margin: 12px 0;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.markdown-content code {
|
||||
background-color: rgb(var(--v-theme-codeBg));
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.9em;
|
||||
color: var(--v-theme-code);
|
||||
}
|
||||
|
||||
/* 代码块中的code标签样式 */
|
||||
.markdown-content pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
font-family: 'Fira Code', 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 0.85em;
|
||||
color: inherit;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 自定义代码高亮样式 */
|
||||
.markdown-content pre {
|
||||
border: 1px solid var(--v-theme-border);
|
||||
background-color: rgb(var(--v-theme-preBg));
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* 确保highlight.js的样式正确应用 */
|
||||
.markdown-content pre code.hljs {
|
||||
background: transparent !important;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* 亮色主题下的代码高亮 */
|
||||
.v-theme--light .markdown-content pre {
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
|
||||
/* 暗色主题下的代码块样式 */
|
||||
.v-theme--dark .markdown-content pre {
|
||||
background-color: #0d1117 !important;
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.v-theme--dark .markdown-content pre code {
|
||||
color: #e6edf3 !important;
|
||||
}
|
||||
|
||||
/* 暗色主题下的highlight.js样式覆盖 */
|
||||
.v-theme--dark .hljs {
|
||||
background: #0d1117 !important;
|
||||
color: #e6edf3 !important;
|
||||
}
|
||||
|
||||
.v-theme--dark .hljs-keyword,
|
||||
.v-theme--dark .hljs-selector-tag,
|
||||
.v-theme--dark .hljs-built_in,
|
||||
.v-theme--dark .hljs-name,
|
||||
.v-theme--dark .hljs-tag {
|
||||
color: #ff7b72 !important;
|
||||
}
|
||||
|
||||
.v-theme--dark .hljs-string,
|
||||
.v-theme--dark .hljs-title,
|
||||
.v-theme--dark .hljs-section,
|
||||
.v-theme--dark .hljs-attribute,
|
||||
.v-theme--dark .hljs-literal,
|
||||
.v-theme--dark .hljs-template-tag,
|
||||
.v-theme--dark .hljs-template-variable,
|
||||
.v-theme--dark .hljs-type,
|
||||
.v-theme--dark .hljs-addition {
|
||||
color: #a5d6ff !important;
|
||||
}
|
||||
|
||||
.v-theme--dark .hljs-comment,
|
||||
.v-theme--dark .hljs-quote,
|
||||
.v-theme--dark .hljs-deletion,
|
||||
.v-theme--dark .hljs-meta {
|
||||
color: #8b949e !important;
|
||||
}
|
||||
|
||||
.v-theme--dark .hljs-number,
|
||||
.v-theme--dark .hljs-regexp,
|
||||
.v-theme--dark .hljs-symbol,
|
||||
.v-theme--dark .hljs-variable,
|
||||
.v-theme--dark .hljs-template-variable,
|
||||
.v-theme--dark .hljs-link,
|
||||
.v-theme--dark .hljs-selector-attr,
|
||||
.v-theme--dark .hljs-selector-pseudo {
|
||||
color: #79c0ff !important;
|
||||
}
|
||||
|
||||
.v-theme--dark .hljs-function,
|
||||
.v-theme--dark .hljs-class,
|
||||
.v-theme--dark .hljs-title.class_ {
|
||||
color: #d2a8ff !important;
|
||||
}
|
||||
|
||||
/* 复制按钮样式 */
|
||||
.copy-code-btn {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
padding: 6px;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
z-index: 10;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.copy-code-btn:hover {
|
||||
background: rgba(255, 255, 255, 1);
|
||||
color: #333;
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.copy-code-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.markdown-content pre:hover .copy-code-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.v-theme--dark .copy-code-btn {
|
||||
background: rgba(45, 45, 45, 0.9);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.v-theme--dark .copy-code-btn:hover {
|
||||
background: rgba(45, 45, 45, 1);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.markdown-content img {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: var(--v-theme-secondaryText);
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-content blockquote {
|
||||
border-left: 4px solid var(--v-theme-secondary);
|
||||
padding-left: 16px;
|
||||
color: var(--v-theme-secondaryText);
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.markdown-content table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.markdown-content th,
|
||||
.markdown-content td {
|
||||
border: 1px solid var(--v-theme-background);
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown-content th {
|
||||
background-color: var(--v-theme-containerBg);
|
||||
}
|
||||
|
||||
/* Stats Menu 样式 */
|
||||
.stats-menu-card {
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
<template>
|
||||
<v-dialog v-model="dialog" :max-width="isMobile ? undefined : '1400'" :fullscreen="isMobile" scrollable>
|
||||
<v-card class="provider-config-dialog" :class="{ 'mobile-dialog': isMobile }">
|
||||
<v-card-title class="d-flex align-center justify-space-between pa-4 pb-0">
|
||||
<div class="d-flex align-center ga-2">
|
||||
<span class="text-h2 font-weight-bold">{{ tm('title') }}</span>
|
||||
</div>
|
||||
<v-btn icon variant="text" @click="closeDialog">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-4 pt-0" :class="{ 'mobile-content': isMobile }"
|
||||
:style="isMobile ? {} : { height: 'calc(100vh - 200px); max-height: 800px;' }">
|
||||
<div :class="isMobile ? 'mobile-layout' : 'd-flex'" :style="isMobile ? {} : { height: '100%' }">
|
||||
<!-- 左侧:Provider Sources 列表 -->
|
||||
<div class="provider-sources-column" :class="{ 'mobile-sources': isMobile }"
|
||||
:style="isMobile ? {} : { width: '320px', minWidth: '320px', borderRight: '1px solid rgba(var(--v-border-color), var(--v-border-opacity))', overflowY: 'auto' }">
|
||||
<ProviderSourcesPanel :displayed-provider-sources="displayedProviderSources"
|
||||
:selected-provider-source="selectedProviderSource" :available-source-types="availableSourceTypes" :tm="tm"
|
||||
:resolve-source-icon="resolveSourceIcon" :get-source-display-name="getSourceDisplayName"
|
||||
@add-provider-source="addProviderSource" @select-provider-source="selectProviderSource"
|
||||
@delete-provider-source="deleteProviderSource" />
|
||||
</div>
|
||||
|
||||
<!-- 右侧:配置和模型 -->
|
||||
<div class="provider-config-column" :class="{ 'mobile-config': isMobile }"
|
||||
:style="isMobile ? {} : { flex: 1, overflowY: 'auto', minWidth: 0 }">
|
||||
<div v-if="selectedProviderSource" class="pa-4">
|
||||
<!-- Provider Source 配置 -->
|
||||
<div class="mb-4">
|
||||
<div class="d-flex align-center justify-space-between mb-3">
|
||||
<div>
|
||||
<div class="text-h5 font-weight-bold">{{ selectedProviderSource.id }}</div>
|
||||
<div class="text-caption text-medium-emphasis">{{ selectedProviderSource.api_base || 'N/A' }}</div>
|
||||
</div>
|
||||
<v-btn color="success" prepend-icon="mdi-check" :loading="savingSource" :disabled="!isSourceModified"
|
||||
@click="saveProviderSource" variant="flat">
|
||||
{{ tm('providerSources.save') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- 基础配置 -->
|
||||
<div class="mb-4">
|
||||
<AstrBotConfig v-if="basicSourceConfig" :iterable="basicSourceConfig" :metadata="configSchema"
|
||||
metadataKey="provider" :is-editing="true" />
|
||||
</div>
|
||||
|
||||
<!-- 高级配置 -->
|
||||
<v-expansion-panels variant="accordion" class="mb-4">
|
||||
<v-expansion-panel elevation="0" class="border rounded-lg">
|
||||
<v-expansion-panel-title>
|
||||
<span class="font-weight-medium">{{ tm('providerSources.advancedConfig') }}</span>
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<AstrBotConfig v-if="advancedSourceConfig" :iterable="advancedSourceConfig"
|
||||
:metadata="configSchema" metadataKey="provider" :is-editing="true" />
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
|
||||
<!-- 模型配置 -->
|
||||
<ProviderModelsPanel :entries="filteredMergedModelEntries" :available-count="availableModels.length"
|
||||
v-model:model-search="modelSearch" :loading-models="loadingModels"
|
||||
:is-source-modified="isSourceModified" :supports-image-input="supportsImageInput"
|
||||
:supports-tool-call="supportsToolCall" :supports-reasoning="supportsReasoning"
|
||||
:format-context-limit="formatContextLimit" :testing-providers="testingProviders" :tm="tm"
|
||||
@fetch-models="fetchAvailableModels" @open-manual-model="openManualModelDialog"
|
||||
@open-provider-edit="openProviderEdit" @toggle-provider-enable="toggleProviderEnable"
|
||||
@test-provider="testProvider" @delete-provider="deleteProvider"
|
||||
@add-model-provider="addModelProvider" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="d-flex align-center justify-center" style="height: 100%;">
|
||||
<div class="text-center text-medium-emphasis">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-cursor-default-click</v-icon>
|
||||
<p class="mt-4 text-h6">{{ tm('providerSources.selectHint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 手动添加模型对话框 -->
|
||||
<v-dialog v-model="showManualModelDialog" max-width="400">
|
||||
<v-card :title="tm('models.manualDialogTitle')">
|
||||
<v-card-text class="py-4">
|
||||
<v-text-field v-model="manualModelId" :label="tm('models.manualDialogModelLabel')" flat variant="solo-filled"
|
||||
autofocus clearable></v-text-field>
|
||||
<v-text-field :model-value="manualProviderId" flat variant="solo-filled"
|
||||
:label="tm('models.manualDialogPreviewLabel')" persistent-hint
|
||||
:hint="tm('models.manualDialogPreviewHint')"></v-text-field>
|
||||
</v-card-text>
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="showManualModelDialog = false">取消</v-btn>
|
||||
<v-btn color="primary" @click="confirmManualModel">添加</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 已配置模型编辑对话框 -->
|
||||
<v-dialog v-model="showProviderEditDialog" width="800">
|
||||
<v-card :title="providerEditData?.id || tm('dialogs.config.editTitle')">
|
||||
<v-card-text class="py-4">
|
||||
<small style="color: gray;">不建议修改 ID,可能会导致指向该模型的相关配置(如默认模型、插件相关配置等)失效。</small>
|
||||
<AstrBotConfig v-if="providerEditData" :iterable="providerEditData" :metadata="configSchema"
|
||||
metadataKey="provider" :is-editing="true" />
|
||||
</v-card-text>
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="showProviderEditDialog = false"
|
||||
:disabled="savingProviders.includes(providerEditData?.id)">
|
||||
{{ tm('dialogs.config.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn color="primary" @click="saveEditedProvider" :loading="savingProviders.includes(providerEditData?.id)">
|
||||
{{ tm('dialogs.config.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useModuleI18n } from '@/i18n/composables'
|
||||
import AstrBotConfig from '@/components/shared/AstrBotConfig.vue'
|
||||
import ProviderModelsPanel from '@/components/provider/ProviderModelsPanel.vue'
|
||||
import ProviderSourcesPanel from '@/components/provider/ProviderSourcesPanel.vue'
|
||||
import { useProviderSources } from '@/composables/useProviderSources'
|
||||
import { getProviderIcon } from '@/utils/providerUtils'
|
||||
import axios from 'axios'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const { tm } = useModuleI18n('features/provider')
|
||||
|
||||
// 检测是否为手机端
|
||||
const isMobile = ref(false)
|
||||
|
||||
function checkMobile() {
|
||||
isMobile.value = window.innerWidth <= 768
|
||||
}
|
||||
|
||||
const dialog = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const snackbar = ref({
|
||||
show: false,
|
||||
message: '',
|
||||
color: 'success'
|
||||
})
|
||||
|
||||
function showMessage(message, color = 'success') {
|
||||
snackbar.value = { show: true, message, color }
|
||||
}
|
||||
|
||||
const {
|
||||
selectedProviderSource,
|
||||
availableModels,
|
||||
loadingModels,
|
||||
savingSource,
|
||||
testingProviders,
|
||||
isSourceModified,
|
||||
configSchema,
|
||||
manualModelId,
|
||||
modelSearch,
|
||||
availableSourceTypes,
|
||||
displayedProviderSources,
|
||||
filteredMergedModelEntries,
|
||||
basicSourceConfig,
|
||||
advancedSourceConfig,
|
||||
manualProviderId,
|
||||
resolveSourceIcon,
|
||||
getSourceDisplayName,
|
||||
supportsImageInput,
|
||||
supportsToolCall,
|
||||
supportsReasoning,
|
||||
formatContextLimit,
|
||||
selectProviderSource,
|
||||
addProviderSource,
|
||||
deleteProviderSource,
|
||||
saveProviderSource,
|
||||
fetchAvailableModels,
|
||||
addModelProvider,
|
||||
deleteProvider,
|
||||
testProvider,
|
||||
loadConfig,
|
||||
modelAlreadyConfigured,
|
||||
} = useProviderSources({
|
||||
defaultTab: 'chat_completion',
|
||||
tm,
|
||||
showMessage
|
||||
})
|
||||
|
||||
const showManualModelDialog = ref(false)
|
||||
const showProviderEditDialog = ref(false)
|
||||
const providerEditData = ref(null)
|
||||
const providerEditOriginalId = ref('')
|
||||
const savingProviders = ref([])
|
||||
|
||||
function closeDialog() {
|
||||
dialog.value = false
|
||||
}
|
||||
|
||||
function openManualModelDialog() {
|
||||
if (!selectedProviderSource.value) {
|
||||
showMessage(tm('providerSources.selectHint'), 'error')
|
||||
return
|
||||
}
|
||||
manualModelId.value = ''
|
||||
showManualModelDialog.value = true
|
||||
}
|
||||
|
||||
async function confirmManualModel() {
|
||||
const modelId = manualModelId.value.trim()
|
||||
if (!selectedProviderSource.value) {
|
||||
showMessage(tm('providerSources.selectHint'), 'error')
|
||||
return
|
||||
}
|
||||
if (!modelId) {
|
||||
showMessage(tm('models.manualModelRequired'), 'error')
|
||||
return
|
||||
}
|
||||
if (modelAlreadyConfigured(modelId)) {
|
||||
showMessage(tm('models.manualModelExists'), 'error')
|
||||
return
|
||||
}
|
||||
await addModelProvider(modelId)
|
||||
showManualModelDialog.value = false
|
||||
}
|
||||
|
||||
function openProviderEdit(provider) {
|
||||
providerEditData.value = JSON.parse(JSON.stringify(provider))
|
||||
providerEditOriginalId.value = provider.id
|
||||
showProviderEditDialog.value = true
|
||||
}
|
||||
|
||||
async function saveEditedProvider() {
|
||||
if (!providerEditData.value) return
|
||||
|
||||
savingProviders.value.push(providerEditData.value.id)
|
||||
try {
|
||||
const res = await axios.post('/api/config/provider/update', {
|
||||
id: providerEditOriginalId.value || providerEditData.value.id,
|
||||
config: providerEditData.value
|
||||
})
|
||||
|
||||
if (res.data.status === 'error') {
|
||||
throw new Error(res.data.message)
|
||||
}
|
||||
|
||||
showMessage(res.data.message || tm('providerSources.saveSuccess'))
|
||||
showProviderEditDialog.value = false
|
||||
await loadConfig()
|
||||
} catch (err) {
|
||||
showMessage(err.response?.data?.message || err.message || tm('providerSources.saveError'), 'error')
|
||||
} finally {
|
||||
savingProviders.value = savingProviders.value.filter(id => id !== providerEditData.value?.id)
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleProviderEnable(provider, value) {
|
||||
provider.enable = value
|
||||
|
||||
try {
|
||||
const res = await axios.post('/api/config/provider/update', {
|
||||
id: provider.id,
|
||||
config: provider
|
||||
})
|
||||
|
||||
if (res.data.status === 'error') {
|
||||
throw new Error(res.data.message)
|
||||
}
|
||||
showMessage(res.data.message || tm('messages.success.statusUpdate'))
|
||||
} catch (error) {
|
||||
showMessage(error.response?.data?.message || error.message || tm('providerSources.saveError'), 'error')
|
||||
} finally {
|
||||
await loadConfig()
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 dialog 打开,加载配置
|
||||
watch(dialog, (newVal) => {
|
||||
if (newVal) {
|
||||
loadConfig()
|
||||
checkMobile()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
checkMobile()
|
||||
window.addEventListener('resize', checkMobile)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', checkMobile)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.provider-config-dialog {
|
||||
height: calc(100vh - 100px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.provider-config-dialog.mobile-dialog {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.provider-sources-column {
|
||||
overflow-y: auto;
|
||||
background-color: var(--v-theme-surface);
|
||||
}
|
||||
|
||||
.provider-config-column {
|
||||
background-color: var(--v-theme-background);
|
||||
}
|
||||
|
||||
.border {
|
||||
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
|
||||
/* 手机端样式 */
|
||||
.mobile-content {
|
||||
padding: 8px !important;
|
||||
padding-top: 0 !important;
|
||||
height: calc(100vh - 64px) !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.mobile-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.mobile-sources {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
border-right: none !important;
|
||||
border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
max-height: 40vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mobile-config {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-width: 100% !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.provider-config-dialog :deep(.v-card-title) {
|
||||
padding: 12px 16px !important;
|
||||
}
|
||||
|
||||
.provider-config-dialog :deep(.v-card-title .text-h2) {
|
||||
font-size: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,11 +1,15 @@
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, computed } from 'vue';
|
||||
import axios from 'axios';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import hljs from 'highlight.js';
|
||||
import { MarkdownRender, enableKatex, enableMermaid } from 'markstream-vue';
|
||||
import 'markstream-vue/index.css';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import 'highlight.js/styles/github.css';
|
||||
import { useI18n } from '@/i18n/composables';
|
||||
|
||||
enableKatex();
|
||||
enableMermaid();
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
@@ -74,29 +78,6 @@ function openRepoInNewTab() {
|
||||
}
|
||||
}
|
||||
|
||||
// 配置markdown-it,启用代码高亮
|
||||
const md = new MarkdownIt({
|
||||
html: true, // 启用HTML标签
|
||||
breaks: true, // 换行转<br>
|
||||
linkify: true, // 自动转链接
|
||||
typographer: false, // 禁用智能引号
|
||||
highlight: function(code, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return hljs.highlight(code, { language: lang }).value;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
return hljs.highlightAuto(code).value;
|
||||
}
|
||||
});
|
||||
|
||||
// 渲染Markdown内容
|
||||
function renderMarkdown(content) {
|
||||
if (!content) return '';
|
||||
return md.render(content);
|
||||
}
|
||||
|
||||
// 刷新README内容
|
||||
function refreshReadme() {
|
||||
@@ -150,7 +131,9 @@ const _show = computed({
|
||||
</div>
|
||||
|
||||
<!-- 内容显示 -->
|
||||
<div v-else-if="content" class="markdown-body" v-html="renderMarkdown(content)"></div>
|
||||
<div v-else-if="content" class="markdown-body">
|
||||
<MarkdownRender :content="content" :typewriter="false" class="markdown-content" />
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<div v-else-if="error" class="d-flex flex-column align-center justify-center" style="height: 100%;">
|
||||
@@ -301,6 +284,9 @@ const _show = computed({
|
||||
<script>
|
||||
export default {
|
||||
name: 'ReadmeDialog',
|
||||
components: {
|
||||
MarkdownRender
|
||||
},
|
||||
computed: {
|
||||
_show: {
|
||||
get() {
|
||||
|
||||
@@ -41,7 +41,8 @@
|
||||
"editTitle": "Edit Title",
|
||||
"fullscreen": "Fullscreen Mode",
|
||||
"exitFullscreen": "Exit Fullscreen",
|
||||
"reply": "Reply"
|
||||
"reply": "Reply",
|
||||
"providerConfig": "AI Configuration"
|
||||
},
|
||||
"conversation": {
|
||||
"newConversation": "New Conversation",
|
||||
|
||||
@@ -41,7 +41,8 @@
|
||||
"editTitle": "编辑标题",
|
||||
"fullscreen": "全屏模式",
|
||||
"exitFullscreen": "退出全屏",
|
||||
"reply": "引用回复"
|
||||
"reply": "引用回复",
|
||||
"providerConfig": "AI 配置"
|
||||
},
|
||||
"conversation": {
|
||||
"newConversation": "新的聊天",
|
||||
|
||||
@@ -5,6 +5,7 @@ import axios from 'axios';
|
||||
import VerticalSidebarVue from './vertical-sidebar/VerticalSidebar.vue';
|
||||
import VerticalHeaderVue from './vertical-header/VerticalHeader.vue';
|
||||
import MigrationDialog from '@/components/shared/MigrationDialog.vue';
|
||||
import Chat from '@/components/chat/Chat.vue';
|
||||
import { useCustomizerStore } from '@/stores/customizer';
|
||||
|
||||
const customizer = useCustomizerStore();
|
||||
@@ -14,6 +15,17 @@ const route = useRoute();
|
||||
const isChatPage = computed(() => {
|
||||
return route.path.startsWith('/chat');
|
||||
});
|
||||
|
||||
// 计算是否显示 sidebar(仅在 bot 模式下显示)
|
||||
const showSidebar = computed(() => {
|
||||
return customizer.viewMode === 'bot';
|
||||
});
|
||||
|
||||
// 计算是否显示 chat 页面(在 chat 模式下显示)
|
||||
const showChatPage = computed(() => {
|
||||
return customizer.viewMode === 'chat';
|
||||
});
|
||||
|
||||
const migrationDialog = ref<InstanceType<typeof MigrationDialog> | null>(null);
|
||||
|
||||
// 检查是否需要迁移
|
||||
@@ -49,14 +61,25 @@ onMounted(() => {
|
||||
:class="[customizer.fontTheme, customizer.mini_sidebar ? 'mini-sidebar' : '', customizer.inputBg ? 'inputWithbg' : '']"
|
||||
>
|
||||
<VerticalHeaderVue />
|
||||
<VerticalSidebarVue />
|
||||
<v-main>
|
||||
<v-container fluid class="page-wrapper" :style="{
|
||||
height: 'calc(100% - 8px)',
|
||||
padding: isChatPage ? '0' : undefined
|
||||
}">
|
||||
<div style="height: 100%;">
|
||||
<RouterView />
|
||||
<VerticalSidebarVue v-if="showSidebar" />
|
||||
<v-main :style="{
|
||||
height: showChatPage ? 'calc(100vh - 55px)' : undefined,
|
||||
overflow: showChatPage ? 'hidden' : undefined
|
||||
}">
|
||||
<v-container
|
||||
fluid
|
||||
class="page-wrapper"
|
||||
:class="{ 'chat-mode-container': showChatPage }"
|
||||
:style="{
|
||||
height: showChatPage ? '100%' : 'calc(100% - 8px)',
|
||||
padding: (isChatPage || showChatPage) ? '0' : undefined,
|
||||
minHeight: showChatPage ? 'unset' : undefined
|
||||
}">
|
||||
<div :style="{ height: '100%', width: '100%', overflow: showChatPage ? 'hidden' : undefined }">
|
||||
<div v-if="showChatPage" style="height: 100%; width: 100%; overflow: hidden;">
|
||||
<Chat />
|
||||
</div>
|
||||
<RouterView v-else />
|
||||
</div>
|
||||
</v-container>
|
||||
</v-main>
|
||||
@@ -66,3 +89,11 @@ onMounted(() => {
|
||||
</v-app>
|
||||
</v-locale-provider>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.chat-mode-container {
|
||||
min-height: unset !important;
|
||||
height: 100% !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,22 +3,23 @@ import { ref, computed } from 'vue';
|
||||
import { useCustomizerStore } from '@/stores/customizer';
|
||||
import axios from 'axios';
|
||||
import Logo from '@/components/shared/Logo.vue';
|
||||
import LanguageSwitcher from '@/components/shared/LanguageSwitcher.vue';
|
||||
import { md5 } from 'js-md5';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useCommonStore } from '@/stores/common';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import { MarkdownRender, enableKatex, enableMermaid } from 'markstream-vue';
|
||||
import 'markstream-vue/index.css';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import 'highlight.js/styles/github.css';
|
||||
import { useI18n } from '@/i18n/composables';
|
||||
import { router } from '@/router';
|
||||
import { useTheme } from 'vuetify';
|
||||
import StyledMenu from '@/components/shared/StyledMenu.vue';
|
||||
import { useLanguageSwitcher } from '@/i18n/composables';
|
||||
import type { Locale } from '@/i18n/types';
|
||||
import AboutPage from '@/views/AboutPage.vue';
|
||||
|
||||
// 配置markdown-it,默认安全设置
|
||||
const md = new MarkdownIt({
|
||||
html: true, // 启用HTML标签
|
||||
breaks: true, // 换行转<br>
|
||||
linkify: true, // 自动转链接
|
||||
typographer: false // 禁用智能引号
|
||||
});
|
||||
enableKatex();
|
||||
enableMermaid();
|
||||
|
||||
const customizer = useCustomizerStore();
|
||||
const theme = useTheme();
|
||||
@@ -26,6 +27,7 @@ const { t } = useI18n();
|
||||
let dialog = ref(false);
|
||||
let accountWarning = ref(false)
|
||||
let updateStatusDialog = ref(false);
|
||||
let aboutDialog = ref(false);
|
||||
const username = localStorage.getItem('user');
|
||||
let password = ref('');
|
||||
let newPassword = ref('');
|
||||
@@ -250,6 +252,14 @@ function openReleaseNotesDialog(body: string, tag: string) {
|
||||
releaseNotesDialog.value = true;
|
||||
}
|
||||
|
||||
function handleLogoClick() {
|
||||
if (customizer.viewMode === 'chat') {
|
||||
aboutDialog.value = true;
|
||||
} else {
|
||||
router.push('/about');
|
||||
}
|
||||
}
|
||||
|
||||
getVersion();
|
||||
checkUpdate();
|
||||
|
||||
@@ -257,37 +267,72 @@ const commonStore = useCommonStore();
|
||||
commonStore.createEventSource(); // log
|
||||
commonStore.getStartTime();
|
||||
|
||||
// 视图模式切换
|
||||
const viewMode = computed({
|
||||
get: () => customizer.viewMode,
|
||||
set: (value: 'bot' | 'chat') => {
|
||||
customizer.SET_VIEW_MODE(value);
|
||||
}
|
||||
});
|
||||
|
||||
// Merry Christmas! 🎄
|
||||
const isChristmas = computed(() => {
|
||||
const today = new Date();
|
||||
const month = today.getMonth() + 1; // getMonth() 返回 0-11
|
||||
const day = today.getDate();
|
||||
return month === 12 && day === 25;
|
||||
});
|
||||
|
||||
// 语言切换相关
|
||||
const { languageOptions, currentLanguage, switchLanguage, locale } = useLanguageSwitcher();
|
||||
const languages = computed(() =>
|
||||
languageOptions.value.map(lang => ({
|
||||
code: lang.value,
|
||||
name: lang.label,
|
||||
flag: lang.flag
|
||||
}))
|
||||
);
|
||||
const currentLocale = computed(() => locale.value);
|
||||
const changeLanguage = async (langCode: string) => {
|
||||
await switchLanguage(langCode as Locale);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-app-bar elevation="0" height="55">
|
||||
|
||||
<v-btn v-if="useCustomizerStore().uiTheme === 'PurpleTheme'" style="margin-left: 22px;"
|
||||
<!-- 桌面端 menu 按钮 - 仅在 bot 模式下显示 -->
|
||||
<v-btn v-if="customizer.viewMode === 'bot' && useCustomizerStore().uiTheme === 'PurpleTheme'" style="margin-left: 22px;"
|
||||
class="hidden-md-and-down text-secondary" color="lightsecondary" icon rounded="sm" variant="flat"
|
||||
@click.stop="customizer.SET_MINI_SIDEBAR(!customizer.mini_sidebar)" size="small">
|
||||
<v-icon>mdi-menu</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-else
|
||||
<v-btn v-else-if="customizer.viewMode === 'bot'"
|
||||
style="margin-left: 22px; color: var(--v-theme-primaryText); background-color: var(--v-theme-secondary)"
|
||||
class="hidden-md-and-down" icon rounded="sm" variant="flat"
|
||||
@click.stop="customizer.SET_MINI_SIDEBAR(!customizer.mini_sidebar)" size="small">
|
||||
<v-icon>mdi-menu</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-if="useCustomizerStore().uiTheme === 'PurpleTheme'" class="hidden-lg-and-up ms-3" color="lightsecondary"
|
||||
<!-- 移动端 menu 按钮 - 仅在 bot 模式下显示 -->
|
||||
<v-btn v-if="customizer.viewMode === 'bot' && useCustomizerStore().uiTheme === 'PurpleTheme'" class="hidden-lg-and-up ms-3" color="lightsecondary"
|
||||
icon rounded="sm" variant="flat" @click.stop="customizer.SET_SIDEBAR_DRAWER" size="small">
|
||||
<v-icon>mdi-menu</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-else class="hidden-lg-and-up ms-3" icon rounded="sm" variant="flat"
|
||||
<v-btn v-else-if="customizer.viewMode === 'bot'" class="hidden-lg-and-up ms-3" icon rounded="sm" variant="flat"
|
||||
@click.stop="customizer.SET_SIDEBAR_DRAWER" size="small">
|
||||
<v-icon>mdi-menu</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<div class="logo-container" :class="{ 'mobile-logo': $vuetify.display.xs }" @click="router.push('/about')">
|
||||
<span class="logo-text">Astr<span class="logo-text-light">Bot</span></span>
|
||||
<div class="logo-container" :class="{ 'mobile-logo': $vuetify.display.xs, 'chat-mode-logo': customizer.viewMode === 'chat' }" @click="handleLogoClick">
|
||||
<span class="logo-text Outfit">Astr<span class="logo-text bot-text-wrapper">Bot
|
||||
<img v-if="isChristmas" src="@/assets/images/xmas-hat.png" alt="Christmas hat" class="xmas-hat" />
|
||||
</span></span>
|
||||
<span class="logo-text logo-text-light Outfit" style="color: grey;" v-if="customizer.viewMode === 'chat'">ChatUI</span>
|
||||
<span class="version-text hidden-xs">{{ botCurrVersion }}</span>
|
||||
</div>
|
||||
|
||||
<v-spacer />
|
||||
<v-spacer />
|
||||
|
||||
<!-- 版本提示信息 - 在手机上隐藏 -->
|
||||
<div class="mr-4 hidden-xs">
|
||||
@@ -298,26 +343,106 @@ commonStore.getStartTime();
|
||||
{{ t('core.header.version.dashboardHasNewVersion') }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Bot/Chat 模式切换按钮 -->
|
||||
<v-btn-toggle
|
||||
v-model="viewMode"
|
||||
mandatory
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
class="mr-4"
|
||||
color="primary"
|
||||
>
|
||||
<v-btn value="bot" size="small">
|
||||
<v-icon start>mdi-robot</v-icon>
|
||||
Bot
|
||||
</v-btn>
|
||||
<v-btn value="chat" size="small">
|
||||
<v-icon start>mdi-chat</v-icon>
|
||||
Chat
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
|
||||
<!-- 语言切换器 -->
|
||||
<LanguageSwitcher variant="header" />
|
||||
|
||||
<!-- 主题切换按钮 -->
|
||||
<v-btn size="small" @click="toggleDarkMode();" class="action-btn" color="var(--v-theme-surface)" variant="flat"
|
||||
rounded="sm" icon>
|
||||
<v-icon v-if="useCustomizerStore().uiTheme === 'PurpleThemeDark'">mdi-weather-night</v-icon>
|
||||
<v-icon v-else>mdi-white-balance-sunny</v-icon>
|
||||
</v-btn>
|
||||
<!-- 功能菜单 -->
|
||||
<StyledMenu offset="12" location="bottom end">
|
||||
<template v-slot:activator="{ props: activatorProps }">
|
||||
<v-btn
|
||||
v-bind="activatorProps"
|
||||
size="small"
|
||||
class="action-btn mr-4"
|
||||
color="var(--v-theme-surface)"
|
||||
variant="flat"
|
||||
rounded="sm"
|
||||
icon
|
||||
>
|
||||
<v-icon>mdi-dots-vertical</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<!-- 语言切换 -->
|
||||
<v-list-item
|
||||
v-for="lang in languages"
|
||||
:key="lang.code"
|
||||
:value="lang.code"
|
||||
@click="changeLanguage(lang.code)"
|
||||
:class="{ 'styled-menu-item-active': currentLocale === lang.code }"
|
||||
class="styled-menu-item"
|
||||
rounded="md"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<span class="language-flag">{{ lang.flag }}</span>
|
||||
</template>
|
||||
<v-list-item-title>{{ lang.name }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<!-- 主题切换 -->
|
||||
<v-list-item
|
||||
@click="toggleDarkMode()"
|
||||
class="styled-menu-item"
|
||||
rounded="md"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon>
|
||||
{{ useCustomizerStore().uiTheme === 'PurpleThemeDark' ? 'mdi-weather-night' : 'mdi-white-balance-sunny' }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>
|
||||
{{ useCustomizerStore().uiTheme === 'PurpleThemeDark' ? t('core.header.buttons.theme.light') : t('core.header.buttons.theme.dark') }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<!-- 更新按钮 -->
|
||||
<v-list-item
|
||||
@click="checkUpdate(); getReleases(); updateStatusDialog = true"
|
||||
class="styled-menu-item"
|
||||
rounded="md"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-arrow-up-circle</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ t('core.header.updateDialog.title') }}</v-list-item-title>
|
||||
<template v-slot:append v-if="hasNewVersion || dashboardHasNewVersion">
|
||||
<v-chip size="x-small" color="primary" variant="tonal" class="ml-2">!</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<!-- 账户按钮 -->
|
||||
<v-list-item
|
||||
@click="dialog = true"
|
||||
class="styled-menu-item"
|
||||
rounded="md"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-account</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ t('core.header.accountDialog.title') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</StyledMenu>
|
||||
|
||||
<!-- 更新对话框 -->
|
||||
<v-dialog v-model="updateStatusDialog" :width="$vuetify.display.smAndDown ? '100%' : '1200'"
|
||||
:fullscreen="$vuetify.display.xs">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn size="small" @click="checkUpdate(); getReleases();" class="action-btn"
|
||||
color="var(--v-theme-surface)" variant="flat" rounded="sm" v-bind="props" icon>
|
||||
<v-icon>mdi-arrow-up-circle</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-card-title class="mobile-card-title">
|
||||
<span class="text-h5">{{ t('core.header.updateDialog.title') }}</span>
|
||||
@@ -335,8 +460,8 @@ commonStore.getStartTime();
|
||||
</div>
|
||||
|
||||
<div v-if="releaseMessage"
|
||||
style="background-color: #646cff24; padding: 16px; border-radius: 10px; font-size: 14px; max-height: 400px; overflow-y: auto;"
|
||||
v-html="md.render(releaseMessage)" class="markdown-content">
|
||||
style="background-color: #646cff24; padding: 16px; border-radius: 10px; font-size: 14px; max-height: 400px; overflow-y: auto;">
|
||||
<MarkdownRender :content="releaseMessage" :typewriter="false" class="markdown-content" />
|
||||
</div>
|
||||
|
||||
<div class="mb-4 mt-4">
|
||||
@@ -353,7 +478,7 @@ commonStore.getStartTime();
|
||||
}}</a> {{ t('core.header.updateDialog.dockerTipContinue') }}</small>
|
||||
</div>
|
||||
|
||||
<v-alert v-if="releases.some(item => isPreRelease(item['tag_name']))" type="warning" variant="tonal"
|
||||
<v-alert v-if="releases.some((item: any) => isPreRelease(item['tag_name']))" type="warning" variant="tonal"
|
||||
border="start">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-alert-circle-outline</v-icon>
|
||||
@@ -369,7 +494,7 @@ commonStore.getStartTime();
|
||||
</v-alert>
|
||||
|
||||
<v-data-table :headers="releasesHeader" :items="releases" item-key="name" :items-per-page="8">
|
||||
<template v-slot:item.tag_name="{ item }: { item: { tag_name: string } }">
|
||||
<template v-slot:item.tag_name="{ item }: { item: any }">
|
||||
<div class="d-flex align-center">
|
||||
<span>{{ item.tag_name }}</span>
|
||||
<v-chip v-if="isPreRelease(item.tag_name)" size="x-small" color="warning" variant="tonal"
|
||||
@@ -433,8 +558,8 @@ commonStore.getStartTime();
|
||||
{{ t('core.header.updateDialog.releaseNotes.title') }}: {{ selectedReleaseTag }}
|
||||
</v-card-title>
|
||||
<v-card-text
|
||||
style="font-size: 14px; max-height: 400px; overflow-y: auto;"
|
||||
v-html="md.render(selectedReleaseNotes)" class="markdown-content">
|
||||
style="font-size: 14px; max-height: 400px; overflow-y: auto;">
|
||||
<MarkdownRender :content="selectedReleaseNotes" :typewriter="false" class="markdown-content" />
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
@@ -447,12 +572,6 @@ commonStore.getStartTime();
|
||||
|
||||
<!-- 账户对话框 -->
|
||||
<v-dialog v-model="dialog" persistent :max-width="$vuetify.display.xs ? '90%' : '500'">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn size="small" class="action-btn mr-4" color="var(--v-theme-surface)" variant="flat" rounded="sm"
|
||||
v-bind="props" icon>
|
||||
<v-icon>mdi-account</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card class="account-dialog">
|
||||
<v-card-text class="py-6">
|
||||
<div class="d-flex flex-column align-center mb-6">
|
||||
@@ -508,6 +627,16 @@ commonStore.getStartTime();
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- About 对话框 - 仅在 chat mode 下使用 -->
|
||||
<v-dialog v-model="aboutDialog"
|
||||
width="600">
|
||||
<v-card>
|
||||
<v-card-text style="overflow-y: auto;">
|
||||
<AboutPage />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-app-bar>
|
||||
</template>
|
||||
|
||||
@@ -567,6 +696,10 @@ commonStore.getStartTime();
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.chat-mode-logo {
|
||||
margin-left: 22px;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 24px;
|
||||
font-weight: 1000;
|
||||
@@ -576,15 +709,35 @@ commonStore.getStartTime();
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.bot-text-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.xmas-hat {
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
right: -14px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.version-text {
|
||||
font-size: 12px;
|
||||
color: var(--v-theme-secondaryText);
|
||||
color: gray;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.language-flag {
|
||||
font-size: 16px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* 移动端对话框标题样式 */
|
||||
.mobile-card-title {
|
||||
display: flex;
|
||||
@@ -616,5 +769,19 @@ commonStore.getStartTime();
|
||||
padding: 0 10px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* 移动端模式切换按钮样式 */
|
||||
.v-btn-toggle {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.v-btn-toggle .v-btn {
|
||||
font-size: 0.75rem;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.v-btn-toggle .v-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -43,11 +43,6 @@ const sidebarItem: menu[] = [
|
||||
icon: 'mdi-book-open-variant',
|
||||
to: '/knowledge-base',
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.chat',
|
||||
icon: 'mdi-chat',
|
||||
to: '/chat'
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.groups.more',
|
||||
icon: 'mdi-dots-horizontal',
|
||||
|
||||
@@ -21,7 +21,7 @@ html {
|
||||
.page-wrapper {
|
||||
min-height: calc(100vh - 100px);
|
||||
padding: 8px;
|
||||
border-radius: $border-radius-root;
|
||||
// border-radius: $border-radius-root;
|
||||
background: rgb(var(--v-theme-containerBg));
|
||||
}
|
||||
$sizes: (
|
||||
@@ -87,6 +87,10 @@ body {
|
||||
.Inter {
|
||||
font-family: 'Inter', sans-serif !important;
|
||||
}
|
||||
|
||||
.Outfit {
|
||||
font-family: 'Outfit', sans-serif !important;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
|
||||
@@ -9,7 +9,8 @@ export const useCustomizerStore = defineStore({
|
||||
mini_sidebar: config.mini_sidebar,
|
||||
fontTheme: "Poppins",
|
||||
uiTheme: config.uiTheme,
|
||||
inputBg: config.inputBg
|
||||
inputBg: config.inputBg,
|
||||
viewMode: (localStorage.getItem('viewMode') as 'bot' | 'chat') || 'bot' // 'bot' 或 'chat'
|
||||
}),
|
||||
|
||||
getters: {},
|
||||
@@ -27,5 +28,9 @@ export const useCustomizerStore = defineStore({
|
||||
this.uiTheme = payload;
|
||||
localStorage.setItem("uiTheme", payload);
|
||||
},
|
||||
SET_VIEW_MODE(payload: 'bot' | 'chat') {
|
||||
this.viewMode = payload;
|
||||
localStorage.setItem("viewMode", payload);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
<h1 class="font-weight-bold">{{ tm('hero.title') }}</h1>
|
||||
<p class="text-subtitle-1" style="color: var(--v-theme-secondaryText);">{{ tm('hero.subtitle') }}</p>
|
||||
<div style="margin-top: 20px; display: flex; justify-content: center;">
|
||||
<v-btn @click="open('https://github.com/AstrBotDevs/AstrBot')" color="primary" variant="tonal"
|
||||
<v-btn @click="open('https://github.com/AstrBotDevs/AstrBot')" color="primary" variant="tonal" size="small"
|
||||
prepend-icon="mdi-star">
|
||||
{{ tm('hero.starButton') }}
|
||||
</v-btn>
|
||||
<v-btn class="ml-4" @click="open('https://github.com/AstrBotDevs/AstrBot/issues')" color="secondary"
|
||||
<v-btn class="ml-4" @click="open('https://github.com/AstrBotDevs/AstrBot/issues')" color="secondary" size="small"
|
||||
variant="tonal" prepend-icon="mdi-comment-question">
|
||||
{{ tm('hero.issueButton') }}
|
||||
</v-btn>
|
||||
|
||||
@@ -329,20 +329,11 @@
|
||||
import axios from 'axios';
|
||||
import { debounce } from 'lodash';
|
||||
import { VueMonacoEditor } from '@guolao/vue-monaco-editor';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import { useCommonStore } from '@/stores/common';
|
||||
import { useCustomizerStore } from '@/stores/customizer';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
import MessageList from '@/components/chat/MessageList.vue';
|
||||
|
||||
// 配置markdown-it,默认安全设置
|
||||
const md = new MarkdownIt({
|
||||
html: false, // 禁用HTML标签(关键!)
|
||||
breaks: true, // 换行转<br>
|
||||
linkify: true, // 自动转链接
|
||||
typographer: false // 禁用智能引号(避免干扰)
|
||||
});
|
||||
|
||||
export default {
|
||||
name: 'ConversationPage',
|
||||
components: {
|
||||
|
||||
@@ -252,7 +252,7 @@
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import * as d3 from "d3"; // npm install d3
|
||||
// import * as d3 from "d3"; // npm install d3
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
|
||||
export default {
|
||||
|
||||
Reference in New Issue
Block a user