6e20ebe901
* 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
376 lines
13 KiB
Vue
376 lines
13 KiB
Vue
<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>
|