feat: update dashboard

This commit is contained in:
LIghtJUNction
2026-03-18 20:12:22 +08:00
parent e8c234f0cf
commit 1b9820af44
148 changed files with 17919 additions and 8448 deletions

View File

@@ -151,7 +151,7 @@ class AstrBotDashboard:
@self.app.route("/")
async def index():
if not self.enable_webui:
return "WebUI is disabled."
return "Buildin WebUI is disabled."
try:
return await self.app.send_static_file("index.html")
except werkzeug.exceptions.NotFound:
@@ -161,7 +161,7 @@ class AstrBotDashboard:
@self.app.errorhandler(404)
async def not_found(e):
if not self.enable_webui:
return "WebUI is disabled."
return "Buildin WebUI is disabled."
if request.path.startswith("/api/"):
return jsonify(Response().error("Not Found").to_json()), 404
try:

29
dashboard/.eslintignore Normal file
View File

@@ -0,0 +1,29 @@
# ESLint ignore file for AstrBot dashboard
# Skip dependency directories and build artifacts
node_modules/
dist/
build/
public/
coverage/
.vite/
.cache/
*.min.js
*.bundle.js
*.map
# Dashboard-specific artifacts (when lint is run from repo root using --prefix)
dashboard/dist/
dashboard/node_modules/
# Generated TypeScript declaration used by environment - can cause parser issues in some setups
env.d.ts
# Scripts and tooling files (often use newer syntax or are run in node env)
scripts/**/*.mjs
scripts/**/*.cjs
# Misc
*.log
.idea/
.vscode/

149
dashboard/.eslintrc.cjs Normal file
View File

@@ -0,0 +1,149 @@
module.exports = {
root: true,
env: {
browser: true,
node: true,
es2021: true,
},
// Use vue-eslint-parser so .vue SFCs are parsed correctly.
parser: "vue-eslint-parser",
parserOptions: {
// vue-eslint-parser will forward the script content to this parser
parser: "@typescript-eslint/parser",
ecmaVersion: "latest",
sourceType: "module",
extraFileExtensions: [".vue"],
ecmaFeatures: {
jsx: true,
},
// NOTE: Intentionally NO `project` here to avoid requiring type-aware linting.
// This keeps eslint fast and avoids the TSConfig inclusion errors.
},
plugins: ["vue", "@typescript-eslint"],
extends: [
"eslint:recommended",
"plugin:vue/vue3-recommended",
"plugin:@typescript-eslint/recommended",
// Intentionally not extending type-aware or prettier-requiring configs.
],
settings: {
// Allow using Vue compiler macros like defineProps/defineEmits in templates
"vue/setup-compiler-macros": true,
},
// Avoid linting build artifacts and generated files
ignorePatterns: [
"dist/",
"build/",
"node_modules/",
"public/",
"dashboard/dist/",
"dashboard/node_modules/",
"env.d.ts",
"scripts/**/*.mjs",
".vite/",
".cache/",
],
rules: {
// Keep console/debug permissible but warned
"no-console": ["warn", { allow: ["warn", "error", "info"] }],
"no-debugger": "warn",
// TypeScript rules (relaxed)
"@typescript-eslint/no-unused-vars": [
"warn",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "off",
// Vue rules adjustments — relax a few rules that generate a lot of noise
// These are intentionally relaxed to allow incremental, safe fixes of template code.
"vue/multi-word-component-names": "off",
"vue/html-self-closing": [
"error",
{
html: {
void: "never",
normal: "always",
component: "always",
},
svg: "always",
math: "always",
},
],
// Reduce template noise for legacy / Vuetify patterns used across this codebase
"vue/valid-v-slot": "off",
"vue/v-on-event-hyphenation": "off",
"vue/no-unused-components": "off",
// Broadly disable unused vars detection for templates to avoid false positives from compiled/generated template usage
"vue/no-unused-vars": "off",
"vue/require-default-prop": "off",
// Keep v-html as a warn so security-sensitive usage is highlighted
"vue/no-v-html": "warn",
},
overrides: [
// Vue Single File Components
{
files: ["*.vue", "src/**/*.vue"],
parser: "vue-eslint-parser",
parserOptions: {
parser: "@typescript-eslint/parser",
extraFileExtensions: [".vue"],
ecmaVersion: "latest",
sourceType: "module",
// Enable type-aware rules for script blocks inside .vue files
project: "./tsconfig.eslint.json",
tsconfigRootDir: __dirname,
},
rules: {
// Component/template specific overrides can go here
},
},
// TypeScript files (no project required)
{
files: ["*.ts", "*.tsx", "src/**/*.ts", "src/**/*.tsx"],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
// Use type-aware linting for TS files via the dedicated tsconfig for ESLint
project: "./tsconfig.eslint.json",
tsconfigRootDir: __dirname,
},
rules: {
// Project-specific relaxations for TS files
},
},
// Node scripts / tooling
{
files: ["scripts/**/*.mjs", "scripts/**/*.cjs", "*.cjs"],
env: { node: true },
parserOptions: { sourceType: "module" },
},
// Disable strict v-slot validation for extension component panels where shorthand slots are used
{
files: [
"src/components/extension/componentPanel/**",
"src/components/extension/**",
],
rules: {
"vue/valid-v-slot": "off",
},
},
],
};

View File

@@ -1,14 +1,28 @@
<template>
<RouterView></RouterView>
<RouterView />
<WaitingForRestart ref="globalWaitingRef" />
<!-- 全局唯一 snackbar -->
<v-snackbar v-if="toastStore.current" v-model="snackbarShow" :color="toastStore.current.color"
:timeout="toastStore.current.timeout" :multi-line="toastStore.current.multiLine"
:location="toastStore.current.location" close-on-back>
<v-snackbar
v-if="toastStore.current"
v-model="snackbarShow"
:color="toastStore.current.color"
:timeout="toastStore.current.timeout"
:multi-line="toastStore.current.multiLine"
:location="toastStore.current.location"
close-on-back
>
{{ toastStore.current.message }}
<template #actions v-if="toastStore.current.closable">
<v-btn variant="text" @click="snackbarShow = false">关闭</v-btn>
<template
v-if="toastStore.current.closable"
#actions
>
<v-btn
variant="text"
@click="snackbarShow = false"
>
关闭
</v-btn>
</template>
</v-snackbar>
</template>

View File

@@ -1,12 +1,27 @@
<template>
<v-dialog v-model="isOpen" max-width="400">
<v-dialog
v-model="isOpen"
max-width="400"
>
<v-card>
<v-card-title class="text-h6">{{ title }}</v-card-title>
<v-card-title class="text-h6">
{{ title }}
</v-card-title>
<v-card-text>{{ message }}</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="gray" @click="handleCancel">{{ t('core.common.dialog.cancelButton') }}</v-btn>
<v-btn color="red" @click="handleConfirm">{{ t('core.common.dialog.confirmButton') }}</v-btn>
<v-spacer />
<v-btn
color="gray"
@click="handleCancel"
>
{{ t('core.common.dialog.cancelButton') }}
</v-btn>
<v-btn
color="red"
@click="handleConfirm"
>
{{ t('core.common.dialog.confirmButton') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>

View File

@@ -1,208 +1,279 @@
<template>
<v-card class="chat-page-card" elevation="0" rounded="0">
<v-card-text class="chat-page-container">
<!-- 遮罩层 (手机端) -->
<div class="mobile-overlay" v-if="isMobile && mobileMenuOpen" @click="closeMobileSidebar"></div>
<v-card
class="chat-page-card"
elevation="0"
rounded="0"
>
<v-card-text class="chat-page-container">
<!-- 遮罩层 (手机端) -->
<div
v-if="isMobile && mobileMenuOpen"
class="mobile-overlay"
@click="closeMobileSidebar"
/>
<div class="chat-layout">
<ConversationSidebar
:sessions="sessions"
:selectedSessions="selectedSessions"
:currSessionId="currSessionId"
:selectedProjectId="selectedProjectId"
:transportMode="transportMode"
:sendShortcut="sendShortcut"
:isDark="isDark"
:chatboxMode="chatboxMode"
:isMobile="isMobile"
:mobileMenuOpen="mobileMenuOpen"
:projects="projects"
@newChat="handleNewChat"
@selectConversation="handleSelectConversation"
@editTitle="showEditTitleDialog"
@deleteConversation="handleDeleteConversation"
@batchDeleteConversations="handleBatchDeleteConversations"
@closeMobileSidebar="closeMobileSidebar"
@toggleTheme="toggleTheme"
@toggleFullscreen="toggleFullscreen"
@selectProject="handleSelectProject"
@createProject="showCreateProjectDialog"
@editProject="showEditProjectDialog"
@deleteProject="handleDeleteProject"
@updateTransportMode="setTransportMode"
@updateSendShortcut="setSendShortcut"
/>
<div class="chat-layout">
<ConversationSidebar
:sessions="sessions"
:selected-sessions="selectedSessions"
:curr-session-id="currSessionId"
:selected-project-id="selectedProjectId"
:transport-mode="transportMode"
:send-shortcut="sendShortcut"
:is-dark="isDark"
:chatbox-mode="chatboxMode"
:is-mobile="isMobile"
:mobile-menu-open="mobileMenuOpen"
:projects="projects"
@newChat="handleNewChat"
@selectConversation="handleSelectConversation"
@editTitle="showEditTitleDialog"
@deleteConversation="handleDeleteConversation"
@batchDeleteConversations="handleBatchDeleteConversations"
@closeMobileSidebar="closeMobileSidebar"
@toggleTheme="toggleTheme"
@toggleFullscreen="toggleFullscreen"
@selectProject="handleSelectProject"
@createProject="showCreateProjectDialog"
@editProject="showEditProjectDialog"
@deleteProject="handleDeleteProject"
@updateTransportMode="setTransportMode"
@updateSendShortcut="setSendShortcut"
/>
<!-- 右侧聊天内容区域 -->
<div class="chat-content-panel">
<!-- Live Mode -->
<LiveMode v-if="liveModeOpen" @close="closeLiveMode" />
<!-- 右侧聊天内容区域 -->
<div class="chat-content-panel">
<!-- Live Mode -->
<LiveMode
v-if="liveModeOpen"
@close="closeLiveMode"
/>
<!-- 正常聊天界面 -->
<template v-else>
<div v-if="currentSessionProject && messages && messages.length > 0" class="breadcrumb-container">
<div class="breadcrumb-content">
<span class="breadcrumb-emoji">{{ currentSessionProject.emoji || '📁' }}</span>
<span class="breadcrumb-project" @click="handleSelectProject(currentSessionProject.project_id)">{{ currentSessionProject.title }}</span>
<v-icon size="small" class="breadcrumb-separator">mdi-chevron-right</v-icon>
<span class="breadcrumb-session">{{ getCurrentSession?.display_name || tm('conversation.newConversation') }}</span>
</div>
</div>
<div class="message-list-wrapper" v-if="currSessionId && !selectedProjectId">
<MessageList :messages="messages" :isDark="isDark"
:isStreaming="isStreaming || isConvRunning"
:isLoadingMessages="isLoadingMessages"
@openImagePreview="openImagePreview"
@replyMessage="handleReplyMessage"
@replyWithText="handleReplyWithText"
@openRefs="handleOpenRefs"
ref="messageList" />
<div class="message-list-fade" :class="{ 'fade-dark': isDark }"></div>
</div>
<ProjectView
v-else-if="selectedProjectId"
:project="currentProject"
:sessions="projectSessions"
@selectSession="(sessionId) => handleSelectConversation([sessionId])"
@editSessionTitle="showEditTitleDialog"
@deleteSession="handleDeleteConversation"
>
<ChatInput
v-model:prompt="prompt"
:stagedImagesUrl="stagedImagesUrl"
:stagedAudioUrl="stagedAudioUrl"
:stagedFiles="stagedNonImageFiles"
:disabled="false"
:is-running="isStreaming || isConvRunning"
:enableStreaming="enableStreaming"
:isRecording="isRecording"
:session-id="currSessionId || null"
:current-session="getCurrentSession"
:replyTo="replyTo"
:send-shortcut="sendShortcut"
@send="handleSendMessage"
@stop="handleStopMessage"
@toggleStreaming="toggleStreaming"
@removeImage="removeImage"
@removeAudio="removeAudio"
@removeFile="removeFile"
@startRecording="handleStartRecording"
@stopRecording="handleStopRecording"
@pasteImage="handlePaste"
@fileSelect="handleFileSelect"
@clearReply="clearReply"
@openLiveMode="openLiveMode"
ref="chatInputRef"
/>
</ProjectView>
<WelcomeView
v-else
:isLoading="isLoadingMessages"
>
<ChatInput
v-model:prompt="prompt"
:stagedImagesUrl="stagedImagesUrl"
:stagedAudioUrl="stagedAudioUrl"
:stagedFiles="stagedNonImageFiles"
:disabled="false"
:is-running="isStreaming || isConvRunning"
:enableStreaming="enableStreaming"
:isRecording="isRecording"
:session-id="currSessionId || null"
:current-session="getCurrentSession"
:replyTo="replyTo"
:send-shortcut="sendShortcut"
@send="handleSendMessage"
@stop="handleStopMessage"
@toggleStreaming="toggleStreaming"
@removeImage="removeImage"
@removeAudio="removeAudio"
@removeFile="removeFile"
@startRecording="handleStartRecording"
@stopRecording="handleStopRecording"
@pasteImage="handlePaste"
@fileSelect="handleFileSelect"
@clearReply="clearReply"
@openLiveMode="openLiveMode"
ref="chatInputRef"
/>
</WelcomeView>
<!-- 输入区域 -->
<ChatInput
v-if="currSessionId && !selectedProjectId"
v-model:prompt="prompt"
:stagedImagesUrl="stagedImagesUrl"
:stagedAudioUrl="stagedAudioUrl"
:stagedFiles="stagedNonImageFiles"
:disabled="false"
:is-running="isStreaming || isConvRunning"
:enableStreaming="enableStreaming"
:isRecording="isRecording"
:session-id="currSessionId || null"
:current-session="getCurrentSession"
:replyTo="replyTo"
:send-shortcut="sendShortcut"
@send="handleSendMessage"
@stop="handleStopMessage"
@toggleStreaming="toggleStreaming"
@removeImage="removeImage"
@removeAudio="removeAudio"
@removeFile="removeFile"
@startRecording="handleStartRecording"
@stopRecording="handleStopRecording"
@pasteImage="handlePaste"
@fileSelect="handleFileSelect"
@clearReply="clearReply"
@openLiveMode="openLiveMode"
ref="chatInputRef"
/>
</template>
</div>
<!-- Refs Sidebar -->
<RefsSidebar v-model="refsSidebarOpen" :refs="refsSidebarRefs" />
<!-- 正常聊天界面 -->
<template v-else>
<div
v-if="currentSessionProject && messages && messages.length > 0"
class="breadcrumb-container"
>
<div class="breadcrumb-content">
<span class="breadcrumb-emoji">{{ currentSessionProject.emoji || '📁' }}</span>
<span
class="breadcrumb-project"
@click="handleSelectProject(currentSessionProject.project_id)"
>{{ currentSessionProject.title }}</span>
<v-icon
size="small"
class="breadcrumb-separator"
>
mdi-chevron-right
</v-icon>
<span class="breadcrumb-session">{{ getCurrentSession?.display_name || tm('conversation.newConversation') }}</span>
</div>
</div>
</v-card-text>
</v-card>
<div
v-if="currSessionId && !selectedProjectId"
class="message-list-wrapper"
>
<MessageList
ref="messageList"
:messages="messages"
:is-dark="isDark"
:is-streaming="isStreaming || isConvRunning"
:is-loading-messages="isLoadingMessages"
@openImagePreview="openImagePreview"
@replyMessage="handleReplyMessage"
@replyWithText="handleReplyWithText"
@openRefs="handleOpenRefs"
/>
<div
class="message-list-fade"
:class="{ 'fade-dark': isDark }"
/>
</div>
<ProjectView
v-else-if="selectedProjectId"
:project="currentProject"
:sessions="projectSessions"
@selectSession="(sessionId) => handleSelectConversation([sessionId])"
@editSessionTitle="showEditTitleDialog"
@deleteSession="handleDeleteConversation"
>
<ChatInput
ref="chatInputRef"
v-model:prompt="prompt"
:staged-images-url="stagedImagesUrl"
:staged-audio-url="stagedAudioUrl"
:staged-files="stagedNonImageFiles"
:disabled="false"
:is-running="isStreaming || isConvRunning"
:enable-streaming="enableStreaming"
:is-recording="isRecording"
:session-id="currSessionId || null"
:current-session="getCurrentSession"
:reply-to="replyTo"
:send-shortcut="sendShortcut"
@send="handleSendMessage"
@stop="handleStopMessage"
@toggleStreaming="toggleStreaming"
@removeImage="removeImage"
@removeAudio="removeAudio"
@removeFile="removeFile"
@startRecording="handleStartRecording"
@stopRecording="handleStopRecording"
@pasteImage="handlePaste"
@fileSelect="handleFileSelect"
@clearReply="clearReply"
@openLiveMode="openLiveMode"
/>
</ProjectView>
<WelcomeView
v-else
:is-loading="isLoadingMessages"
>
<ChatInput
ref="chatInputRef"
v-model:prompt="prompt"
:staged-images-url="stagedImagesUrl"
:staged-audio-url="stagedAudioUrl"
:staged-files="stagedNonImageFiles"
:disabled="false"
:is-running="isStreaming || isConvRunning"
:enable-streaming="enableStreaming"
:is-recording="isRecording"
:session-id="currSessionId || null"
:current-session="getCurrentSession"
:reply-to="replyTo"
:send-shortcut="sendShortcut"
@send="handleSendMessage"
@stop="handleStopMessage"
@toggleStreaming="toggleStreaming"
@removeImage="removeImage"
@removeAudio="removeAudio"
@removeFile="removeFile"
@startRecording="handleStartRecording"
@stopRecording="handleStopRecording"
@pasteImage="handlePaste"
@fileSelect="handleFileSelect"
@clearReply="clearReply"
@openLiveMode="openLiveMode"
/>
</WelcomeView>
<!-- 输入区域 -->
<ChatInput
v-if="currSessionId && !selectedProjectId"
ref="chatInputRef"
v-model:prompt="prompt"
:staged-images-url="stagedImagesUrl"
:staged-audio-url="stagedAudioUrl"
:staged-files="stagedNonImageFiles"
:disabled="false"
:is-running="isStreaming || isConvRunning"
:enable-streaming="enableStreaming"
:is-recording="isRecording"
:session-id="currSessionId || null"
:current-session="getCurrentSession"
:reply-to="replyTo"
:send-shortcut="sendShortcut"
@send="handleSendMessage"
@stop="handleStopMessage"
@toggleStreaming="toggleStreaming"
@removeImage="removeImage"
@removeAudio="removeAudio"
@removeFile="removeFile"
@startRecording="handleStartRecording"
@stopRecording="handleStopRecording"
@pasteImage="handlePaste"
@fileSelect="handleFileSelect"
@clearReply="clearReply"
@openLiveMode="openLiveMode"
/>
</template>
</div>
<!-- Refs Sidebar -->
<RefsSidebar
v-model="refsSidebarOpen"
:refs="refsSidebarRefs"
/>
</div>
</v-card-text>
</v-card>
<!-- 编辑对话标题对话框 -->
<v-dialog v-model="editTitleDialog" max-width="400">
<v-card>
<v-card-title class="dialog-title">{{ tm('actions.editTitle') }}</v-card-title>
<v-card-text>
<v-text-field v-model="editingTitle" :label="tm('conversation.newConversation')" variant="outlined"
hide-details class="mt-2" @keyup.enter="handleSaveTitle" autofocus />
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn variant="text" @click="editTitleDialog = false" color="grey-darken-1">{{ t('core.common.cancel') }}</v-btn>
<v-btn variant="text" @click="handleSaveTitle" color="primary">{{ t('core.common.save') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 编辑对话标题对话框 -->
<v-dialog
v-model="editTitleDialog"
max-width="400"
>
<v-card>
<v-card-title class="dialog-title">
{{ tm('actions.editTitle') }}
</v-card-title>
<v-card-text>
<v-text-field
v-model="editingTitle"
:label="tm('conversation.newConversation')"
variant="outlined"
hide-details
class="mt-2"
autofocus
@keyup.enter="handleSaveTitle"
/>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
variant="text"
color="grey-darken-1"
@click="editTitleDialog = false"
>
{{ t('core.common.cancel') }}
</v-btn>
<v-btn
variant="text"
color="primary"
@click="handleSaveTitle"
>
{{ t('core.common.save') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 图片预览对话框 -->
<v-dialog v-model="imagePreviewDialog" max-width="90vw" max-height="90vh">
<v-card class="image-preview-card" elevation="8">
<v-card-title class="d-flex justify-space-between align-center pa-4">
<span>{{ t('core.common.imagePreview') }}</span>
<v-btn icon="mdi-close" variant="text" @click="imagePreviewDialog = false" />
</v-card-title>
<v-card-text class="text-center pa-4">
<img :src="previewImageUrl" class="preview-image-large" />
</v-card-text>
</v-card>
</v-dialog>
<!-- 图片预览对话框 -->
<v-dialog
v-model="imagePreviewDialog"
max-width="90vw"
max-height="90vh"
>
<v-card
class="image-preview-card"
elevation="8"
>
<v-card-title class="d-flex justify-space-between align-center pa-4">
<span>{{ t('core.common.imagePreview') }}</span>
<v-btn
icon="mdi-close"
variant="text"
@click="imagePreviewDialog = false"
/>
</v-card-title>
<v-card-text class="text-center pa-4">
<img
:src="previewImageUrl"
class="preview-image-large"
>
</v-card-text>
</v-card>
</v-dialog>
<!-- 创建/编辑项目对话框 -->
<ProjectDialog
v-model="projectDialog"
:project="editingProject"
@save="handleSaveProject"
/>
<!-- 创建/编辑项目对话框 -->
<ProjectDialog
v-model="projectDialog"
:project="editingProject"
@save="handleSaveProject"
/>
</template>
<script setup lang="ts">

View File

@@ -1,82 +1,166 @@
<template>
<div class="input-area fade-in" @dragover.prevent="handleDragOver" @dragleave.prevent="handleDragLeave"
@drop.prevent="handleDrop">
<div class="input-container" :style="{
width: '85%',
maxWidth: '900px',
margin: '0 auto',
border: isDark ? 'none' : '1px solid #e0e0e0',
borderRadius: '24px',
boxShadow: isDark ? 'none' : '0px 2px 2px rgba(0, 0, 0, 0.1)',
backgroundColor: isDark ? '#2d2d2d' : 'transparent',
position: 'relative'
}">
<!-- 拖拽上传遮罩 -->
<transition name="fade">
<div v-if="isDragging" class="drop-overlay">
<div class="drop-overlay-content">
<v-icon size="48" color="primary">mdi-cloud-upload</v-icon>
<span class="drop-text">{{ tm('input.dropToUpload') }}</span>
</div>
</div>
</transition>
<!-- 引用预览区 -->
<transition name="slideReply" @after-leave="handleReplyAfterLeave">
<div class="reply-preview" v-if="props.replyTo && !isReplyClosing">
<div class="reply-content">
<v-icon size="small" class="reply-icon">mdi-reply</v-icon>
"<span class="reply-text">{{ props.replyTo.selectedText }}</span>"
</div>
<v-btn @click="handleClearReply" class="remove-reply-btn" icon="mdi-close" size="x-small"
color="grey" variant="text" />
</div>
</transition>
<textarea ref="inputField" v-model="localPrompt" @keydown="handleKeyDown" :disabled="disabled"
placeholder="Ask AstrBot..." class="chat-textarea"
autocomplete="off" autocorrect="off" autocapitalize="sentences" spellcheck="false"
style="width: 100%; resize: none; outline: none; border: 1px solid var(--v-theme-border); border-radius: 12px; padding: 16px 20px; min-height: 40px; max-height: 200px; overflow-y: auto; font-family: inherit; font-size: 16px; background-color: var(--v-theme-surface);"></textarea>
<div style="display: flex; justify-content: space-between; align-items: center; padding: 6px 14px;">
<div
style="display: flex; justify-content: flex-start; margin-top: 4px; align-items: center; gap: 8px; min-width: 0; flex: 1; overflow: hidden;">
<!-- Settings Menu -->
<StyledMenu offset="8" location="top start" :close-on-content-click="false">
<template v-slot:activator="{ props: activatorProps }">
<v-btn v-bind="activatorProps" icon="mdi-plus" variant="text" color="primary" />
</template>
<div
class="input-area fade-in"
@dragover.prevent="handleDragOver"
@dragleave.prevent="handleDragLeave"
@drop.prevent="handleDrop"
>
<div
class="input-container"
:style="{
width: '85%',
maxWidth: '900px',
margin: '0 auto',
border: isDark ? 'none' : '1px solid #e0e0e0',
borderRadius: '24px',
boxShadow: isDark ? 'none' : '0px 2px 2px rgba(0, 0, 0, 0.1)',
backgroundColor: isDark ? '#2d2d2d' : 'transparent',
position: 'relative'
}"
>
<!-- 拖拽上传遮罩 -->
<transition name="fade">
<div
v-if="isDragging"
class="drop-overlay"
>
<div class="drop-overlay-content">
<v-icon
size="48"
color="primary"
>
mdi-cloud-upload
</v-icon>
<span class="drop-text">{{ tm('input.dropToUpload') }}</span>
</div>
</div>
</transition>
<!-- 引用预览区 -->
<transition
name="slideReply"
@after-leave="handleReplyAfterLeave"
>
<div
v-if="props.replyTo && !isReplyClosing"
class="reply-preview"
>
<div class="reply-content">
<v-icon
size="small"
class="reply-icon"
>
mdi-reply
</v-icon>
"<span class="reply-text">{{ props.replyTo.selectedText }}</span>"
</div>
<v-btn
class="remove-reply-btn"
icon="mdi-close"
size="x-small"
color="grey"
variant="text"
@click="handleClearReply"
/>
</div>
</transition>
<textarea
ref="inputField"
v-model="localPrompt"
:disabled="disabled"
placeholder="Ask AstrBot..."
class="chat-textarea"
autocomplete="off"
autocorrect="off"
autocapitalize="sentences"
spellcheck="false"
style="width: 100%; resize: none; outline: none; border: 1px solid var(--v-theme-border); border-radius: 12px; padding: 16px 20px; min-height: 40px; max-height: 200px; overflow-y: auto; font-family: inherit; font-size: 16px; background-color: var(--v-theme-surface);"
@keydown="handleKeyDown"
/>
<div style="display: flex; justify-content: space-between; align-items: center; padding: 6px 14px;">
<div
style="display: flex; justify-content: flex-start; margin-top: 4px; align-items: center; gap: 8px; min-width: 0; flex: 1; overflow: hidden;"
>
<!-- Settings Menu -->
<StyledMenu
offset="8"
location="top start"
:close-on-content-click="false"
>
<template #activator="{ props: activatorProps }">
<v-btn
v-bind="activatorProps"
icon="mdi-plus"
variant="text"
color="primary"
/>
</template>
<!-- Upload Files -->
<v-list-item class="styled-menu-item" rounded="md" @click="triggerImageInput">
<template v-slot:prepend>
<v-icon icon="mdi-file-upload-outline" size="small"></v-icon>
</template>
<v-list-item-title>
{{ tm('input.upload') }}
</v-list-item-title>
</v-list-item>
<!-- Upload Files -->
<v-list-item
class="styled-menu-item"
rounded="md"
@click="triggerImageInput"
>
<template #prepend>
<v-icon
icon="mdi-file-upload-outline"
size="small"
/>
</template>
<v-list-item-title>
{{ tm('input.upload') }}
</v-list-item-title>
</v-list-item>
<!-- Config Selector in Menu -->
<ConfigSelector :session-id="sessionId || null" :platform-id="sessionPlatformId"
:is-group="sessionIsGroup" :initial-config-id="props.configId"
@config-changed="handleConfigChange" />
<!-- Config Selector in Menu -->
<ConfigSelector
:session-id="sessionId || null"
:platform-id="sessionPlatformId"
:is-group="sessionIsGroup"
:initial-config-id="props.configId"
@config-changed="handleConfigChange"
/>
<!-- Streaming Toggle in Menu -->
<v-list-item class="styled-menu-item" rounded="md" @click="$emit('toggleStreaming')">
<template v-slot:prepend>
<v-icon :icon="enableStreaming ? 'mdi-flash' : 'mdi-flash-off'" size="small"></v-icon>
</template>
<v-list-item-title>
{{ enableStreaming ? tm('streaming.enabled') : tm('streaming.disabled') }}
</v-list-item-title>
</v-list-item>
</StyledMenu>
<!-- Streaming Toggle in Menu -->
<v-list-item
class="styled-menu-item"
rounded="md"
@click="$emit('toggleStreaming')"
>
<template #prepend>
<v-icon
:icon="enableStreaming ? 'mdi-flash' : 'mdi-flash-off'"
size="small"
/>
</template>
<v-list-item-title>
{{ enableStreaming ? tm('streaming.enabled') : tm('streaming.disabled') }}
</v-list-item-title>
</v-list-item>
</StyledMenu>
<!-- Provider/Model Selector Menu -->
<ProviderModelMenu v-if="showProviderSelector" ref="providerModelMenuRef" />
</div>
<div style="display: flex; justify-content: flex-end; margin-top: 8px; align-items: center; flex-shrink: 0;">
<input type="file" ref="imageInputRef" @change="handleFileSelect" style="display: none" multiple />
<v-progress-circular v-if="disabled && !mobile" indeterminate size="16" class="mr-1" width="1.5" />
<!-- <v-btn @click="$emit('openLiveMode')"
<!-- Provider/Model Selector Menu -->
<ProviderModelMenu
v-if="showProviderSelector"
ref="providerModelMenuRef"
/>
</div>
<div style="display: flex; justify-content: flex-end; margin-top: 8px; align-items: center; flex-shrink: 0;">
<input
ref="imageInputRef"
type="file"
style="display: none"
multiple
@change="handleFileSelect"
>
<v-progress-circular
v-if="disabled && !mobile"
indeterminate
size="16"
class="mr-1"
width="1.5"
/>
<!-- <v-btn @click="$emit('openLiveMode')"
icon
variant="text"
color="purple"
@@ -87,54 +171,136 @@
{{ tm('voice.liveMode') }}
</v-tooltip>
</v-btn> -->
<v-btn @click="handleRecordClick" icon variant="text" :color="isRecording ? 'error' : 'primary'"
class="record-btn">
<v-icon :icon="isRecording ? 'mdi-stop-circle' : 'mdi-microphone'" variant="text"
plain></v-icon>
<v-tooltip activator="parent" location="top">
{{ isRecording ? tm('voice.speaking') : tm('voice.startRecording') }}
</v-tooltip>
</v-btn>
<v-btn icon v-if="isRunning && !canSend" @click="$emit('stop')" variant="tonal" color="primary" class="send-btn">
<v-icon icon="mdi-stop" variant="text" plain></v-icon>
<v-tooltip activator="parent" location="top">
{{ tm('input.stopGenerating') }}
</v-tooltip>
</v-btn>
<v-btn v-else @click="$emit('send')" icon="mdi-send" variant="tonal" color="primary"
:disabled="!canSend" class="send-btn" />
</div>
</div>
</div>
<!-- 附件预览区 -->
<div class="attachments-preview"
v-if="stagedImagesUrl.length > 0 || stagedAudioUrl || (stagedFiles && stagedFiles.length > 0)">
<div v-for="(img, index) in stagedImagesUrl" :key="'img-' + index" class="image-preview">
<img :src="img" class="preview-image" />
<v-btn @click="$emit('removeImage', index)" class="remove-attachment-btn" icon="mdi-close" size="small"
color="error" variant="text" />
</div>
<div v-if="stagedAudioUrl" class="audio-preview">
<v-chip color="primary" variant="tonal" class="audio-chip">
<v-icon start icon="mdi-microphone" size="small"></v-icon>
{{ tm('voice.recording') }}
</v-chip>
<v-btn @click="$emit('removeAudio')" class="remove-attachment-btn" icon="mdi-close" size="small"
color="error" variant="text" />
</div>
<div v-for="(file, index) in stagedFiles" :key="'file-' + index" class="file-preview">
<v-chip color="primary" variant="tonal" class="file-chip">
<v-icon start icon="mdi-file-document-outline" size="small"></v-icon>
<span class="file-name-preview">{{ file.original_name }}</span>
</v-chip>
<v-btn @click="$emit('removeFile', index)" class="remove-attachment-btn" icon="mdi-close" size="small"
color="error" variant="text" />
</div>
<v-btn
icon
variant="text"
:color="isRecording ? 'error' : 'primary'"
class="record-btn"
@click="handleRecordClick"
>
<v-icon
:icon="isRecording ? 'mdi-stop-circle' : 'mdi-microphone'"
variant="text"
plain
/>
<v-tooltip
activator="parent"
location="top"
>
{{ isRecording ? tm('voice.speaking') : tm('voice.startRecording') }}
</v-tooltip>
</v-btn>
<v-btn
v-if="isRunning && !canSend"
icon
variant="tonal"
color="primary"
class="send-btn"
@click="$emit('stop')"
>
<v-icon
icon="mdi-stop"
variant="text"
plain
/>
<v-tooltip
activator="parent"
location="top"
>
{{ tm('input.stopGenerating') }}
</v-tooltip>
</v-btn>
<v-btn
v-else
icon="mdi-send"
variant="tonal"
color="primary"
:disabled="!canSend"
class="send-btn"
@click="$emit('send')"
/>
</div>
</div>
</div>
<!-- 附件预览区 -->
<div
v-if="stagedImagesUrl.length > 0 || stagedAudioUrl || (stagedFiles && stagedFiles.length > 0)"
class="attachments-preview"
>
<div
v-for="(img, index) in stagedImagesUrl"
:key="'img-' + index"
class="image-preview"
>
<img
:src="img"
class="preview-image"
>
<v-btn
class="remove-attachment-btn"
icon="mdi-close"
size="small"
color="error"
variant="text"
@click="$emit('removeImage', index)"
/>
</div>
<div
v-if="stagedAudioUrl"
class="audio-preview"
>
<v-chip
color="primary"
variant="tonal"
class="audio-chip"
>
<v-icon
start
icon="mdi-microphone"
size="small"
/>
{{ tm('voice.recording') }}
</v-chip>
<v-btn
class="remove-attachment-btn"
icon="mdi-close"
size="small"
color="error"
variant="text"
@click="$emit('removeAudio')"
/>
</div>
<div
v-for="(file, index) in stagedFiles"
:key="'file-' + index"
class="file-preview"
>
<v-chip
color="primary"
variant="tonal"
class="file-chip"
>
<v-icon
start
icon="mdi-file-document-outline"
size="small"
/>
<span class="file-name-preview">{{ file.original_name }}</span>
</v-chip>
<v-btn
class="remove-attachment-btn"
icon="mdi-close"
size="small"
color="error"
variant="text"
@click="$emit('removeFile', index)"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">

View File

@@ -1,75 +1,112 @@
<template>
<div>
<v-list-item
class="styled-menu-item"
rounded="md"
@click="openDialog"
:disabled="loadingConfigs || saving"
>
<template v-slot:prepend>
<v-icon icon="mdi-cog-outline" size="small"></v-icon>
</template>
<v-list-item-title>
{{ tm('config.title') }}
</v-list-item-title>
<v-list-item-subtitle class="text-caption">
{{ selectedConfigLabel }}
</v-list-item-subtitle>
<template v-slot:append>
<v-icon icon="mdi-chevron-right" size="small" class="text-medium-emphasis"></v-icon>
</template>
</v-list-item>
<div>
<v-list-item
class="styled-menu-item"
rounded="md"
:disabled="loadingConfigs || saving"
@click="openDialog"
>
<template #prepend>
<v-icon
icon="mdi-cog-outline"
size="small"
/>
</template>
<v-list-item-title>
{{ tm('config.title') }}
</v-list-item-title>
<v-list-item-subtitle class="text-caption">
{{ selectedConfigLabel }}
</v-list-item-subtitle>
<template #append>
<v-icon
icon="mdi-chevron-right"
size="small"
class="text-medium-emphasis"
/>
</template>
</v-list-item>
<v-dialog v-model="dialog" max-width="480">
<v-card>
<v-card-title class="d-flex align-center justify-space-between">
<span>选择配置文件</span>
<v-btn icon variant="text" @click="closeDialog">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text>
<div v-if="loadingConfigs" class="text-center py-6">
<v-progress-circular indeterminate color="primary"></v-progress-circular>
</div>
<v-dialog
v-model="dialog"
max-width="480"
>
<v-card>
<v-card-title class="d-flex align-center justify-space-between">
<span>选择配置文件</span>
<v-btn
icon
variant="text"
@click="closeDialog"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text>
<div
v-if="loadingConfigs"
class="text-center py-6"
>
<v-progress-circular
indeterminate
color="primary"
/>
</div>
<v-list v-else class="config-list" density="comfortable">
<v-list-item
v-for="config in configOptions"
:key="config.id"
:active="tempSelectedConfig === config.id"
rounded="lg"
variant="text"
@click="tempSelectedConfig = config.id"
>
<v-list-item-title>{{ config.name }}</v-list-item-title>
<v-list-item-subtitle class="text-caption text-grey">
{{ config.id }}
</v-list-item-subtitle>
<template #append>
<v-icon v-if="tempSelectedConfig === config.id" color="primary">mdi-check</v-icon>
</template>
</v-list-item>
<div v-if="configOptions.length === 0" class="text-center text-body-2 text-medium-emphasis">
暂无可选配置请先在配置页创建
</div>
</v-list>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn variant="text" @click="closeDialog">取消</v-btn>
<v-btn
color="primary"
@click="confirmSelection"
:disabled="!tempSelectedConfig"
:loading="saving"
>
应用
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
<v-list
v-else
class="config-list"
density="comfortable"
>
<v-list-item
v-for="config in configOptions"
:key="config.id"
:active="tempSelectedConfig === config.id"
rounded="lg"
variant="text"
@click="tempSelectedConfig = config.id"
>
<v-list-item-title>{{ config.name }}</v-list-item-title>
<v-list-item-subtitle class="text-caption text-grey">
{{ config.id }}
</v-list-item-subtitle>
<template #append>
<v-icon
v-if="tempSelectedConfig === config.id"
color="primary"
>
mdi-check
</v-icon>
</template>
</v-list-item>
<div
v-if="configOptions.length === 0"
class="text-center text-body-2 text-medium-emphasis"
>
暂无可选配置请先在配置页创建
</div>
</v-list>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
variant="text"
@click="closeDialog"
>
取消
</v-btn>
<v-btn
color="primary"
:disabled="!tempSelectedConfig"
:loading="saving"
@click="confirmSelection"
>
应用
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script setup lang="ts">

View File

@@ -1,301 +1,456 @@
<template>
<div class="sidebar-panel"
:class="{
'sidebar-collapsed': sidebarCollapsed && !isMobile,
'mobile-sidebar-open': isMobile && mobileMenuOpen,
'mobile-sidebar': isMobile
}"
:style="{ backgroundColor: sidebarCollapsed && !isMobile ? 'rgb(var(--v-theme-surface))' : 'rgb(var(--v-theme-mcpCardBg))' }">
<div
class="sidebar-panel"
:class="{
'sidebar-collapsed': sidebarCollapsed && !isMobile,
'mobile-sidebar-open': isMobile && mobileMenuOpen,
'mobile-sidebar': isMobile
}"
:style="{ backgroundColor: sidebarCollapsed && !isMobile ? 'rgb(var(--v-theme-surface))' : 'rgb(var(--v-theme-mcpCardBg))' }"
>
<div
v-if="!isMobile"
class="sidebar-collapse-btn-container"
>
<v-btn
icon
class="sidebar-collapse-btn"
variant="text"
color="deep-purple"
@click="toggleSidebar"
>
<v-icon>{{ sidebarCollapsed ? 'mdi-chevron-right' : 'mdi-chevron-left' }}</v-icon>
</v-btn>
</div>
<div class="sidebar-collapse-btn-container" v-if="!isMobile">
<v-btn icon class="sidebar-collapse-btn" @click="toggleSidebar" variant="text" color="deep-purple">
<v-icon>{{ sidebarCollapsed ? 'mdi-chevron-right' : 'mdi-chevron-left' }}</v-icon>
</v-btn>
</div>
<div
v-if="isMobile"
class="sidebar-collapse-btn-container"
>
<v-btn
icon
class="sidebar-collapse-btn"
variant="text"
color="deep-purple"
@click="$emit('closeMobileSidebar')"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
<div class="sidebar-collapse-btn-container" v-if="isMobile">
<v-btn icon class="sidebar-collapse-btn" @click="$emit('closeMobileSidebar')" variant="text"
color="deep-purple">
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
<div style="padding: 8px; opacity: 0.6;">
<div
v-if="!sidebarCollapsed || isMobile"
class="new-chat-row"
>
<v-btn
block
variant="text"
class="new-chat-btn"
:disabled="!currSessionId && !selectedProjectId"
prepend-icon="mdi-square-edit-outline"
@click="$emit('newChat')"
>
{{ tm('actions.newChat') }}
</v-btn>
<v-btn
v-if="sessions.length > 0"
icon
size="small"
variant="text"
:color="batchMode ? 'primary' : undefined"
@click="toggleBatchMode"
>
<v-icon>mdi-checkbox-multiple-marked-outline</v-icon>
</v-btn>
</div>
<v-btn
v-if="sidebarCollapsed && !isMobile"
icon="mdi-square-edit-outline"
rounded="xl"
:disabled="!currSessionId && !selectedProjectId"
elevation="0"
@click="$emit('newChat')"
/>
</div>
<div style="padding: 8px; opacity: 0.6;">
<div class="new-chat-row" v-if="!sidebarCollapsed || isMobile">
<v-btn block variant="text" class="new-chat-btn" @click="$emit('newChat')" :disabled="!currSessionId && !selectedProjectId"
prepend-icon="mdi-square-edit-outline">{{ tm('actions.newChat') }}</v-btn>
<v-btn v-if="sessions.length > 0" icon size="small" variant="text" @click="toggleBatchMode"
:color="batchMode ? 'primary' : undefined">
<v-icon>mdi-checkbox-multiple-marked-outline</v-icon>
</v-btn>
</div>
<v-btn icon="mdi-square-edit-outline" rounded="xl" @click="$emit('newChat')" :disabled="!currSessionId && !selectedProjectId"
v-if="sidebarCollapsed && !isMobile" elevation="0"></v-btn>
</div>
<!-- Batch action bar -->
<div
v-if="batchMode && (!sidebarCollapsed || isMobile)"
class="batch-action-bar"
>
<v-btn
size="x-small"
variant="text"
@click="toggleSelectAll"
>
{{ isAllSelected ? tm('batch.deselectAll') : tm('batch.selectAll') }}
</v-btn>
<span class="batch-selected-count">{{ tm('batch.selected', { count: batchSelected.length }) }}</span>
<v-spacer />
<v-btn
size="x-small"
variant="text"
color="error"
:disabled="batchSelected.length === 0"
@click="handleBatchDelete"
>
{{ tm('batch.delete') }}
</v-btn>
</div>
<!-- Batch action bar -->
<div v-if="batchMode && (!sidebarCollapsed || isMobile)" class="batch-action-bar">
<v-btn size="x-small" variant="text" @click="toggleSelectAll">
{{ isAllSelected ? tm('batch.deselectAll') : tm('batch.selectAll') }}
</v-btn>
<span class="batch-selected-count">{{ tm('batch.selected', { count: batchSelected.length }) }}</span>
<v-spacer />
<v-btn size="x-small" variant="text" color="error" :disabled="batchSelected.length === 0"
@click="handleBatchDelete">
{{ tm('batch.delete') }}
</v-btn>
</div>
<!-- 项目列表组件 -->
<ProjectList
v-if="!sidebarCollapsed || isMobile"
:projects="projects"
@selectProject="$emit('selectProject', $event)"
@createProject="$emit('createProject')"
@editProject="$emit('editProject', $event)"
@deleteProject="$emit('deleteProject', $event)"
/>
<!-- 项目列表组件 -->
<ProjectList
v-if="!sidebarCollapsed || isMobile"
:projects="projects"
@selectProject="$emit('selectProject', $event)"
@createProject="$emit('createProject')"
@editProject="$emit('editProject', $event)"
@deleteProject="$emit('deleteProject', $event)"
/>
<div
v-if="!sidebarCollapsed || isMobile"
style="overflow-y: auto; flex-grow: 1; overscroll-behavior-y: contain;"
>
<v-card
v-if="sessions.length > 0"
flat
style="background-color: transparent;"
>
<v-list
density="compact"
nav
class="conversation-list"
style="background-color: transparent;"
:selected="batchMode ? [] : selectedSessions"
@update:selected="handleListSelect"
>
<v-list-item
v-for="item in sessions"
:key="item.session_id"
:value="item.session_id"
rounded="lg"
class="conversation-item"
active-color="secondary"
@click="batchMode ? toggleBatchItem(item.session_id) : undefined"
>
<template #prepend>
<div
class="batch-checkbox-slot"
:class="{ 'batch-checkbox-slot--active': batchMode }"
>
<v-checkbox-btn
:model-value="batchSelected.includes(item.session_id)"
density="compact"
hide-details
class="batch-checkbox"
@update:model-value="toggleBatchItem(item.session_id)"
@click.stop
/>
</div>
</template>
<div style="overflow-y: auto; flex-grow: 1; overscroll-behavior-y: contain;"
v-if="!sidebarCollapsed || isMobile">
<v-card v-if="sessions.length > 0" flat style="background-color: transparent;">
<v-list density="compact" nav class="conversation-list"
style="background-color: transparent;" :selected="batchMode ? [] : selectedSessions"
@update:selected="handleListSelect">
<v-list-item v-for="item in sessions" :key="item.session_id" :value="item.session_id"
rounded="lg" class="conversation-item" active-color="secondary"
@click="batchMode ? toggleBatchItem(item.session_id) : undefined">
<template v-slot:prepend>
<div class="batch-checkbox-slot" :class="{ 'batch-checkbox-slot--active': batchMode }">
<v-checkbox-btn
:model-value="batchSelected.includes(item.session_id)"
@update:model-value="toggleBatchItem(item.session_id)"
@click.stop
density="compact"
hide-details
class="batch-checkbox"
/>
</div>
</template>
<v-list-item-title v-if="!sidebarCollapsed || isMobile" class="conversation-title"
:style="{ color: 'rgb(var(--v-theme-primaryText))' }">
{{ item.display_name || tm('conversation.newConversation') }}
</v-list-item-title>
<!-- <v-list-item-subtitle v-if="!sidebarCollapsed || isMobile" class="timestamp">
<v-list-item-title
v-if="!sidebarCollapsed || isMobile"
class="conversation-title"
:style="{ color: 'rgb(var(--v-theme-primaryText))' }"
>
{{ item.display_name || tm('conversation.newConversation') }}
</v-list-item-title>
<!-- <v-list-item-subtitle v-if="!sidebarCollapsed || isMobile" class="timestamp">
{{ new Date(item.updated_at).toLocaleString() }}
</v-list-item-subtitle> -->
<template v-if="!batchMode && (!sidebarCollapsed || isMobile)" v-slot:append>
<div class="conversation-actions">
<v-btn icon="mdi-pencil" size="x-small" variant="text"
class="edit-title-btn"
@click.stop="$emit('editTitle', item.session_id, item.display_name ?? '')" />
<v-btn icon="mdi-delete" size="x-small" variant="text"
class="delete-conversation-btn" color="error"
@click.stop="handleDeleteConversation(item)" />
</div>
</template>
</v-list-item>
</v-list>
</v-card>
<template
v-if="!batchMode && (!sidebarCollapsed || isMobile)"
#append
>
<div class="conversation-actions">
<v-btn
icon="mdi-pencil"
size="x-small"
variant="text"
class="edit-title-btn"
@click.stop="$emit('editTitle', item.session_id, item.display_name ?? '')"
/>
<v-btn
icon="mdi-delete"
size="x-small"
variant="text"
class="delete-conversation-btn"
color="error"
@click.stop="handleDeleteConversation(item)"
/>
</div>
</template>
</v-list-item>
</v-list>
</v-card>
<v-fade-transition>
<div class="no-conversations" v-if="sessions.length === 0">
<v-icon icon="mdi-message-text-outline" size="large" color="grey-lighten-1"></v-icon>
<div class="no-conversations-text" v-if="!sidebarCollapsed || isMobile">
{{ tm('conversation.noHistory') }}
</div>
</div>
</v-fade-transition>
<v-fade-transition>
<div
v-if="sessions.length === 0"
class="no-conversations"
>
<v-icon
icon="mdi-message-text-outline"
size="large"
color="grey-lighten-1"
/>
<div
v-if="!sidebarCollapsed || isMobile"
class="no-conversations-text"
>
{{ tm('conversation.noHistory') }}
</div>
</div>
<!-- 收起时的占位元素 -->
<div class="sidebar-spacer" v-if="sidebarCollapsed && !isMobile"></div>
<!-- 底部设置按钮 -->
<div class="sidebar-footer">
<StyledMenu location="top" :close-on-content-click="false">
<template v-slot:activator="{ props: menuProps }">
<v-btn
v-bind="menuProps"
:icon="sidebarCollapsed && !isMobile"
:block="!sidebarCollapsed || isMobile"
variant="text"
class="settings-btn"
:class="{ 'settings-btn-collapsed': sidebarCollapsed && !isMobile }"
:prepend-icon="(!sidebarCollapsed || isMobile) ? 'mdi-cog-outline' : undefined"
>
<v-icon v-if="sidebarCollapsed && !isMobile">mdi-cog-outline</v-icon>
<template v-if="!sidebarCollapsed || isMobile">{{ t('core.common.settings') }}</template>
</v-btn>
</template>
<!-- 语言切换分组 -->
<v-menu
:open-on-hover="!isMobile"
:open-on-click="isMobile"
:open-delay="!isMobile ? 60 : 0"
:close-delay="!isMobile ? 120 : 0"
:location="isMobile ? 'bottom' : 'end center'"
offset="8"
close-on-content-click
>
<template v-slot:activator="{ props: languageMenuProps }">
<v-list-item
v-bind="languageMenuProps"
class="styled-menu-item chat-settings-group-trigger"
rounded="md"
>
<template v-slot:prepend>
<v-icon>mdi-translate</v-icon>
</template>
<v-list-item-title>{{ t('core.common.language') }}</v-list-item-title>
<template v-slot:append>
<span class="chat-settings-group-current">{{ currentLanguage?.flag }}</span>
<v-icon size="18" class="chat-settings-group-arrow">mdi-chevron-right</v-icon>
</template>
</v-list-item>
</template>
<v-card class="styled-menu-card" style="min-width: 180px;" elevation="8" rounded="lg">
<v-list density="compact" class="styled-menu-list pa-1">
<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>
</v-card>
</v-menu>
<!-- 主题切换 -->
<v-list-item class="styled-menu-item" @click="$emit('toggleTheme')">
<template v-slot:prepend>
<v-icon>{{ isDark ? 'mdi-weather-night' : 'mdi-white-balance-sunny' }}</v-icon>
</template>
<v-list-item-title>{{ isDark ? tm('modes.lightMode') : tm('modes.darkMode') }}</v-list-item-title>
</v-list-item>
<!-- 通信传输模式分组 -->
<v-menu
:open-on-hover="!isMobile"
:open-on-click="isMobile"
:open-delay="!isMobile ? 60 : 0"
:close-delay="!isMobile ? 120 : 0"
:location="isMobile ? 'bottom' : 'end center'"
offset="8"
close-on-content-click
>
<template v-slot:activator="{ props: transportMenuProps }">
<v-list-item
v-bind="transportMenuProps"
class="styled-menu-item chat-settings-group-trigger"
rounded="md"
>
<template v-slot:prepend>
<v-icon>mdi-lan-connect</v-icon>
</template>
<v-list-item-title>{{ tm('transport.title') }}</v-list-item-title>
<template v-slot:append>
<span class="chat-settings-group-current chat-settings-transport-current">{{ currentTransportLabel }}</span>
<v-icon size="18" class="chat-settings-group-arrow">mdi-chevron-right</v-icon>
</template>
</v-list-item>
</template>
<v-card class="styled-menu-card" style="min-width: 220px;" elevation="8" rounded="lg">
<v-list density="compact" class="styled-menu-list pa-1">
<v-list-item
v-for="opt in transportOptions"
:key="opt.value"
:value="opt.value"
@click="handleTransportModeChange(opt.value)"
:class="{ 'styled-menu-item-active': transportMode === opt.value }"
class="styled-menu-item"
rounded="md"
>
<v-list-item-title>{{ opt.label }}</v-list-item-title>
</v-list-item>
</v-list>
</v-card>
</v-menu>
<!-- 发送快捷键分组 -->
<v-menu
:open-on-hover="!isMobile"
:open-on-click="isMobile"
:open-delay="!isMobile ? 60 : 0"
:close-delay="!isMobile ? 120 : 0"
:location="isMobile ? 'bottom' : 'end center'"
offset="8"
close-on-content-click
>
<template v-slot:activator="{ props: sendShortcutMenuProps }">
<v-list-item
v-bind="sendShortcutMenuProps"
class="styled-menu-item chat-settings-group-trigger"
rounded="md"
>
<template v-slot:prepend>
<v-icon>mdi-keyboard-outline</v-icon>
</template>
<v-list-item-title>{{ tm('shortcuts.sendKey.title') }}</v-list-item-title>
<template v-slot:append>
<span class="chat-settings-group-current chat-settings-transport-current">{{ currentSendShortcutLabel }}</span>
<v-icon size="18" class="chat-settings-group-arrow">mdi-chevron-right</v-icon>
</template>
</v-list-item>
</template>
<v-card class="styled-menu-card" style="min-width: 220px;" elevation="8" rounded="lg">
<v-list density="compact" class="styled-menu-list pa-1">
<v-list-item
v-for="opt in sendShortcutOptions"
:key="opt.value"
:value="opt.value"
@click="handleSendShortcutChange(opt.value)"
:class="{ 'styled-menu-item-active': props.sendShortcut === opt.value }"
class="styled-menu-item"
rounded="md"
>
<v-list-item-title>{{ opt.label }}</v-list-item-title>
</v-list-item>
</v-list>
</v-card>
</v-menu>
<!-- 全屏/退出全屏 -->
<v-list-item class="styled-menu-item" @click="$emit('toggleFullscreen')">
<template v-slot:prepend>
<v-icon>{{ chatboxMode ? 'mdi-fullscreen-exit' : 'mdi-fullscreen' }}</v-icon>
</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" />
</v-fade-transition>
</div>
<!-- 收起时的占位元素 -->
<div
v-if="sidebarCollapsed && !isMobile"
class="sidebar-spacer"
/>
<!-- 底部设置按钮 -->
<div class="sidebar-footer">
<StyledMenu
location="top"
:close-on-content-click="false"
>
<template #activator="{ props: menuProps }">
<v-btn
v-bind="menuProps"
:icon="sidebarCollapsed && !isMobile"
:block="!sidebarCollapsed || isMobile"
variant="text"
class="settings-btn"
:class="{ 'settings-btn-collapsed': sidebarCollapsed && !isMobile }"
:prepend-icon="(!sidebarCollapsed || isMobile) ? 'mdi-cog-outline' : undefined"
>
<v-icon v-if="sidebarCollapsed && !isMobile">
mdi-cog-outline
</v-icon>
<template v-if="!sidebarCollapsed || isMobile">
{{ t('core.common.settings') }}
</template>
</v-btn>
</template>
<!-- 语言切换分组 -->
<v-menu
:open-on-hover="!isMobile"
:open-on-click="isMobile"
:open-delay="!isMobile ? 60 : 0"
:close-delay="!isMobile ? 120 : 0"
:location="isMobile ? 'bottom' : 'end center'"
offset="8"
close-on-content-click
>
<template #activator="{ props: languageMenuProps }">
<v-list-item
v-bind="languageMenuProps"
class="styled-menu-item chat-settings-group-trigger"
rounded="md"
>
<template #prepend>
<v-icon>mdi-translate</v-icon>
</template>
<v-list-item-title>{{ t('core.common.language') }}</v-list-item-title>
<template #append>
<span class="chat-settings-group-current">{{ currentLanguage?.flag }}</span>
<v-icon
size="18"
class="chat-settings-group-arrow"
>
mdi-chevron-right
</v-icon>
</template>
</v-list-item>
</template>
<v-card
class="styled-menu-card"
style="min-width: 180px;"
elevation="8"
rounded="lg"
>
<v-list
density="compact"
class="styled-menu-list pa-1"
>
<v-list-item
v-for="lang in languages"
:key="lang.code"
:value="lang.code"
:class="{ 'styled-menu-item-active': currentLocale === lang.code }"
class="styled-menu-item"
rounded="md"
@click="changeLanguage(lang.code)"
>
<template #prepend>
<span class="language-flag">{{ lang.flag }}</span>
</template>
<v-list-item-title>{{ lang.name }}</v-list-item-title>
</v-list-item>
</v-list>
</v-card>
</v-menu>
<!-- 主题切换 -->
<v-list-item
class="styled-menu-item"
@click="$emit('toggleTheme')"
>
<template #prepend>
<v-icon>{{ isDark ? 'mdi-weather-night' : 'mdi-white-balance-sunny' }}</v-icon>
</template>
<v-list-item-title>{{ isDark ? tm('modes.lightMode') : tm('modes.darkMode') }}</v-list-item-title>
</v-list-item>
<!-- 通信传输模式分组 -->
<v-menu
:open-on-hover="!isMobile"
:open-on-click="isMobile"
:open-delay="!isMobile ? 60 : 0"
:close-delay="!isMobile ? 120 : 0"
:location="isMobile ? 'bottom' : 'end center'"
offset="8"
close-on-content-click
>
<template #activator="{ props: transportMenuProps }">
<v-list-item
v-bind="transportMenuProps"
class="styled-menu-item chat-settings-group-trigger"
rounded="md"
>
<template #prepend>
<v-icon>mdi-lan-connect</v-icon>
</template>
<v-list-item-title>{{ tm('transport.title') }}</v-list-item-title>
<template #append>
<span class="chat-settings-group-current chat-settings-transport-current">{{ currentTransportLabel }}</span>
<v-icon
size="18"
class="chat-settings-group-arrow"
>
mdi-chevron-right
</v-icon>
</template>
</v-list-item>
</template>
<v-card
class="styled-menu-card"
style="min-width: 220px;"
elevation="8"
rounded="lg"
>
<v-list
density="compact"
class="styled-menu-list pa-1"
>
<v-list-item
v-for="opt in transportOptions"
:key="opt.value"
:value="opt.value"
:class="{ 'styled-menu-item-active': transportMode === opt.value }"
class="styled-menu-item"
rounded="md"
@click="handleTransportModeChange(opt.value)"
>
<v-list-item-title>{{ opt.label }}</v-list-item-title>
</v-list-item>
</v-list>
</v-card>
</v-menu>
<!-- 发送快捷键分组 -->
<v-menu
:open-on-hover="!isMobile"
:open-on-click="isMobile"
:open-delay="!isMobile ? 60 : 0"
:close-delay="!isMobile ? 120 : 0"
:location="isMobile ? 'bottom' : 'end center'"
offset="8"
close-on-content-click
>
<template #activator="{ props: sendShortcutMenuProps }">
<v-list-item
v-bind="sendShortcutMenuProps"
class="styled-menu-item chat-settings-group-trigger"
rounded="md"
>
<template #prepend>
<v-icon>mdi-keyboard-outline</v-icon>
</template>
<v-list-item-title>{{ tm('shortcuts.sendKey.title') }}</v-list-item-title>
<template #append>
<span class="chat-settings-group-current chat-settings-transport-current">{{ currentSendShortcutLabel }}</span>
<v-icon
size="18"
class="chat-settings-group-arrow"
>
mdi-chevron-right
</v-icon>
</template>
</v-list-item>
</template>
<v-card
class="styled-menu-card"
style="min-width: 220px;"
elevation="8"
rounded="lg"
>
<v-list
density="compact"
class="styled-menu-list pa-1"
>
<v-list-item
v-for="opt in sendShortcutOptions"
:key="opt.value"
:value="opt.value"
:class="{ 'styled-menu-item-active': props.sendShortcut === opt.value }"
class="styled-menu-item"
rounded="md"
@click="handleSendShortcutChange(opt.value)"
>
<v-list-item-title>{{ opt.label }}</v-list-item-title>
</v-list-item>
</v-list>
</v-card>
</v-menu>
<!-- 全屏/退出全屏 -->
<v-list-item
class="styled-menu-item"
@click="$emit('toggleFullscreen')"
>
<template #prepend>
<v-icon>{{ chatboxMode ? 'mdi-fullscreen-exit' : 'mdi-fullscreen' }}</v-icon>
</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 #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>
<script setup lang="ts">

View File

@@ -1,13 +1,18 @@
<template>
<div class="live-mode-container">
<div class="header-controls">
<v-btn icon="mdi-close" @click="handleClose" flat variant="text" />
<v-btn
icon="mdi-close"
flat
variant="text"
@click="handleClose"
/>
<v-btn
:icon="isCodeMode ? 'mdi-code-tags-check' : 'mdi-code-tags'"
@click="toggleCodeMode"
flat
variant="text"
:color="isCodeMode ? 'primary' : ''"
@click="toggleCodeMode"
/>
<v-btn
:icon="
@@ -15,22 +20,26 @@
? 'mdi-emoticon-confused'
: 'mdi-emoticon-confused-outline'
"
@click="toggleNervousMode"
flat
variant="text"
:color="isNervousMode ? 'primary' : ''"
@click="toggleNervousMode"
/>
</div>
<span style="color: gray; padding-left: 16px"
>We're developing Astr Live Mode on ChatUI & Desktop right now. Stay
tuned!</span
>
<span style="color: gray; padding-left: 16px">We're developing Astr Live Mode on ChatUI & Desktop right now. Stay
tuned!</span>
<div class="live-mode-content">
<div class="center-circle-container" @click="handleCircleClick">
<div
class="center-circle-container"
@click="handleCircleClick"
>
<!-- 爆炸效果层 -->
<div v-if="isExploding" class="explosion-wave"></div>
<div
v-if="isExploding"
class="explosion-wave"
/>
<SiriOrb
:energy="orbEnergy"
@@ -44,7 +53,10 @@
<div class="status-text">
{{ statusText }}
</div>
<div class="messages-container" v-if="messages.length > 0">
<div
v-if="messages.length > 0"
class="messages-container"
>
<div
v-for="(msg, index) in messages"
:key="index"
@@ -57,40 +69,27 @@
</div>
</div>
<div class="metrics-container" v-if="Object.keys(metrics).length > 0">
<span v-if="metrics.wav_assemble_time"
>WAV Assemble:
{{ (metrics.wav_assemble_time * 1000).toFixed(0) }}ms</span
>
<span v-if="metrics.llm_ttft"
>LLM First Token Latency:
{{ (metrics.llm_ttft * 1000).toFixed(0) }}ms</span
>
<span v-if="metrics.llm_total_time"
>LLM Total Latency:
{{ (metrics.llm_total_time * 1000).toFixed(0) }}ms</span
>
<span v-if="metrics.tts_first_frame_time"
>TTS First Frame Latency:
{{ (metrics.tts_first_frame_time * 1000).toFixed(0) }}ms</span
>
<span v-if="metrics.tts_total_time"
>TTS Total Larency:
{{ (metrics.tts_total_time * 1000).toFixed(0) }}ms</span
>
<span v-if="metrics.speak_to_first_frame"
>Speak -> First TTS Frame:
{{ (metrics.speak_to_first_frame * 1000).toFixed(0) }}ms</span
>
<span v-if="metrics.wav_to_tts_total_time"
>Speak -> End:
{{ (metrics.wav_to_tts_total_time * 1000).toFixed(0) }}ms</span
>
<div
v-if="Object.keys(metrics).length > 0"
class="metrics-container"
>
<span v-if="metrics.wav_assemble_time">WAV Assemble:
{{ (metrics.wav_assemble_time * 1000).toFixed(0) }}ms</span>
<span v-if="metrics.llm_ttft">LLM First Token Latency:
{{ (metrics.llm_ttft * 1000).toFixed(0) }}ms</span>
<span v-if="metrics.llm_total_time">LLM Total Latency:
{{ (metrics.llm_total_time * 1000).toFixed(0) }}ms</span>
<span v-if="metrics.tts_first_frame_time">TTS First Frame Latency:
{{ (metrics.tts_first_frame_time * 1000).toFixed(0) }}ms</span>
<span v-if="metrics.tts_total_time">TTS Total Larency:
{{ (metrics.tts_total_time * 1000).toFixed(0) }}ms</span>
<span v-if="metrics.speak_to_first_frame">Speak -> First TTS Frame:
{{ (metrics.speak_to_first_frame * 1000).toFixed(0) }}ms</span>
<span v-if="metrics.wav_to_tts_total_time">Speak -> End:
{{ (metrics.wav_to_tts_total_time * 1000).toFixed(0) }}ms</span>
<span v-if="metrics.stt">STT Provider: {{ metrics.stt }}</span>
<span v-if="metrics.tts">TTS Provider: {{ metrics.tts }}</span>
<span v-if="metrics.chat_model"
>Chat Model: {{ metrics.chat_model }}</span
>
<span v-if="metrics.chat_model">Chat Model: {{ metrics.chat_model }}</span>
</div>
</div>
</div>

View File

@@ -1,53 +1,111 @@
<template>
<div class="live-orb-container" ref="containerRef" :class="{ 'dark': isDark }" :style="styleVars">
<div class="live-orb">
<div
ref="containerRef"
class="live-orb-container"
:class="{ 'dark': isDark }"
:style="styleVars"
>
<div class="live-orb" />
<div class="eyes-container">
<div
class="eye"
:class="{ 'blink': isBlinking, 'nervous': nervousMode }"
>
<!-- Nervous Mode > -->
<div
v-if="nervousMode"
class="nervous-eye-content"
>
<svg
viewBox="0 0 30 60"
width="100%"
height="100%"
>
<path
d="M 0 10 L 30 30 L 0 50"
fill="none"
stroke="#7d80e4"
stroke-width="8"
/>
</svg>
</div>
<div class="eyes-container">
<div class="eye" :class="{ 'blink': isBlinking, 'nervous': nervousMode }">
<!-- Nervous Mode > -->
<div v-if="nervousMode" class="nervous-eye-content">
<svg viewBox="0 0 30 60" width="100%" height="100%">
<path d="M 0 10 L 30 30 L 0 50" fill="none" stroke="#7d80e4" stroke-width="8" />
</svg>
</div>
<!-- Code Mode Layer -->
<transition name="fade">
<div v-if="codeMode && !nervousMode" class="code-rain-container">
<div v-for="(col, i) in codeColumns" :key="i" class="code-column" :style="col.style">
{{ col.content }}
</div>
</div>
</transition>
<!-- Code Mode Layer -->
<transition name="fade">
<div
v-if="codeMode && !nervousMode"
class="code-rain-container"
>
<div
v-for="(col, i) in codeColumns"
:key="i"
class="code-column"
:style="col.style"
>
{{ col.content }}
</div>
<div class="eye" :class="{ 'blink': isBlinking, 'nervous': nervousMode }">
<!-- Nervous Mode < -->
<div v-if="nervousMode" class="nervous-eye-content">
<svg viewBox="0 0 30 60" width="100%" height="100%">
<path d="M 30 10 L 0 30 L 30 50" fill="none" stroke="#7d80e4" stroke-width="8" />
</svg>
</div>
</div>
</transition>
</div>
<div
class="eye"
:class="{ 'blink': isBlinking, 'nervous': nervousMode }"
>
<!-- Nervous Mode < -->
<div
v-if="nervousMode"
class="nervous-eye-content"
>
<svg
viewBox="0 0 30 60"
width="100%"
height="100%"
>
<path
d="M 30 10 L 0 30 L 30 50"
fill="none"
stroke="#7d80e4"
stroke-width="8"
/>
</svg>
</div>
<!-- Code Mode Layer -->
<transition name="fade">
<div v-if="codeMode && !nervousMode" class="code-rain-container">
<div v-for="(col, i) in codeColumns" :key="i" class="code-column" :style="col.style">
{{ col.content }}
</div>
</div>
</transition>
<!-- Code Mode Layer -->
<transition name="fade">
<div
v-if="codeMode && !nervousMode"
class="code-rain-container"
>
<div
v-for="(col, i) in codeColumns"
:key="i"
class="code-column"
:style="col.style"
>
{{ col.content }}
</div>
</div>
<!-- Hair Accessory Star -->
<div class="accessory-star">
<svg viewBox="0 0 24 24" width="100%" height="100%">
<path d="M12 2l2.4 7.2h7.6l-6 4.8 2.4 7.2-6-4.8-6 4.8 2.4-7.2-6-4.8h7.6z"
fill="rgba(125, 128, 228, 0.4)" stroke="rgba(180, 182, 255, 0.6)" stroke-width="3"
stroke-linejoin="round" />
</svg>
</div>
</div>
</transition>
</div>
</div>
<!-- Hair Accessory Star -->
<div class="accessory-star">
<svg
viewBox="0 0 24 24"
width="100%"
height="100%"
>
<path
d="M12 2l2.4 7.2h7.6l-6 4.8 2.4 7.2-6-4.8-6 4.8 2.4-7.2-6-4.8h7.6z"
fill="rgba(125, 128, 228, 0.4)"
stroke="rgba(180, 182, 255, 0.6)"
stroke-width="3"
stroke-linejoin="round"
/>
</svg>
</div>
</div>
</template>
<script setup lang="ts">

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,62 @@
<template>
<v-dialog v-model="isOpen" max-width="500" @update:model-value="handleDialogChange">
<v-card>
<v-card-title class="dialog-title">
{{ isEditing ? tm('project.edit') : tm('project.create') }}
</v-card-title>
<v-card-text>
<v-text-field v-model="form.emoji" :label="tm('project.emoji')" flat variant="solo-filled" hide-details class="mb-3" />
<v-text-field v-model="form.title" :label="tm('project.name')" flat variant="solo-filled" hide-details class="mb-3" autofocus
@keyup.enter="handleSave" />
<v-textarea v-model="form.description" :label="tm('project.description')" flat variant="solo-filled" hide-details rows="3" rounded="lg" />
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn variant="text" @click="handleCancel" color="grey-darken-1">{{ t('core.common.cancel') }}</v-btn>
<v-btn variant="text" @click="handleSave" color="primary" :disabled="!form.title.trim()">{{ t('core.common.save') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog
v-model="isOpen"
max-width="500"
@update:model-value="handleDialogChange"
>
<v-card>
<v-card-title class="dialog-title">
{{ isEditing ? tm('project.edit') : tm('project.create') }}
</v-card-title>
<v-card-text>
<v-text-field
v-model="form.emoji"
:label="tm('project.emoji')"
flat
variant="solo-filled"
hide-details
class="mb-3"
/>
<v-text-field
v-model="form.title"
:label="tm('project.name')"
flat
variant="solo-filled"
hide-details
class="mb-3"
autofocus
@keyup.enter="handleSave"
/>
<v-textarea
v-model="form.description"
:label="tm('project.description')"
flat
variant="solo-filled"
hide-details
rows="3"
rounded="lg"
/>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
variant="text"
color="grey-darken-1"
@click="handleCancel"
>
{{ t('core.common.cancel') }}
</v-btn>
<v-btn
variant="text"
color="primary"
:disabled="!form.title.trim()"
@click="handleSave"
>
{{ t('core.common.save') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">

View File

@@ -1,44 +1,84 @@
<template>
<div>
<!-- 项目按钮 -->
<div style="padding: 0 8px 0px 8px; opacity: 0.6;">
<v-btn block variant="text" class="project-btn" @click="toggleExpanded" prepend-icon="mdi-folder-outline">
{{ tm('project.title') }}
<template v-slot:append>
<v-icon size="small">{{ expanded ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
</template>
</v-btn>
</div>
<!-- 项目列表 -->
<v-expand-transition>
<div v-show="expanded" style="padding: 0 8px;">
<v-list density="compact" nav class="project-list" style="background-color: transparent;">
<v-list-item @click="$emit('createProject')" class="create-project-item" rounded="lg">
<template v-slot:prepend>
<span class="project-emoji"><v-icon size="small">mdi-plus</v-icon></span>
</template>
<v-list-item-title style="font-size: 13px;">{{ tm('project.create') }}</v-list-item-title>
</v-list-item>
<v-list-item v-for="project in projects" :key="project.project_id"
@click="$emit('selectProject', project.project_id)" rounded="lg" class="project-item">
<template v-slot:prepend>
<span class="project-emoji">{{ project.emoji || '📁' }}</span>
</template>
<v-list-item-title class="project-title">{{ project.title }}</v-list-item-title>
<template v-slot:append>
<div class="project-actions">
<v-btn icon="mdi-pencil" size="x-small" variant="text" class="edit-project-btn"
@click.stop="$emit('editProject', project)" />
<v-btn icon="mdi-delete" size="x-small" variant="text" class="delete-project-btn"
color="error" @click.stop="handleDeleteProject(project)" />
</div>
</template>
</v-list-item>
</v-list>
</div>
</v-expand-transition>
<div>
<!-- 项目按钮 -->
<div style="padding: 0 8px 0px 8px; opacity: 0.6;">
<v-btn
block
variant="text"
class="project-btn"
prepend-icon="mdi-folder-outline"
@click="toggleExpanded"
>
{{ tm('project.title') }}
<template #append>
<v-icon size="small">
{{ expanded ? 'mdi-chevron-up' : 'mdi-chevron-down' }}
</v-icon>
</template>
</v-btn>
</div>
<!-- 项目列表 -->
<v-expand-transition>
<div
v-show="expanded"
style="padding: 0 8px;"
>
<v-list
density="compact"
nav
class="project-list"
style="background-color: transparent;"
>
<v-list-item
class="create-project-item"
rounded="lg"
@click="$emit('createProject')"
>
<template #prepend>
<span class="project-emoji"><v-icon size="small">mdi-plus</v-icon></span>
</template>
<v-list-item-title style="font-size: 13px;">
{{ tm('project.create') }}
</v-list-item-title>
</v-list-item>
<v-list-item
v-for="project in projects"
:key="project.project_id"
rounded="lg"
class="project-item"
@click="$emit('selectProject', project.project_id)"
>
<template #prepend>
<span class="project-emoji">{{ project.emoji || '📁' }}</span>
</template>
<v-list-item-title class="project-title">
{{ project.title }}
</v-list-item-title>
<template #append>
<div class="project-actions">
<v-btn
icon="mdi-pencil"
size="x-small"
variant="text"
class="edit-project-btn"
@click.stop="$emit('editProject', project)"
/>
<v-btn
icon="mdi-delete"
size="x-small"
variant="text"
class="delete-project-btn"
color="error"
@click.stop="handleDeleteProject(project)"
/>
</div>
</template>
</v-list-item>
</v-list>
</div>
</v-expand-transition>
</div>
</template>
<script setup lang="ts">

View File

@@ -1,47 +1,76 @@
<template>
<div class="project-sessions-container fade-in">
<div class="project-header">
<div class="project-header-info">
<span class="project-header-emoji">{{ project?.emoji || '📁' }}</span>
<h2 class="project-header-title">{{ project?.title }}</h2>
</div>
<p class="project-header-description" v-if="project?.description">
{{ project.description }}
</p>
</div>
<div class="project-input-slot">
<slot></slot>
</div>
<v-card flat class="project-sessions-list">
<v-list v-if="sessions.length > 0">
<v-list-item v-for="session in sessions" :key="session.session_id"
@click="$emit('selectSession', session.session_id)" class="project-session-item" rounded="lg">
<v-list-item-title>
{{ session.display_name || tm('conversation.newConversation') }}
</v-list-item-title>
<v-list-item-subtitle>
{{ formatDate(session.updated_at) }}
</v-list-item-subtitle>
<template v-slot:append>
<div class="session-actions">
<v-btn icon="mdi-pencil" size="x-small" variant="text"
class="edit-session-btn"
@click.stop="$emit('editSessionTitle', session.session_id, session.display_name ?? '')" />
<v-btn icon="mdi-delete" size="x-small" variant="text"
class="delete-session-btn" color="error"
@click.stop="handleDeleteSession(session)" />
</div>
</template>
</v-list-item>
</v-list>
<div v-else class="no-sessions-in-project">
<v-icon icon="mdi-message-off-outline" size="large" color="grey-lighten-1"></v-icon>
<p>{{ tm('project.noSessions') }}</p>
</div>
</v-card>
<div class="project-sessions-container fade-in">
<div class="project-header">
<div class="project-header-info">
<span class="project-header-emoji">{{ project?.emoji || '📁' }}</span>
<h2 class="project-header-title">
{{ project?.title }}
</h2>
</div>
<p
v-if="project?.description"
class="project-header-description"
>
{{ project.description }}
</p>
</div>
<div class="project-input-slot">
<slot />
</div>
<v-card
flat
class="project-sessions-list"
>
<v-list v-if="sessions.length > 0">
<v-list-item
v-for="session in sessions"
:key="session.session_id"
class="project-session-item"
rounded="lg"
@click="$emit('selectSession', session.session_id)"
>
<v-list-item-title>
{{ session.display_name || tm('conversation.newConversation') }}
</v-list-item-title>
<v-list-item-subtitle>
{{ formatDate(session.updated_at) }}
</v-list-item-subtitle>
<template #append>
<div class="session-actions">
<v-btn
icon="mdi-pencil"
size="x-small"
variant="text"
class="edit-session-btn"
@click.stop="$emit('editSessionTitle', session.session_id, session.display_name ?? '')"
/>
<v-btn
icon="mdi-delete"
size="x-small"
variant="text"
class="delete-session-btn"
color="error"
@click.stop="handleDeleteSession(session)"
/>
</div>
</template>
</v-list-item>
</v-list>
<div
v-else
class="no-sessions-in-project"
>
<v-icon
icon="mdi-message-off-outline"
size="large"
color="grey-lighten-1"
/>
<p>{{ tm('project.noSessions') }}</p>
</div>
</v-card>
</div>
</template>
<script setup lang="ts">

View File

@@ -1,80 +1,161 @@
<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-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-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%' }">
<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
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">
<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 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">
<v-btn
color="success"
prepend-icon="mdi-check"
:loading="savingSource"
:disabled="!isSourceModified"
variant="flat"
@click="saveProviderSource"
>
{{ tm('providerSources.save') }}
</v-btn>
</div>
<!-- 基础配置 -->
<div class="mb-4">
<AstrBotConfig v-if="basicSourceConfig" :iterable="basicSourceConfig" :metadata="configSchema"
metadataKey="provider" :is-editing="true" />
<AstrBotConfig
v-if="basicSourceConfig"
:iterable="basicSourceConfig"
:metadata="configSchema"
metadata-key="provider"
:is-editing="true"
/>
</div>
<!-- 高级配置 -->
<v-expansion-panels variant="accordion" class="mb-4">
<v-expansion-panel elevation="0" class="border rounded-lg">
<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" />
<AstrBotConfig
v-if="advancedSourceConfig"
:iterable="advancedSourceConfig"
:metadata="configSchema"
metadata-key="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" />
<ProviderModelsPanel
v-model:model-search="modelSearch"
:entries="filteredMergedModelEntries"
:available-count="availableModels.length"
: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
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>
<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>
@@ -83,38 +164,77 @@
</v-card>
<!-- 手动添加模型对话框 -->
<v-dialog v-model="showManualModelDialog" max-width="400">
<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-text-field
v-model="manualModelId"
:label="tm('models.manualDialogModelLabel')"
flat
variant="solo-filled"
autofocus
clearable
/>
<v-text-field
:model-value="manualProviderId"
flat
variant="solo-filled"
:label="tm('models.manualDialogPreviewLabel')"
persistent-hint
:hint="tm('models.manualDialogPreviewHint')"
/>
</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-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-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" />
<AstrBotConfig
v-if="providerEditData"
:iterable="providerEditData"
:metadata="configSchema"
metadata-key="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)">
<v-spacer />
<v-btn
variant="text"
:disabled="savingProviders.includes(providerEditData?.id)"
@click="showProviderEditDialog = false"
>
{{ tm('dialogs.config.cancel') }}
</v-btn>
<v-btn color="primary" @click="saveEditedProvider" :loading="savingProviders.includes(providerEditData?.id)">
<v-btn
color="primary"
:loading="savingProviders.includes(providerEditData?.id)"
@click="saveEditedProvider"
>
{{ tm('dialogs.config.save') }}
</v-btn>
</v-card-actions>

View File

@@ -1,60 +1,117 @@
<template>
<v-menu v-model="menuOpen" :close-on-content-click="false" location="top" @update:model-value="handleMenuToggle">
<template v-slot:activator="{ props: menuProps }">
<v-chip v-bind="menuProps" class="text-none provider-chip" variant="tonal" :size="chipSize">
<v-icon start size="14">mdi-creation</v-icon>
<span v-if="selectedProviderId">
{{ selectedProviderId }}
</span>
<span v-else>Model</span>
</v-chip>
</template>
<v-card class="provider-menu-card" min-width="280" max-width="400">
<v-card-text class="pa-2">
<v-text-field
v-model="searchQuery"
placeholder="Search..."
hide-details
variant="plain"
flat
density="compact"
prepend-inner-icon="mdi-magnify"
class="ml-2 mb-2 mr-2"
clearable
/>
<v-list density="compact" nav class="provider-menu-list">
<v-list-item v-for="provider in filteredProviders" :key="provider.id"
:active="selectedProviderId === provider.id" @click="selectProvider(provider)" rounded="lg"
class="provider-menu-item">
<v-list-item-title class="text-body-2">{{ provider.id }}</v-list-item-title>
<v-list-item-subtitle class="provider-subtitle">
<span class="model-name">{{ provider.model }}</span>
<span class="meta-icons">
<v-tooltip text="支持图像输入" location="top" v-if="supportsImageInput(provider)">
<template v-slot:activator="{ props: tipProps }">
<v-icon v-bind="tipProps" size="12" color="grey">mdi-eye-outline</v-icon>
</template>
</v-tooltip>
<v-tooltip text="支持工具调用" location="top" v-if="supportsToolCall(provider)">
<template v-slot:activator="{ props: tipProps }">
<v-icon v-bind="tipProps" size="12" color="grey">mdi-wrench</v-icon>
</template>
</v-tooltip>
<v-tooltip text="支持推理" location="top" v-if="supportsReasoning(provider)">
<template v-slot:activator="{ props: tipProps }">
<v-icon v-bind="tipProps" size="12" color="grey">mdi-brain</v-icon>
</template>
</v-tooltip>
</span>
</v-list-item-subtitle>
</v-list-item>
</v-list>
<div v-if="providerConfigs.length === 0" class="empty-hint">
No available models
</div>
</v-card-text>
</v-card>
</v-menu>
<v-menu
v-model="menuOpen"
:close-on-content-click="false"
location="top"
@update:model-value="handleMenuToggle"
>
<template #activator="{ props: menuProps }">
<v-chip
v-bind="menuProps"
class="text-none provider-chip"
variant="tonal"
:size="chipSize"
>
<v-icon
start
size="14"
>
mdi-creation
</v-icon>
<span v-if="selectedProviderId">
{{ selectedProviderId }}
</span>
<span v-else>Model</span>
</v-chip>
</template>
<v-card
class="provider-menu-card"
min-width="280"
max-width="400"
>
<v-card-text class="pa-2">
<v-text-field
v-model="searchQuery"
placeholder="Search..."
hide-details
variant="plain"
flat
density="compact"
prepend-inner-icon="mdi-magnify"
class="ml-2 mb-2 mr-2"
clearable
/>
<v-list
density="compact"
nav
class="provider-menu-list"
>
<v-list-item
v-for="provider in filteredProviders"
:key="provider.id"
:active="selectedProviderId === provider.id"
rounded="lg"
class="provider-menu-item"
@click="selectProvider(provider)"
>
<v-list-item-title class="text-body-2">
{{ provider.id }}
</v-list-item-title>
<v-list-item-subtitle class="provider-subtitle">
<span class="model-name">{{ provider.model }}</span>
<span class="meta-icons">
<v-tooltip
v-if="supportsImageInput(provider)"
text="支持图像输入"
location="top"
>
<template #activator="{ props: tipProps }">
<v-icon
v-bind="tipProps"
size="12"
color="grey"
>mdi-eye-outline</v-icon>
</template>
</v-tooltip>
<v-tooltip
v-if="supportsToolCall(provider)"
text="支持工具调用"
location="top"
>
<template #activator="{ props: tipProps }">
<v-icon
v-bind="tipProps"
size="12"
color="grey"
>mdi-wrench</v-icon>
</template>
</v-tooltip>
<v-tooltip
v-if="supportsReasoning(provider)"
text="支持推理"
location="top"
>
<template #activator="{ props: tipProps }">
<v-icon
v-bind="tipProps"
size="12"
color="grey"
>mdi-brain</v-icon>
</template>
</v-tooltip>
</span>
</v-list-item-subtitle>
</v-list-item>
</v-list>
<div
v-if="providerConfigs.length === 0"
class="empty-hint"
>
No available models
</div>
</v-card-text>
</v-card>
</v-menu>
</template>
<script setup lang="ts">

View File

@@ -1,18 +1,25 @@
<template>
<v-card class="standalone-chat-card" elevation="0" rounded="0">
<v-card
class="standalone-chat-card"
elevation="0"
rounded="0"
>
<v-card-text class="standalone-chat-container">
<div class="chat-layout">
<!-- 聊天内容区域 -->
<div class="chat-content-panel">
<MessageList
v-if="messages && messages.length > 0"
:messages="messages"
:isDark="isDark"
:isStreaming="isStreaming || isConvRunning"
@openImagePreview="openImagePreview"
ref="messageList"
:messages="messages"
:is-dark="isDark"
:is-streaming="isStreaming || isConvRunning"
@openImagePreview="openImagePreview"
/>
<div class="welcome-container fade-in" v-else>
<div
v-else
class="welcome-container fade-in"
>
<div class="welcome-title">
<span>Hello, I'm</span>
<span class="bot-name">AstrBot </span>
@@ -24,13 +31,14 @@
<!-- 输入区域 -->
<ChatInput
ref="chatInputRef"
v-model:prompt="prompt"
:stagedImagesUrl="stagedImagesUrl"
:stagedAudioUrl="stagedAudioUrl"
:staged-images-url="stagedImagesUrl"
:staged-audio-url="stagedAudioUrl"
:disabled="isStreaming"
:is-running="isStreaming || isConvRunning"
:enableStreaming="enableStreaming"
:isRecording="isRecording"
:enable-streaming="enableStreaming"
:is-recording="isRecording"
:session-id="currSessionId || null"
:current-session="getCurrentSession"
:config-id="configId"
@@ -43,8 +51,6 @@
@stopRecording="handleStopRecording"
@pasteImage="handlePaste"
@fileSelect="handleFileSelect"
@openLiveMode=""
ref="chatInputRef"
/>
</div>
</div>
@@ -52,8 +58,15 @@
</v-card>
<!-- 图片预览对话框 -->
<v-dialog v-model="imagePreviewDialog" max-width="90vw" max-height="90vh">
<v-card class="image-preview-card" elevation="8">
<v-dialog
v-model="imagePreviewDialog"
max-width="90vw"
max-height="90vh"
>
<v-card
class="image-preview-card"
elevation="8"
>
<v-card-title class="d-flex justify-space-between align-center pa-4">
<span>{{ t("core.common.imagePreview") }}</span>
<v-btn
@@ -63,7 +76,10 @@
/>
</v-card-title>
<v-card-text class="text-center pa-4">
<img :src="previewImageUrl" class="preview-image-large" />
<img
:src="previewImageUrl"
class="preview-image-large"
>
</v-card-text>
</v-card>
</v-dialog>

View File

@@ -1,29 +1,32 @@
<template>
<div class="welcome-container fade-in">
<div v-if="isLoading" class="loading-overlay-welcome">
<v-progress-circular
indeterminate
size="48"
width="4"
color="primary"
></v-progress-circular>
</div>
<template v-else>
<div class="welcome-content">
<div class="welcome-title">
<span class="bot-name-container">
<span class="bot-name-text">
Hello, I'm <span class="highlight-name">AstrBot</span>
</span>
<span class="bot-name-star"></span>
</span>
</div>
</div>
<div class="welcome-input">
<slot></slot>
</div>
</template>
<div class="welcome-container fade-in">
<div
v-if="isLoading"
class="loading-overlay-welcome"
>
<v-progress-circular
indeterminate
size="48"
width="4"
color="primary"
/>
</div>
<template v-else>
<div class="welcome-content">
<div class="welcome-title">
<span class="bot-name-container">
<span class="bot-name-text">
Hello, I'm <span class="highlight-name">AstrBot</span>
</span>
<span class="bot-name-star"></span>
</span>
</div>
</div>
<div class="welcome-input">
<slot />
</div>
</template>
</div>
</template>
<script setup lang="ts">

View File

@@ -1,20 +1,41 @@
<template>
<div v-if="refs && refs.used && refs.used.length > 0" class="refs-container" @click="handleClick">
<div class="refs-avatars">
<div v-for="(ref, refIdx) in refs.used.slice(0, 3)" :key="refIdx" class="ref-avatar"
:style="{ zIndex: 3 - refIdx }">
<img v-if="ref.favicon" :src="ref.favicon" class="ref-favicon"
@error="(e) => e.target.style.display = 'none'" />
<span v-else class="ref-initial">{{ getRefInitial(ref.title) }}</span>
</div>
<span v-if="refs.used.length > 3" class="refs-more">
+{{ refs.used.length - 3 }}
</span>
<span class="ml-2" style="color: gray;">
{{ tm('refs.sources') }}
</span>
</div>
<div
v-if="refs && refs.used && refs.used.length > 0"
class="refs-container"
@click="handleClick"
>
<div class="refs-avatars">
<div
v-for="(ref, refIdx) in refs.used.slice(0, 3)"
:key="refIdx"
class="ref-avatar"
:style="{ zIndex: 3 - refIdx }"
>
<img
v-if="ref.favicon"
:src="ref.favicon"
class="ref-favicon"
@error="(e) => e.target.style.display = 'none'"
>
<span
v-else
class="ref-initial"
>{{ getRefInitial(ref.title) }}</span>
</div>
<span
v-if="refs.used.length > 3"
class="refs-more"
>
+{{ refs.used.length - 3 }}
</span>
<span
class="ml-2"
style="color: gray;"
>
{{ tm('refs.sources') }}
</span>
</div>
</div>
</template>
<script>

View File

@@ -1,24 +1,41 @@
<template>
<div class="ipython-tool-block" :class="{ compact: !showHeader }">
<div v-if="displayExpanded" class="py-3 animate-fade-in">
<!-- Code Section -->
<div class="code-section">
<div v-if="shikiReady && code" class="code-highlighted"
v-html="highlightedCode"></div>
<pre v-else class="code-fallback"
:class="{ 'dark-theme': isDark }">{{ code || 'No code available' }}</pre>
</div>
<div
class="ipython-tool-block"
:class="{ compact: !showHeader }"
>
<div
v-if="displayExpanded"
class="py-3 animate-fade-in"
>
<!-- Code Section -->
<div class="code-section">
<div
v-if="shikiReady && code"
class="code-highlighted"
v-html="highlightedCode"
/>
<pre
v-else
class="code-fallback"
:class="{ 'dark-theme': isDark }"
>{{ code || 'No code available' }}</pre>
</div>
<!-- Result Section -->
<div v-if="result" class="result-section">
<div class="result-label">
{{ tm('ipython.output') }}:
</div>
<pre class="result-content"
:class="{ 'dark-theme': isDark }">{{ formattedResult }}</pre>
</div>
<!-- Result Section -->
<div
v-if="result"
class="result-section"
>
<div class="result-label">
{{ tm('ipython.output') }}:
</div>
<pre
class="result-content"
:class="{ 'dark-theme': isDark }"
>{{ formattedResult }}</pre>
</div>
</div>
</div>
</template>
<script setup>

View File

@@ -1,118 +1,207 @@
<template>
<template v-for="(renderPart, renderIndex) in getRenderParts(parts)" :key="renderPart.key">
<!-- Grouped Tool Calls (consecutive tool_call parts) -->
<div v-if="renderPart.type === 'tool_group'" class="tool-call-compact">
<transition-group name="tool-call-item" tag="div" class="tool-call-items">
<ToolCallItem v-for="(toolCall, tcIndex) in renderPart.toolCalls" :key="toolCall.id" :is-dark="isDark">
<template #label="{ expanded }">
<v-icon size="x-small" v-if="toolCall.name.includes('web_search') || toolCall.name.includes('tavily')">
mdi-web
</v-icon>
<v-icon size="x-small" v-else-if="toolCall.name === 'astrbot_execute_shell'">
mdi-console-line
</v-icon>
<v-icon size="x-small" v-else>
mdi-wrench
</v-icon>
{{ tm('actions.toolCallUsed', { name: toolCall.name }) }}
<span style="opacity: 0.6;">{{ toolCall.finished_ts ? formatDuration(toolCall.finished_ts -
toolCall.ts) : getElapsedTime(toolCall.ts) }}</span>
<v-icon size="x-small" class="tool-call-chevron" :class="{ rotated: expanded }">
mdi-chevron-right
</v-icon>
</template>
<template #details>
<div class="tool-call-detail-row">
<span class="detail-label">ID:</span>
<code class="detail-value">{{ toolCall.id }}</code>
</div>
<div class="tool-call-detail-row">
<span class="detail-label">Args:</span>
<pre class="detail-value detail-json">{{ formatToolArgs(toolCall.args) }}</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>
</div>
</template>
</ToolCallItem>
</transition-group>
</div>
<!-- iPython Tool Block -->
<ToolCallItem v-else-if="renderPart.type === 'ipython'" :is-dark="isDark" style="margin: 8px 0 4px;">
<template #label="{ expanded }">
<v-icon size="x-small">
mdi-code-json
</v-icon>
<span class="ipython-label">{{ tm('actions.pythonCodeAnalysis') }}</span>
<span style="opacity: 0.6;">{{ renderPart.toolCall.finished_ts ?
formatDuration(renderPart.toolCall.finished_ts -
renderPart.toolCall.ts) : getElapsedTime(renderPart.toolCall.ts) }}</span>
<v-icon size="small" class="ipython-icon" :class="{ rotated: expanded }">
mdi-chevron-right
</v-icon>
</template>
<template #details>
<IPythonToolBlock :tool-call="renderPart.toolCall" :is-dark="isDark" :show-header="false"
:force-expanded="true" />
</template>
<template
v-for="(renderPart, renderIndex) in getRenderParts(parts)"
:key="renderPart.key"
>
<!-- Grouped Tool Calls (consecutive tool_call parts) -->
<div
v-if="renderPart.type === 'tool_group'"
class="tool-call-compact"
>
<transition-group
name="tool-call-item"
tag="div"
class="tool-call-items"
>
<ToolCallItem
v-for="(toolCall, tcIndex) in renderPart.toolCalls"
:key="toolCall.id"
:is-dark="isDark"
>
<template #label="{ expanded }">
<v-icon
v-if="toolCall.name.includes('web_search') || toolCall.name.includes('tavily')"
size="x-small"
>
mdi-web
</v-icon>
<v-icon
v-else-if="toolCall.name === 'astrbot_execute_shell'"
size="x-small"
>
mdi-console-line
</v-icon>
<v-icon
v-else
size="x-small"
>
mdi-wrench
</v-icon>
{{ tm('actions.toolCallUsed', { name: toolCall.name }) }}
<span style="opacity: 0.6;">{{ toolCall.finished_ts ? formatDuration(toolCall.finished_ts -
toolCall.ts) : getElapsedTime(toolCall.ts) }}</span>
<v-icon
size="x-small"
class="tool-call-chevron"
:class="{ rotated: expanded }"
>
mdi-chevron-right
</v-icon>
</template>
<template #details>
<div class="tool-call-detail-row">
<span class="detail-label">ID:</span>
<code class="detail-value">{{ toolCall.id }}</code>
</div>
<div class="tool-call-detail-row">
<span class="detail-label">Args:</span>
<pre class="detail-value detail-json">{{ formatToolArgs(toolCall.args) }}</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>
</div>
</template>
</ToolCallItem>
</transition-group>
</div>
<!-- Text (Markdown) -->
<MarkdownRender
v-else-if="renderPart.part.type === 'plain' && renderPart.part.text && renderPart.part.text.trim()"
:key="`${renderPart.key}-${isDark ? 'dark' : 'light'}`"
custom-id="message-list" :custom-html-tags="['ref']" :content="renderPart.part.text" :typewriter="false"
class="markdown-content" :is-dark="isDark" />
<!-- iPython Tool Block -->
<ToolCallItem
v-else-if="renderPart.type === 'ipython'"
:is-dark="isDark"
style="margin: 8px 0 4px;"
>
<template #label="{ expanded }">
<v-icon size="x-small">
mdi-code-json
</v-icon>
<span class="ipython-label">{{ tm('actions.pythonCodeAnalysis') }}</span>
<span style="opacity: 0.6;">{{ renderPart.toolCall.finished_ts ?
formatDuration(renderPart.toolCall.finished_ts -
renderPart.toolCall.ts) : getElapsedTime(renderPart.toolCall.ts) }}</span>
<v-icon
size="small"
class="ipython-icon"
:class="{ rotated: expanded }"
>
mdi-chevron-right
</v-icon>
</template>
<template #details>
<IPythonToolBlock
:tool-call="renderPart.toolCall"
:is-dark="isDark"
:show-header="false"
:force-expanded="true"
/>
</template>
</ToolCallItem>
<!-- Image -->
<div v-else-if="renderPart.part.type === 'image' && renderPart.part.embedded_url" class="embedded-images">
<div class="embedded-image">
<img :src="renderPart.part.embedded_url" class="bot-embedded-image"
@click="emitOpenImage(renderPart.part.embedded_url)" />
</div>
</div>
<!-- Text (Markdown) -->
<MarkdownRender
v-else-if="renderPart.part.type === 'plain' && renderPart.part.text && renderPart.part.text.trim()"
:key="`${renderPart.key}-${isDark ? 'dark' : 'light'}`"
custom-id="message-list"
:custom-html-tags="['ref']"
:content="renderPart.part.text"
:typewriter="false"
class="markdown-content"
:is-dark="isDark"
/>
<!-- Audio -->
<div v-else-if="renderPart.part.type === 'record' && renderPart.part.embedded_url" class="embedded-audio">
<audio controls class="audio-player">
<source :src="renderPart.part.embedded_url" type="audio/wav">
{{ t('messages.errors.browser.audioNotSupported') }}
</audio>
</div>
<!-- Image -->
<div
v-else-if="renderPart.part.type === 'image' && renderPart.part.embedded_url"
class="embedded-images"
>
<div class="embedded-image">
<img
:src="renderPart.part.embedded_url"
class="bot-embedded-image"
@click="emitOpenImage(renderPart.part.embedded_url)"
>
</div>
</div>
<!-- Files -->
<div v-else-if="renderPart.part.type === 'file' && renderPart.part.embedded_file" class="embedded-files">
<div class="embedded-file">
<a v-if="renderPart.part.embedded_file.url" :href="renderPart.part.embedded_file.url"
:download="renderPart.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">{{ renderPart.part.embedded_file.filename }}</span>
</a>
<a v-else @click="emitDownloadFile(renderPart.part.embedded_file)" 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">{{ renderPart.part.embedded_file.filename }}</span>
<v-icon v-if="downloadingFiles?.has(renderPart.part.embedded_file.attachment_id)" size="small"
class="download-icon">mdi-loading mdi-spin</v-icon>
<v-icon v-else size="small" class="download-icon">mdi-download</v-icon>
</a>
</div>
</div>
</template>
<!-- Audio -->
<div
v-else-if="renderPart.part.type === 'record' && renderPart.part.embedded_url"
class="embedded-audio"
>
<audio
controls
class="audio-player"
>
<source
:src="renderPart.part.embedded_url"
type="audio/wav"
>
{{ t('messages.errors.browser.audioNotSupported') }}
</audio>
</div>
<!-- Files -->
<div
v-else-if="renderPart.part.type === 'file' && renderPart.part.embedded_file"
class="embedded-files"
>
<div class="embedded-file">
<a
v-if="renderPart.part.embedded_file.url"
:href="renderPart.part.embedded_file.url"
:download="renderPart.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">{{ renderPart.part.embedded_file.filename }}</span>
</a>
<a
v-else
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)'
} : {}"
@click="emitDownloadFile(renderPart.part.embedded_file)"
>
<v-icon
size="small"
class="file-icon"
:style="isDark ? { color: 'var(--v-theme-secondary)' } : {}"
>mdi-file-document-outline</v-icon>
<span class="file-name">{{ renderPart.part.embedded_file.filename }}</span>
<v-icon
v-if="downloadingFiles?.has(renderPart.part.embedded_file.attachment_id)"
size="small"
class="download-icon"
>mdi-loading mdi-spin</v-icon>
<v-icon
v-else
size="small"
class="download-icon"
>mdi-download</v-icon>
</a>
</div>
</div>
</template>
</template>
<script setup>

View File

@@ -1,18 +1,37 @@
<template>
<div class="reasoning-block" :class="{ 'reasoning-block--dark': isDark }">
<div class="reasoning-header" @click="toggleExpanded">
<v-icon size="small" class="reasoning-icon" :class="{ 'rotate-90': isExpanded }">
mdi-chevron-right
</v-icon>
<span class="reasoning-title">
{{ tm('reasoning.thinking') }}
</span>
</div>
<div v-if="isExpanded" class="reasoning-content animate-fade-in">
<MarkdownRender :key="`reasoning-${isDark ? 'dark' : 'light'}`" :content="reasoning" class="reasoning-text markdown-content"
:typewriter="false" :is-dark="isDark" :style="isDark ? { opacity: '0.85' } : {}" />
</div>
<div
class="reasoning-block"
:class="{ 'reasoning-block--dark': isDark }"
>
<div
class="reasoning-header"
@click="toggleExpanded"
>
<v-icon
size="small"
class="reasoning-icon"
:class="{ 'rotate-90': isExpanded }"
>
mdi-chevron-right
</v-icon>
<span class="reasoning-title">
{{ tm('reasoning.thinking') }}
</span>
</div>
<div
v-if="isExpanded"
class="reasoning-content animate-fade-in"
>
<MarkdownRender
:key="`reasoning-${isDark ? 'dark' : 'light'}`"
:content="reasoning"
class="reasoning-text markdown-content"
:typewriter="false"
:is-dark="isDark"
:style="isDark ? { opacity: '0.85' } : {}"
/>
</div>
</div>
</template>
<script setup>

View File

@@ -1,12 +1,28 @@
<template>
<v-chip v-if="domain" class="ref-chip" size="x-small" variant="flat"
:style="chipStyle" :href="url"
target="_blank" clickable>
<v-icon start size="x-small" color>mdi-link-variant</v-icon>
<span>{{ domain }}</span>
</v-chip>
<span v-else class="ref-fallback" :style="fallbackStyle">{{ 'site' }}</span>
<v-chip
v-if="domain"
class="ref-chip"
size="x-small"
variant="flat"
:style="chipStyle"
:href="url"
target="_blank"
clickable
>
<v-icon
start
size="x-small"
color
>
mdi-link-variant
</v-icon>
<span>{{ domain }}</span>
</v-chip>
<span
v-else
class="ref-fallback"
:style="fallbackStyle"
>{{ 'site' }}</span>
</template>
<script setup>

View File

@@ -1,28 +1,66 @@
<template>
<transition name="slide-left">
<div v-if="isOpen" class="refs-sidebar">
<div class="sidebar-header">
<h3 class="sidebar-title">{{ tm('refs.title') }}</h3>
<v-btn icon="mdi-close" size="small" variant="text" @click="close"></v-btn>
</div>
<transition name="slide-left">
<div
v-if="isOpen"
class="refs-sidebar"
>
<div class="sidebar-header">
<h3 class="sidebar-title">
{{ tm('refs.title') }}
</h3>
<v-btn
icon="mdi-close"
size="small"
variant="text"
@click="close"
/>
</div>
<div class="refs-list">
<div v-for="(ref, index) in refs?.used || []" :key="index" class="ref-item" @click="openLink(ref.url)">
<div class="ref-item-icon">
<img v-if="ref.favicon" :src="ref.favicon" class="ref-item-favicon"
@error="(e) => e.target.style.display = 'none'" />
<div v-else class="ref-item-initial">{{ getRefInitial(ref.title) }}</div>
</div>
<div class="ref-item-content">
<div class="ref-item-title">{{ ref.title }}</div>
<div class="ref-item-url">{{ formatUrl(ref.url) }}</div>
<div v-if="ref.snippet" class="ref-item-snippet">{{ ref.snippet }}</div>
</div>
<v-icon size="small" class="ref-item-arrow">mdi-open-in-new</v-icon>
</div>
<div class="refs-list">
<div
v-for="(ref, index) in refs?.used || []"
:key="index"
class="ref-item"
@click="openLink(ref.url)"
>
<div class="ref-item-icon">
<img
v-if="ref.favicon"
:src="ref.favicon"
class="ref-item-favicon"
@error="(e) => e.target.style.display = 'none'"
>
<div
v-else
class="ref-item-initial"
>
{{ getRefInitial(ref.title) }}
</div>
</div>
<div class="ref-item-content">
<div class="ref-item-title">
{{ ref.title }}
</div>
<div class="ref-item-url">
{{ formatUrl(ref.url) }}
</div>
<div
v-if="ref.snippet"
class="ref-item-snippet"
>
{{ ref.snippet }}
</div>
</div>
<v-icon
size="small"
class="ref-item-arrow"
>
mdi-open-in-new
</v-icon>
</div>
</transition>
</div>
</div>
</transition>
</template>
<script>

View File

@@ -1,59 +1,99 @@
<template>
<div class="tool-call-card" :class="{ 'is-dark': isDark, 'expanded': isExpanded }" :style="isDark ? {
backgroundColor: 'rgba(40, 60, 100, 0.4)',
borderColor: 'rgba(100, 140, 200, 0.4)'
} : {}">
<!-- Header -->
<div class="tool-call-header" :class="{ 'is-dark': isDark }" @click="toggleExpanded">
<v-icon size="small" class="tool-call-expand-icon" :class="{ 'expanded': isExpanded }">
mdi-chevron-right
</v-icon>
<v-icon size="small" class="tool-call-icon">mdi-wrench-outline</v-icon>
<div class="tool-call-info">
<span class="tool-call-name">{{ toolCall.name }}</span>
</div>
<span class="tool-call-status"
:class="{ 'status-running': !toolCall.finished_ts, 'status-finished': toolCall.finished_ts }">
<template v-if="toolCall.finished_ts">
<v-icon size="x-small" class="status-icon">mdi-check-circle</v-icon>
{{ formatDuration(toolCall.finished_ts - toolCall.ts) }}
</template>
<template v-else>
<v-icon size="x-small" class="status-icon spinning">mdi-loading</v-icon>
{{ elapsedTime }}
</template>
</span>
</div>
<!-- Details -->
<div v-if="isExpanded" class="tool-call-details" :style="isDark ? {
borderTopColor: 'rgba(100, 140, 200, 0.3)',
backgroundColor: 'rgba(30, 45, 70, 0.5)'
} : {}">
<!-- ID -->
<div class="tool-call-detail-row">
<span class="detail-label">ID:</span>
<code class="detail-value" :style="isDark ? { backgroundColor: 'transparent' } : {}">
{{ toolCall.id }}
</code>
</div>
<!-- Args -->
<div class="tool-call-detail-row">
<span class="detail-label">Args:</span>
<pre class="detail-value detail-json" :style="isDark ? { backgroundColor: 'transparent' } : {}">{{
JSON.stringify(toolCall.args, null, 2) }}</pre>
</div>
<!-- Result -->
<div v-if="toolCall.result" class="tool-call-detail-row">
<span class="detail-label">Result:</span>
<pre class="detail-value detail-json detail-result"
:style="isDark ? { backgroundColor: 'transparent' } : {}">{{
formattedResult }}</pre>
</div>
</div>
<div
class="tool-call-card"
:class="{ 'is-dark': isDark, 'expanded': isExpanded }"
:style="isDark ? {
backgroundColor: 'rgba(40, 60, 100, 0.4)',
borderColor: 'rgba(100, 140, 200, 0.4)'
} : {}"
>
<!-- Header -->
<div
class="tool-call-header"
:class="{ 'is-dark': isDark }"
@click="toggleExpanded"
>
<v-icon
size="small"
class="tool-call-expand-icon"
:class="{ 'expanded': isExpanded }"
>
mdi-chevron-right
</v-icon>
<v-icon
size="small"
class="tool-call-icon"
>
mdi-wrench-outline
</v-icon>
<div class="tool-call-info">
<span class="tool-call-name">{{ toolCall.name }}</span>
</div>
<span
class="tool-call-status"
:class="{ 'status-running': !toolCall.finished_ts, 'status-finished': toolCall.finished_ts }"
>
<template v-if="toolCall.finished_ts">
<v-icon
size="x-small"
class="status-icon"
>mdi-check-circle</v-icon>
{{ formatDuration(toolCall.finished_ts - toolCall.ts) }}
</template>
<template v-else>
<v-icon
size="x-small"
class="status-icon spinning"
>mdi-loading</v-icon>
{{ elapsedTime }}
</template>
</span>
</div>
<!-- Details -->
<div
v-if="isExpanded"
class="tool-call-details"
:style="isDark ? {
borderTopColor: 'rgba(100, 140, 200, 0.3)',
backgroundColor: 'rgba(30, 45, 70, 0.5)'
} : {}"
>
<!-- ID -->
<div class="tool-call-detail-row">
<span class="detail-label">ID:</span>
<code
class="detail-value"
:style="isDark ? { backgroundColor: 'transparent' } : {}"
>
{{ toolCall.id }}
</code>
</div>
<!-- Args -->
<div class="tool-call-detail-row">
<span class="detail-label">Args:</span>
<pre
class="detail-value detail-json"
:style="isDark ? { backgroundColor: 'transparent' } : {}"
>{{
JSON.stringify(toolCall.args, null, 2) }}</pre>
</div>
<!-- Result -->
<div
v-if="toolCall.result"
class="tool-call-detail-row"
>
<span class="detail-label">Result:</span>
<pre
class="detail-value detail-json detail-result"
:style="isDark ? { backgroundColor: 'transparent' } : {}"
>{{
formattedResult }}</pre>
</div>
</div>
</div>
</template>
<script setup>

View File

@@ -1,17 +1,28 @@
<template>
<div class="tool-call-item">
<div class="tool-call-line" role="button" tabindex="0"
@click="toggleExpanded"
@keydown.enter="toggleExpanded"
@keydown.space.prevent="toggleExpanded">
<slot name="label" :expanded="isExpanded" />
</div>
<transition name="tool-call-fade">
<div v-if="isExpanded" class="tool-call-inline-details" :class="{ 'is-dark': isDark }">
<slot name="details" />
</div>
</transition>
<div class="tool-call-item">
<div
class="tool-call-line"
role="button"
tabindex="0"
@click="toggleExpanded"
@keydown.enter="toggleExpanded"
@keydown.space.prevent="toggleExpanded"
>
<slot
name="label"
:expanded="isExpanded"
/>
</div>
<transition name="tool-call-fade">
<div
v-if="isExpanded"
class="tool-call-inline-details"
:class="{ 'is-dark': isDark }"
>
<slot name="details" />
</div>
</transition>
</div>
</template>
<script setup>

View File

@@ -1,45 +1,75 @@
<template>
<div :class="$vuetify.display.mobile ? '' : 'd-flex'">
<v-tabs v-model="tab" :direction="$vuetify.display.mobile ? 'horizontal' : 'vertical'"
:align-tabs="$vuetify.display.mobile ? 'left' : 'start'" color="deep-purple-accent-4" class="config-tabs">
<v-tab v-for="section in visibleSections" :key="section.key" :value="section.key"
style="font-weight: 1000; font-size: 15px">
{{ tm(section.value['name']) }}
</v-tab>
</v-tabs>
<v-tabs-window v-model="tab" class="config-tabs-window" :style="readonly ? 'pointer-events: none; opacity: 0.6;' : ''">
<v-tabs-window-item v-for="section in visibleSections" :key="section.key" :value="section.key">
<v-container fluid>
<div v-for="(val2, key2, index2) in section.value['metadata']" :key="key2">
<!-- Support both traditional and JSON selector metadata -->
<AstrBotConfigV4
:metadata="{ [key2]: section.value['metadata'][key2] }"
:iterable="config_data"
:metadataKey="key2"
:search-keyword="searchKeyword"
>
</AstrBotConfigV4>
</div>
</v-container>
</v-tabs-window-item>
<div :class="$vuetify.display.mobile ? '' : 'd-flex'">
<v-tabs
v-model="tab"
:direction="$vuetify.display.mobile ? 'horizontal' : 'vertical'"
:align-tabs="$vuetify.display.mobile ? 'left' : 'start'"
color="deep-purple-accent-4"
class="config-tabs"
>
<v-tab
v-for="section in visibleSections"
:key="section.key"
:value="section.key"
style="font-weight: 1000; font-size: 15px"
>
{{ tm(section.value['name']) }}
</v-tab>
</v-tabs>
<v-tabs-window
v-model="tab"
class="config-tabs-window"
:style="readonly ? 'pointer-events: none; opacity: 0.6;' : ''"
>
<v-tabs-window-item
v-for="section in visibleSections"
:key="section.key"
:value="section.key"
>
<v-container fluid>
<div
v-for="(val2, key2, index2) in section.value['metadata']"
:key="key2"
>
<!-- Support both traditional and JSON selector metadata -->
<AstrBotConfigV4
:metadata="{ [key2]: section.value['metadata'][key2] }"
:iterable="config_data"
:metadata-key="key2"
:search-keyword="searchKeyword"
/>
</div>
</v-container>
</v-tabs-window-item>
<div style="margin-left: 16px; padding-bottom: 16px">
<small>{{ tm('help.helpPrefix') }}
<a href="https://astrbot.app/" target="_blank">{{ tm('help.documentation') }}</a>
{{ tm('help.helpMiddle') }}
<a href="https://qm.qq.com/cgi-bin/qm/qr?k=EYGsuUTfe00_iOu9JTXS7_TEpMkXOvwv&jump_from=webapi&authKey=uUEMKCROfsseS+8IzqPjzV3y1tzy4AkykwTib2jNkOFdzezF9s9XknqnIaf3CDft"
target="_blank">{{ tm('help.support') }}</a>{{ tm('help.helpSuffix') }}
</small>
</div>
</v-tabs-window>
</div>
<v-container v-if="visibleSections.length === 0" fluid class="px-0">
<v-alert type="info" variant="tonal">
{{ tm('search.noResult') }}
</v-alert>
</v-container>
<div style="margin-left: 16px; padding-bottom: 16px">
<small>{{ tm('help.helpPrefix') }}
<a
href="https://astrbot.app/"
target="_blank"
>{{ tm('help.documentation') }}</a>
{{ tm('help.helpMiddle') }}
<a
href="https://qm.qq.com/cgi-bin/qm/qr?k=EYGsuUTfe00_iOu9JTXS7_TEpMkXOvwv&jump_from=webapi&authKey=uUEMKCROfsseS+8IzqPjzV3y1tzy4AkykwTib2jNkOFdzezF9s9XknqnIaf3CDft"
target="_blank"
>{{ tm('help.support') }}</a>{{ tm('help.helpSuffix') }}
</small>
</div>
</v-tabs-window>
</div>
<v-container
v-if="visibleSections.length === 0"
fluid
class="px-0"
>
<v-alert
type="info"
variant="tonal"
>
{{ tm('search.noResult') }}
</v-alert>
</v-container>
</template>
<script>

View File

@@ -1,12 +1,22 @@
<template>
<v-dialog v-model="isOpen" max-width="480" persistent>
<v-dialog
v-model="isOpen"
max-width="480"
persistent
>
<v-card>
<v-card-title class="dialog-title d-flex align-center justify-space-between">
<span>{{ title }}</span>
<v-btn icon="mdi-close" variant="text" @click="handleClose"></v-btn>
<v-btn
icon="mdi-close"
variant="text"
@click="handleClose"
/>
</v-card-title>
<v-card-text>
<div class="message-text">{{ message }}</div>
<div class="message-text">
{{ message }}
</div>
<div class="action-hints">
<span class="hint-item">{{ confirmHint }}</span>
<span class="hint-item">{{ cancelHint }}</span>
@@ -14,9 +24,20 @@
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="gray" @click="handleCancel">{{ t('core.common.dialog.cancelButton') }}</v-btn>
<v-btn color="red" @click="handleConfirm" class="confirm-button">{{ t('core.common.dialog.confirmButton') }}</v-btn>
<v-spacer />
<v-btn
color="gray"
@click="handleCancel"
>
{{ t('core.common.dialog.cancelButton') }}
</v-btn>
<v-btn
color="red"
class="confirm-button"
@click="handleConfirm"
>
{{ t('core.common.dialog.confirmButton') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>

View File

@@ -81,7 +81,7 @@ const handleInstall = (plugin) => {
border-radius: 8px;
object-fit: cover;
"
/>
>
</div>
<div
@@ -108,23 +108,25 @@ const handleInstall = (plugin) => {
plugin.display_name?.length
? plugin.display_name
: showPluginFullName
? plugin.name
: plugin.trimmedName
? plugin.name
: plugin.trimmedName
}}
</span>
</div>
<div class="d-flex align-center" style="gap: 4px; margin-bottom: 6px">
<div
class="d-flex align-center"
style="gap: 4px; margin-bottom: 6px"
>
<v-icon
icon="mdi-account"
size="x-small"
style="color: rgba(var(--v-theme-on-surface), 0.5)"
></v-icon>
/>
<a
v-if="plugin?.social_link"
:href="plugin.social_link"
target="_blank"
@click.stop
class="text-subtitle-2 font-weight-medium"
style="
text-decoration: none;
@@ -133,6 +135,7 @@ const handleInstall = (plugin) => {
overflow: hidden;
text-overflow: ellipsis;
"
@click.stop
>
{{ plugin.author }}
</a>
@@ -156,7 +159,7 @@ const handleInstall = (plugin) => {
icon="mdi-source-branch"
size="x-small"
style="margin-right: 2px"
></v-icon>
/>
<span>{{ plugin.version }}</span>
</div>
</div>
@@ -186,7 +189,10 @@ const handleInstall = (plugin) => {
/>
</div>
<div class="d-flex align-center" style="gap: 8px; margin-top: auto">
<div
class="d-flex align-center"
style="gap: 8px; margin-top: auto"
>
<div
v-if="plugin.stars !== undefined"
class="d-flex align-center text-subtitle-2"
@@ -196,7 +202,7 @@ const handleInstall = (plugin) => {
icon="mdi-star"
size="x-small"
style="margin-right: 2px"
></v-icon>
/>
<span>{{ plugin.stars }}</span>
</div>
<div
@@ -208,7 +214,7 @@ const handleInstall = (plugin) => {
icon="mdi-clock-outline"
size="x-small"
style="margin-right: 2px"
></v-icon>
/>
<span>{{ new Date(plugin.updated_at).toLocaleString() }}</span>
</div>
</div>
@@ -229,8 +235,12 @@ const handleInstall = (plugin) => {
>
{{ tag === "danger" ? tm("tags.danger") : tag }}
</v-chip>
<v-menu v-if="plugin.tags && plugin.tags.length > 2" open-on-hover offset-y>
<template v-slot:activator="{ props: menuProps }">
<v-menu
v-if="plugin.tags && plugin.tags.length > 2"
open-on-hover
offset-y
>
<template #activator="{ props: menuProps }">
<v-chip
v-bind="menuProps"
color="grey"
@@ -242,14 +252,21 @@ const handleInstall = (plugin) => {
</v-chip>
</template>
<v-list density="compact">
<v-list-item v-for="tag in plugin.tags.slice(2)" :key="tag">
<v-chip :color="tag === 'danger' ? 'error' : 'primary'" label size="small">
<v-list-item
v-for="tag in plugin.tags.slice(2)"
:key="tag"
>
<v-chip
:color="tag === 'danger' ? 'error' : 'primary'"
label
size="small"
>
{{ tag === "danger" ? tm("tags.danger") : tag }}
</v-chip>
</v-list-item>
</v-list>
</v-menu>
<v-spacer></v-spacer>
<v-spacer />
<v-btn
v-if="plugin?.repo"
color="secondary"
@@ -260,21 +277,31 @@ const handleInstall = (plugin) => {
target="_blank"
style="height: 32px"
>
<v-icon icon="mdi-github" start size="small"></v-icon>
<v-icon
icon="mdi-github"
start
size="small"
/>
{{ tm("buttons.viewRepo") }}
</v-btn>
<v-btn
v-if="!plugin?.installed"
color="primary"
size="small"
@click="handleInstall(plugin)"
variant="flat"
class="market-action-btn"
style="height: 32px"
@click="handleInstall(plugin)"
>
{{ tm("buttons.install") }}
</v-btn>
<v-chip v-else color="success" size="x-small" label style="height: 20px">
<v-chip
v-else
color="success"
size="x-small"
label
style="height: 20px"
>
{{ tm("status.installed") }}
</v-chip>
</v-card-actions>

View File

@@ -1,62 +1,130 @@
<template>
<div class="tools-page">
<v-container fluid class="pa-0" elevation="0">
<v-container
fluid
class="pa-0"
elevation="0"
>
<!-- 页面标题 -->
<v-row class="d-flex justify-space-between align-center px-4 py-3 pb-8">
<div>
<v-btn color="success" prepend-icon="mdi-plus" class="me-2" variant="tonal"
@click="showMcpServerDialog = true" >
<v-btn
color="success"
prepend-icon="mdi-plus"
class="me-2"
variant="tonal"
@click="showMcpServerDialog = true"
>
{{ tm('mcpServers.buttons.add') }}
</v-btn>
<v-btn color="success" prepend-icon="mdi-refresh" variant="tonal" @click="showSyncMcpServerDialog = true"
>
<v-btn
color="success"
prepend-icon="mdi-refresh"
variant="tonal"
@click="showSyncMcpServerDialog = true"
>
{{ tm('mcpServers.buttons.sync') }}
</v-btn>
</div>
</v-row>
<!-- MCP 服务器部分 -->
<div v-if="mcpServers.length === 0" class="text-center pa-8">
<v-icon size="64" color="grey-lighten-1">mdi-server-off</v-icon>
<p class="text-grey mt-4">{{ tm('mcpServers.empty') }}</p>
<div
v-if="mcpServers.length === 0"
class="text-center pa-8"
>
<v-icon
size="64"
color="grey-lighten-1"
>
mdi-server-off
</v-icon>
<p class="text-grey mt-4">
{{ tm('mcpServers.empty') }}
</p>
</div>
<v-row v-else>
<v-col v-for="(server, index) in mcpServers || []" :key="index" cols="12" md="6" lg="4" xl="3">
<item-card style="background-color: rgb(var(--v-theme-mcpCardBg));" :item="server" title-field="name"
enabled-field="active" @toggle-enabled="updateServerStatus" @delete="deleteServer" @edit="editServer">
<template v-slot:item-details="{ item }">
<v-col
v-for="(server, index) in mcpServers || []"
:key="index"
cols="12"
md="6"
lg="4"
xl="3"
>
<item-card
style="background-color: rgb(var(--v-theme-mcpCardBg));"
:item="server"
title-field="name"
enabled-field="active"
@toggle-enabled="updateServerStatus"
@delete="deleteServer"
@edit="editServer"
>
<template #item-details="{ item }">
<div class="d-flex align-center mb-2">
<v-icon size="small" color="grey" class="me-2">mdi-file-code</v-icon>
<span class="text-caption text-medium-emphasis text-truncate" :title="getServerConfigSummary(item)">
<v-icon
size="small"
color="grey"
class="me-2"
>
mdi-file-code
</v-icon>
<span
class="text-caption text-medium-emphasis text-truncate"
:title="getServerConfigSummary(item)"
>
{{ getServerConfigSummary(item) }}
</span>
</div>
<div class="d-flex" style="gap: 8px;">
<div
class="d-flex"
style="gap: 8px;"
>
<div>
<div v-if="item.tools && item.tools.length > 0">
<div class="d-flex align-center mb-1">
<v-icon size="small" color="grey" class="me-2">mdi-tools</v-icon>
<v-icon
size="small"
color="grey"
class="me-2"
>
mdi-tools
</v-icon>
<v-dialog max-width="600px">
<template v-slot:activator="{ props: listToolsProps }">
<span class="text-caption text-medium-emphasis cursor-pointer" v-bind="listToolsProps"
style="text-decoration: underline;">
<template #activator="{ props: listToolsProps }">
<span
class="text-caption text-medium-emphasis cursor-pointer"
v-bind="listToolsProps"
style="text-decoration: underline;"
>
{{ tm('mcpServers.status.availableTools', { count: item.tools.length }) }} ({{ item.tools.length }})
</span>
</template>
<template v-slot:default="{ isActive }">
<template #default="{ isActive }">
<v-card style="padding: 16px;">
<v-card-title class="d-flex align-center">
<span>{{ tm('mcpServers.status.availableTools') }}</span>
</v-card-title>
<v-card-text>
<ul>
<li v-for="(tool, idx) in item.tools" :key="idx" style="margin: 8px 0px;">{{ tool }}</li>
<li
v-for="(tool, idx) in item.tools"
:key="idx"
style="margin: 8px 0px;"
>
{{ tool }}
</li>
</ul>
</v-card-text>
<v-card-actions class="d-flex justify-end">
<v-btn variant="text" color="primary" @click="isActive.value = false">
<v-btn
variant="text"
color="primary"
@click="isActive.value = false"
>
Close
</v-btn>
</v-card-actions>
@@ -65,13 +133,29 @@
</v-dialog>
</div>
</div>
<div v-else class="text-caption text-medium-emphasis">
<v-icon size="small" color="warning" class="me-1">mdi-alert-circle</v-icon>
<div
v-else
class="text-caption text-medium-emphasis"
>
<v-icon
size="small"
color="warning"
class="me-1"
>
mdi-alert-circle
</v-icon>
{{ tm('mcpServers.status.noTools') }}
</div>
</div>
<div v-if="mcpServerUpdateLoaders[item.name]" class="text-caption text-medium-emphasis">
<v-progress-circular indeterminate color="primary" size="16"></v-progress-circular>
<div
v-if="mcpServerUpdateLoaders[item.name]"
class="text-caption text-medium-emphasis"
>
<v-progress-circular
indeterminate
color="primary"
size="16"
/>
</div>
</div>
</template>
@@ -81,69 +165,129 @@
</v-container>
<!-- 添加/编辑 MCP 服务器对话框 -->
<v-dialog v-model="showMcpServerDialog" max-width="750px">
<v-dialog
v-model="showMcpServerDialog"
max-width="750px"
>
<v-card>
<v-card-title class="pa-4 pl-6">
<v-icon class="me-2">{{ isEditMode ? 'mdi-pencil' : 'mdi-plus' }}</v-icon>
<v-icon class="me-2">
{{ isEditMode ? 'mdi-pencil' : 'mdi-plus' }}
</v-icon>
<span>{{ isEditMode ? tm('dialogs.addServer.editTitle') : tm('dialogs.addServer.title') }}</span>
</v-card-title>
<v-card-text class="py-4">
<v-form @submit.prevent="saveServer" ref="form">
<v-text-field v-model="currentServer.name" :label="tm('dialogs.addServer.fields.name')" variant="outlined"
:rules="[v => !!v || tm('dialogs.addServer.fields.nameRequired')]" required class="mb-3"></v-text-field>
<v-form
ref="form"
@submit.prevent="saveServer"
>
<v-text-field
v-model="currentServer.name"
:label="tm('dialogs.addServer.fields.name')"
variant="outlined"
:rules="[v => !!v || tm('dialogs.addServer.fields.nameRequired')]"
required
class="mb-3"
/>
<div class="mb-2 d-flex align-center">
<span class="text-subtitle-1">{{ tm('dialogs.addServer.fields.config') }}</span>
<v-spacer></v-spacer>
<v-btn size="small" color="primary" variant="tonal" @click="setConfigTemplate('stdio')" class="me-1">
<v-spacer />
<v-btn
size="small"
color="primary"
variant="tonal"
class="me-1"
@click="setConfigTemplate('stdio')"
>
{{ tm('mcpServers.buttons.useTemplateStdio') }}
</v-btn>
<v-btn size="small" color="primary" variant="tonal" @click="setConfigTemplate('streamable_http')"
class="me-1">
<v-btn
size="small"
color="primary"
variant="tonal"
class="me-1"
@click="setConfigTemplate('streamable_http')"
>
{{ tm('mcpServers.buttons.useTemplateStreamableHttp') }}
</v-btn>
<v-btn size="small" color="primary" variant="tonal" @click="setConfigTemplate('sse')" class="me-1">
<v-btn
size="small"
color="primary"
variant="tonal"
class="me-1"
@click="setConfigTemplate('sse')"
>
{{ tm('mcpServers.buttons.useTemplateSse') }}
</v-btn>
</div>
<small style="color: grey">*{{ tm('dialogs.addServer.tips.timeoutConfig') }}</small>
<div class="monaco-container" style="margin-top: 16px;">
<VueMonacoEditor v-model:value="serverConfigJson" theme="vs-dark" language="json" :options="{
minimap: {
enabled: false
},
scrollBeyondLastLine: false,
automaticLayout: true,
lineNumbers: 'on',
roundedSelection: true,
tabSize: 2
}" @change="validateJson" />
<div
class="monaco-container"
style="margin-top: 16px;"
>
<VueMonacoEditor
v-model:value="serverConfigJson"
theme="vs-dark"
language="json"
:options="{
minimap: {
enabled: false
},
scrollBeyondLastLine: false,
automaticLayout: true,
lineNumbers: 'on',
roundedSelection: true,
tabSize: 2
}"
@change="validateJson"
/>
</div>
<div v-if="jsonError" class="mt-2 text-error">
<v-icon color="error" size="small" class="me-1">mdi-alert-circle</v-icon>
<div
v-if="jsonError"
class="mt-2 text-error"
>
<v-icon
color="error"
size="small"
class="me-1"
>
mdi-alert-circle
</v-icon>
<span>{{ jsonError }}</span>
</div>
</v-form>
<div style="margin-top: 8px;">
<small>{{ addServerDialogMessage }}</small>
</div>
</v-card-text>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="closeServerDialog" :disabled="loading">
<v-spacer />
<v-btn
variant="text"
:disabled="loading"
@click="closeServerDialog"
>
{{ tm('dialogs.addServer.buttons.cancel') }}
</v-btn>
<v-btn variant="text" @click="testServerConnection" :disabled="loading">
<v-btn
variant="text"
:disabled="loading"
@click="testServerConnection"
>
{{ tm('dialogs.addServer.buttons.testConnection') }}
</v-btn>
<v-btn color="primary" @click="saveServer" :loading="loading" :disabled="!isServerFormValid">
<v-btn
color="primary"
:loading="loading"
:disabled="!isServerFormValid"
@click="saveServer"
>
{{ tm('dialogs.addServer.buttons.save') }}
</v-btn>
</v-card-actions>
@@ -151,43 +295,82 @@
</v-dialog>
<!-- 同步 MCP 服务器对话框 -->
<v-dialog v-model="showSyncMcpServerDialog" max-width="500px" persistent>
<v-dialog
v-model="showSyncMcpServerDialog"
max-width="500px"
persistent
>
<v-card>
<v-card-title class="bg-primary text-white py-3">
<span>同步外部平台 MCP 服务器</span>
</v-card-title>
<v-card-text class="py-4">
<v-select v-model="selectedMcpServerProvider" :items="mcpServerProviderList"
label="选择平台" variant="outlined" required></v-select>
<v-select
v-model="selectedMcpServerProvider"
:items="mcpServerProviderList"
label="选择平台"
variant="outlined"
required
/>
<div v-if="selectedMcpServerProvider === 'modelscope'">
<v-timeline align="start" side="end">
<v-timeline-item icon="mdi-numeric-1" icon-color="rgb(var(--v-theme-background))">
<v-timeline
align="start"
side="end"
>
<v-timeline-item
icon="mdi-numeric-1"
icon-color="rgb(var(--v-theme-background))"
>
<div>
<div class="text-h4">发现 MCP 服务器</div>
<div class="text-h4">
发现 MCP 服务器
</div>
<p class="mt-2">
访问 <a href="https://www.modelscope.cn/mcp" target="_blank">ModelScope 平台</a> 浏览需要的 MCP 服务器
访问 <a
href="https://www.modelscope.cn/mcp"
target="_blank"
>ModelScope 平台</a> 浏览需要的 MCP 服务器
</p>
</div>
</v-timeline-item>
<v-timeline-item icon="mdi-numeric-2" icon-color="rgb(var(--v-theme-background))">
<v-timeline-item
icon="mdi-numeric-2"
icon-color="rgb(var(--v-theme-background))"
>
<div>
<div class="text-h4">获取访问令牌</div>
<div class="text-h4">
获取访问令牌
</div>
<p class="mt-2">
<a href="https://modelscope.cn/my/myaccesstoken" target="_blank">账户设置</a>中获取个人访问令牌
<a
href="https://modelscope.cn/my/myaccesstoken"
target="_blank"
>账户设置</a>中获取个人访问令牌
</p>
</div>
</v-timeline-item>
<v-timeline-item icon="mdi-numeric-3" icon-color="rgb(var(--v-theme-background))">
<v-timeline-item
icon="mdi-numeric-3"
icon-color="rgb(var(--v-theme-background))"
>
<div>
<div class="text-h4">输入您的访问令牌</div>
<div class="text-h4">
输入您的访问令牌
</div>
<p class="mt-2">
输入您的访问令牌以同步 MCP 服务器
</p>
<v-text-field v-model="mcpProviderToken" type="password" variant="outlined"
label="访问令牌" class="mt-2" hide-details/>
<v-text-field
v-model="mcpProviderToken"
type="password"
variant="outlined"
label="访问令牌"
class="mt-2"
hide-details
/>
</div>
</v-timeline-item>
</v-timeline>
@@ -195,11 +378,20 @@
</v-card-text>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="showSyncMcpServerDialog = false" :disabled="loading">
<v-spacer />
<v-btn
variant="text"
:disabled="loading"
@click="showSyncMcpServerDialog = false"
>
{{ tm('dialogs.addServer.buttons.cancel') }}
</v-btn>
<v-btn color="primary" @click="syncMcpServers" :loading="loading" :disabled="loading">
<v-btn
color="primary"
:loading="loading"
:disabled="loading"
@click="syncMcpServers"
>
{{ tm('dialogs.addServer.buttons.sync') }}
</v-btn>
</v-card-actions>
@@ -207,7 +399,13 @@
</v-dialog>
<!-- 消息提示 -->
<v-snackbar :timeout="3000" elevation="24" :color="save_message_success" v-model="save_message_snack" location="top">
<v-snackbar
v-model="save_message_snack"
:timeout="3000"
elevation="24"
:color="save_message_success"
location="top"
>
{{ save_message }}
</v-snackbar>
</div>

View File

@@ -54,7 +54,9 @@ const toggleOrder = () => {
@update:model-value="updateSortBy"
>
<template #prepend-inner>
<v-icon size="small">mdi-sort</v-icon>
<v-icon size="small">
mdi-sort
</v-icon>
</template>
</v-select>
@@ -65,10 +67,15 @@ const toggleOrder = () => {
density="compact"
@click="toggleOrder"
>
<v-icon>{{
order === "desc" ? "mdi-arrow-down-thin" : "mdi-arrow-up-thin"
}}</v-icon>
<v-tooltip activator="parent" location="top">
<v-icon>
{{
order === "desc" ? "mdi-arrow-down-thin" : "mdi-arrow-up-thin"
}}
</v-icon>
<v-tooltip
activator="parent"
location="top"
>
{{ order === "desc" ? descendingLabel : ascendingLabel }}
</v-tooltip>
</v-btn>

View File

@@ -1,6 +1,10 @@
<template>
<div class="skills-page">
<v-container fluid class="pa-0" elevation="0">
<v-container
fluid
class="pa-0"
elevation="0"
>
<v-row class="d-flex justify-space-between align-center px-4 py-3 pb-4">
<div>
<v-btn
@@ -22,15 +26,30 @@
{{ tm("skills.refresh") }}
</v-btn>
</div>
<v-btn-toggle v-model="mode" mandatory divided density="comfortable">
<v-btn value="local">{{ tm("skills.modeLocal") }}</v-btn>
<v-btn value="neo" :disabled="!neoEnabled">{{
tm("skills.modeNeo")
}}</v-btn>
<v-btn-toggle
v-model="mode"
mandatory
divided
density="comfortable"
>
<v-btn value="local">
{{ tm("skills.modeLocal") }}
</v-btn>
<v-btn
value="neo"
:disabled="!neoEnabled"
>
{{
tm("skills.modeNeo")
}}
</v-btn>
</v-btn-toggle>
</v-row>
<div v-if="mode === 'local'" class="px-2 pb-2 d-flex flex-column ga-2">
<div
v-if="mode === 'local'"
class="px-2 pb-2 d-flex flex-column ga-2"
>
<small style="color: grey">{{ tm("skills.runtimeHint") }}</small>
<v-alert
v-if="runtime === 'sandbox' && !sandboxCache.ready"
@@ -43,7 +62,10 @@
</v-alert>
</div>
<div v-if="mode === 'neo' && !neoEnabled" class="px-3 pb-3">
<div
v-if="mode === 'neo' && !neoEnabled"
class="px-3 pb-3"
>
<v-alert
type="warning"
variant="tonal"
@@ -59,15 +81,28 @@
v-if="loading"
indeterminate
color="primary"
></v-progress-linear>
/>
<div v-else-if="skills.length === 0" class="text-center pa-8">
<v-icon size="64" color="grey-lighten-1">mdi-folder-open</v-icon>
<p class="text-grey mt-4">{{ tm("skills.empty") }}</p>
<div
v-else-if="skills.length === 0"
class="text-center pa-8"
>
<v-icon
size="64"
color="grey-lighten-1"
>
mdi-folder-open
</v-icon>
<p class="text-grey mt-4">
{{ tm("skills.empty") }}
</p>
<small class="text-grey">{{ tm("skills.emptyHint") }}</small>
</div>
<v-row v-else align="stretch">
<v-row
v-else
align="stretch"
>
<v-col
v-for="skill in skills"
:key="skill.name"
@@ -100,12 +135,22 @@
<div
class="text-caption text-medium-emphasis skill-description"
>
<v-icon size="small" class="me-1">mdi-text</v-icon>
<v-icon
size="small"
class="me-1"
>
mdi-text
</v-icon>
{{ item.description || tm("skills.noDescription") }}
</div>
</div>
<div class="text-caption text-medium-emphasis skill-path">
<v-icon size="small" class="me-1">mdi-file-document</v-icon>
<v-icon
size="small"
class="me-1"
>
mdi-file-document
</v-icon>
{{ tm("skills.path") }}: {{ item.path }}
</div>
</template>
@@ -117,8 +162,8 @@
rounded="xl"
:disabled="
itemLoading[item.name] ||
false ||
isSandboxPresetSkill(item)
false ||
isSandboxPresetSkill(item)
"
@click="downloadSkill(item)"
>
@@ -131,12 +176,17 @@
</template>
<template v-else-if="mode === 'neo' && neoEnabled">
<v-card class="mx-3 mb-4 pa-4 neo-filter-card" variant="outlined">
<v-card
class="mx-3 mb-4 pa-4 neo-filter-card"
variant="outlined"
>
<div
class="d-flex flex-wrap justify-space-between align-center ga-2 mb-3"
>
<div>
<div class="text-subtitle-1 font-weight-bold">Neo Skills</div>
<div class="text-subtitle-1 font-weight-bold">
Neo Skills
</div>
<div class="text-caption text-medium-emphasis">
{{ tm("skills.neoFilterHint") }}
</div>
@@ -152,7 +202,10 @@
</div>
<v-row class="ga-md-0 ga-2">
<v-col cols="12" md="4">
<v-col
cols="12"
md="4"
>
<v-text-field
v-model="neoFilters.skill_key"
:label="tm('skills.neoSkillKey')"
@@ -162,7 +215,10 @@
variant="outlined"
/>
</v-col>
<v-col cols="12" md="4">
<v-col
cols="12"
md="4"
>
<v-select
v-model="neoFilters.status"
:label="tm('skills.neoStatus')"
@@ -175,7 +231,10 @@
variant="outlined"
/>
</v-col>
<v-col cols="12" md="4">
<v-col
cols="12"
md="4"
>
<v-select
v-model="neoFilters.stage"
:label="tm('skills.neoStage')"
@@ -195,24 +254,41 @@
v-if="neoLoading"
indeterminate
color="primary"
></v-progress-linear>
/>
<div class="mx-3 mb-3 d-flex flex-wrap ga-2">
<v-chip size="small" color="primary" variant="tonal"
>Candidates: {{ neoCandidates.length }}</v-chip
<v-chip
size="small"
color="primary"
variant="tonal"
>
<v-chip size="small" color="indigo" variant="tonal"
>Releases: {{ neoReleases.length }}</v-chip
Candidates: {{ neoCandidates.length }}
</v-chip>
<v-chip
size="small"
color="indigo"
variant="tonal"
>
<v-chip size="small" color="success" variant="tonal"
>Active: {{ activeReleaseCount }}</v-chip
Releases: {{ neoReleases.length }}
</v-chip>
<v-chip
size="small"
color="success"
variant="tonal"
>
Active: {{ activeReleaseCount }}
</v-chip>
</div>
<v-card class="mx-3 mb-4 neo-table-card" variant="outlined">
<v-card-title class="text-subtitle-1 font-weight-bold">{{
tm("skills.neoCandidates")
}}</v-card-title>
<v-card
class="mx-3 mb-4 neo-table-card"
variant="outlined"
>
<v-card-title class="text-subtitle-1 font-weight-bold">
{{
tm("skills.neoCandidates")
}}
</v-card-title>
<v-data-table
:headers="candidateHeaders"
:items="neoCandidates"
@@ -282,10 +358,15 @@
</v-data-table>
</v-card>
<v-card class="mx-3 mb-4 neo-table-card" variant="outlined">
<v-card-title class="text-subtitle-1 font-weight-bold">{{
tm("skills.neoReleases")
}}</v-card-title>
<v-card
class="mx-3 mb-4 neo-table-card"
variant="outlined"
>
<v-card-title class="text-subtitle-1 font-weight-bold">
{{
tm("skills.neoReleases")
}}
</v-card-title>
<v-data-table
:headers="releaseHeaders"
:items="neoReleases"
@@ -339,7 +420,11 @@
</template>
</v-container>
<v-dialog v-model="uploadDialog" max-width="880px" :persistent="uploading">
<v-dialog
v-model="uploadDialog"
max-width="880px"
:persistent="uploading"
>
<v-card class="skills-upload-dialog">
<v-card-title class="skills-upload-dialog__header px-6 pt-6 pb-2">
<div class="skills-upload-dialog__heading">
@@ -364,26 +449,34 @@
</p>
<div class="skills-upload-structure-note">
<v-icon size="18">mdi-information-outline</v-icon>
<v-icon size="18">
mdi-information-outline
</v-icon>
<span>{{ tm("skills.structureRequirement") }}</span>
</div>
<div class="skills-upload-capabilities">
<div class="skills-upload-capability">
<div class="skills-upload-capability__icon">
<v-icon size="18">mdi-layers-outline</v-icon>
<v-icon size="18">
mdi-layers-outline
</v-icon>
</div>
<span>{{ tm("skills.abilityMultiple") }}</span>
</div>
<div class="skills-upload-capability">
<div class="skills-upload-capability__icon">
<v-icon size="18">mdi-shield-check-outline</v-icon>
<v-icon size="18">
mdi-shield-check-outline
</v-icon>
</div>
<span>{{ tm("skills.abilityValidate") }}</span>
</div>
<div class="skills-upload-capability">
<div class="skills-upload-capability__icon">
<v-icon size="18">mdi-skip-next-circle-outline</v-icon>
<v-icon size="18">
mdi-skip-next-circle-outline
</v-icon>
</div>
<span>{{ tm("skills.abilitySkip") }}</span>
</div>
@@ -403,7 +496,9 @@
@drop.prevent="handleUploadDrop"
>
<div class="skills-dropzone__icon">
<v-icon size="34">mdi-folder-zip-outline</v-icon>
<v-icon size="34">
mdi-folder-zip-outline
</v-icon>
</div>
<div class="text-h6 font-weight-medium">
{{ tm("skills.dropzoneTitle") }}
@@ -421,10 +516,13 @@
hidden
accept=".zip"
@change="handleUploadSelection"
/>
>
</div>
<div v-if="uploadItems.length > 0" class="skills-upload-summary">
<div
v-if="uploadItems.length > 0"
class="skills-upload-summary"
>
<v-chip
size="small"
variant="flat"
@@ -479,7 +577,10 @@
</v-chip>
</div>
<div v-if="uploadItems.length > 0" class="skills-upload-list">
<div
v-if="uploadItems.length > 0"
class="skills-upload-list"
>
<div class="skills-upload-list__header">
<span>{{ tm("skills.fileListTitle") }}</span>
</div>
@@ -489,7 +590,9 @@
class="skills-upload-row"
>
<div class="skills-upload-row__meta">
<div class="skills-upload-row__name">{{ item.name }}</div>
<div class="skills-upload-row__name">
{{ item.name }}
</div>
<div class="skills-upload-row__size">
{{ formatFileSize(item.size) }}
</div>
@@ -515,7 +618,10 @@
</div>
</div>
</div>
<div v-else class="skills-upload-empty">
<div
v-else
class="skills-upload-empty"
>
{{ tm("skills.fileListEmpty") }}
</div>
</v-card-text>
@@ -546,31 +652,51 @@
</v-card>
</v-dialog>
<v-dialog v-model="deleteDialog" max-width="400px">
<v-dialog
v-model="deleteDialog"
max-width="400px"
>
<v-card>
<v-card-title>{{ tm("skills.deleteTitle") }}</v-card-title>
<v-card-text>{{ tm("skills.deleteMessage") }}</v-card-text>
<v-card-actions class="d-flex justify-end">
<v-btn variant="text" @click="deleteDialog = false">{{
tm("skills.cancel")
}}</v-btn>
<v-btn color="error" :loading="deleting" @click="deleteSkill">
<v-btn
variant="text"
@click="deleteDialog = false"
>
{{
tm("skills.cancel")
}}
</v-btn>
<v-btn
color="error"
:loading="deleting"
@click="deleteSkill"
>
{{ t("core.common.itemCard.delete") }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="payloadDialog.show" max-width="820px">
<v-dialog
v-model="payloadDialog.show"
max-width="820px"
>
<v-card>
<v-card-title>{{ tm("skills.neoPayloadTitle") }}</v-card-title>
<v-card-text>
<pre class="payload-preview">{{ payloadDialog.content }}</pre>
</v-card-text>
<v-card-actions class="d-flex justify-end">
<v-btn variant="text" @click="payloadDialog.show = false">{{
tm("skills.cancel")
}}</v-btn>
<v-btn
variant="text"
@click="payloadDialog.show = false"
>
{{
tm("skills.cancel")
}}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>

View File

@@ -58,49 +58,68 @@ const statusItems = [
<template>
<!-- 过滤器行 -->
<v-row class="mb-4" align="center">
<v-col cols="12" sm="6" md="3">
<v-row
class="mb-4"
align="center"
>
<v-col
cols="12"
sm="6"
md="3"
>
<v-select
:model-value="pluginFilter"
@update:model-value="emit('update:pluginFilter', $event)"
:items="pluginItems"
:label="tm('filters.byPlugin')"
density="compact"
variant="outlined"
hide-details
@update:model-value="emit('update:pluginFilter', $event)"
/>
</v-col>
<v-col cols="12" sm="6" md="2">
<v-col
cols="12"
sm="6"
md="2"
>
<v-select
:model-value="typeFilter"
@update:model-value="emit('update:typeFilter', $event)"
:items="typeItems"
:label="tm('filters.byType')"
density="compact"
variant="outlined"
hide-details
@update:model-value="emit('update:typeFilter', $event)"
/>
</v-col>
<v-col cols="12" sm="6" md="2">
<v-col
cols="12"
sm="6"
md="2"
>
<v-select
:model-value="permissionFilter"
@update:model-value="emit('update:permissionFilter', $event)"
:items="permissionItems"
:label="tm('filters.byPermission')"
density="compact"
variant="outlined"
hide-details
@update:model-value="emit('update:permissionFilter', $event)"
/>
</v-col>
<v-col cols="12" sm="6" md="2">
<v-col
cols="12"
sm="6"
md="2"
>
<v-select
:model-value="statusFilter"
@update:model-value="emit('update:statusFilter', $event)"
:items="statusItems"
:label="tm('filters.byStatus')"
density="compact"
variant="outlined"
hide-details
@update:model-value="emit('update:statusFilter', $event)"
/>
</v-col>
</v-row>
@@ -110,7 +129,6 @@ const statusItems = [
<div style="min-width: 200px; max-width: 350px; flex: 1; border: 1px solid #B9B9B9; border-radius: 16px;">
<v-text-field
:model-value="searchQuery"
@update:model-value="emit('update:searchQuery', normalizeTextInput($event))"
density="compact"
:label="tm('search.placeholder')"
prepend-inner-icon="mdi-magnify"
@@ -119,25 +137,40 @@ const statusItems = [
flat
hide-details
single-line
@update:model-value="emit('update:searchQuery', normalizeTextInput($event))"
/>
</div>
<div class="d-flex align-center ga-4">
<slot name="stats"></slot>
<v-divider vertical class="mx-1" style="height: 20px;" />
<slot name="stats" />
<v-divider
vertical
class="mx-1"
style="height: 20px;"
/>
<v-checkbox
:model-value="effectiveShowSystemPlugins"
@update:model-value="emit('update:showSystemPlugins', !!$event)"
:label="tm('filters.showSystemPlugins')"
density="compact"
hide-details
:disabled="hasSystemPluginConflict"
class="system-plugin-checkbox"
@update:model-value="emit('update:showSystemPlugins', !!$event)"
>
<template v-slot:label>
<template #label>
<span class="text-body-2">{{ tm('filters.showSystemPlugins') }}</span>
<v-tooltip v-if="hasSystemPluginConflict" location="top">
<template v-slot:activator="{ props: tooltipProps }">
<v-icon v-bind="tooltipProps" size="16" color="warning" class="ml-1">mdi-alert-circle</v-icon>
<v-tooltip
v-if="hasSystemPluginConflict"
location="top"
>
<template #activator="{ props: tooltipProps }">
<v-icon
v-bind="tooltipProps"
size="16"
color="warning"
class="ml-1"
>
mdi-alert-circle
</v-icon>
</template>
{{ tm('filters.systemPluginConflictHint') }}
</v-tooltip>

View File

@@ -102,7 +102,7 @@ const getRowProps = ({ item }: { item: CommandItem }) => {
:row-props="getRowProps"
:loading="props.loading"
>
<template v-slot:item.effective_command="{ item }">
<template #item.effective_command="{ item }">
<div class="d-flex align-center py-2">
<!-- 展开/折叠按钮针对指令组 -->
<v-btn
@@ -113,10 +113,15 @@ const getRowProps = ({ item }: { item: CommandItem }) => {
class="mr-1"
@click.stop="emit('toggle-expand', item)"
>
<v-icon size="18">{{ isGroupExpanded(item) ? 'mdi-chevron-down' : 'mdi-chevron-right' }}</v-icon>
<v-icon size="18">
{{ isGroupExpanded(item) ? 'mdi-chevron-down' : 'mdi-chevron-right' }}
</v-icon>
</v-btn>
<!-- 子指令缩进 -->
<div v-else-if="item.type === 'sub_command'" class="ml-6"></div>
<div
v-else-if="item.type === 'sub_command'"
class="ml-6"
/>
<div>
<div class="text-subtitle-1 font-weight-medium">
<code :class="{ 'sub-command-code': item.type === 'sub_command' }">{{ item.effective_command }}</code>
@@ -125,30 +130,40 @@ const getRowProps = ({ item }: { item: CommandItem }) => {
</div>
</template>
<template v-slot:item.type="{ item }">
<template #item.type="{ item }">
<v-chip
:color="getTypeInfo(item.type).color"
size="small"
variant="tonal"
>
<v-icon start size="14">{{ getTypeInfo(item.type).icon }}</v-icon>
<v-icon
start
size="14"
>
{{ getTypeInfo(item.type).icon }}
</v-icon>
{{ getTypeInfo(item.type).text }}{{ item.is_group && item.sub_commands?.length > 0 ? `(${item.sub_commands.length})` : '' }}
</v-chip>
</template>
<template v-slot:item.plugin="{ item }">
<div class="text-body-2">{{ item.plugin_display_name || item.plugin }}</div>
<template #item.plugin="{ item }">
<div class="text-body-2">
{{ item.plugin_display_name || item.plugin }}
</div>
</template>
<template v-slot:item.description="{ item }">
<div class="text-body-2 text-medium-emphasis" style="max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
<template #item.description="{ item }">
<div
class="text-body-2 text-medium-emphasis"
style="max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
>
{{ item.description || '-' }}
</div>
</template>
<template v-slot:item.permission="{ item }">
<template #item.permission="{ item }">
<v-menu location="bottom">
<template v-slot:activator="{ props }">
<template #activator="{ props }">
<v-chip
v-bind="props"
:color="getPermissionColor(item.permission)"
@@ -157,21 +172,26 @@ const getRowProps = ({ item }: { item: CommandItem }) => {
link
>
{{ getPermissionLabel(item.permission) }}
<v-icon end size="14">mdi-chevron-down</v-icon>
<v-icon
end
size="14"
>
mdi-chevron-down
</v-icon>
</v-chip>
</template>
<v-list density="compact">
<v-list-item
:value="'member'"
@click="$emit('update-permission', item, 'member')"
:active="item.permission !== 'admin'"
@click="$emit('update-permission', item, 'member')"
>
<v-list-item-title>{{ tm('permission.everyone') }}</v-list-item-title>
</v-list-item>
<v-list-item
:value="'admin'"
@click="$emit('update-permission', item, 'admin')"
:active="item.permission === 'admin'"
@click="$emit('update-permission', item, 'admin')"
>
<v-list-item-title>{{ tm('permission.admin') }}</v-list-item-title>
</v-list-item>
@@ -179,7 +199,7 @@ const getRowProps = ({ item }: { item: CommandItem }) => {
</v-menu>
</template>
<template v-slot:item.enabled="{ item }">
<template #item.enabled="{ item }">
<v-chip
:color="getStatusInfo(item).color"
size="small"
@@ -190,9 +210,13 @@ const getRowProps = ({ item }: { item: CommandItem }) => {
</v-chip>
</template>
<template v-slot:item.actions="{ item }">
<template #item.actions="{ item }">
<div class="d-flex align-center">
<v-btn-group density="default" variant="text" color="primary">
<v-btn-group
density="default"
variant="text"
color="primary"
>
<v-btn
v-if="!item.enabled"
icon
@@ -200,8 +224,15 @@ const getRowProps = ({ item }: { item: CommandItem }) => {
color="success"
@click="emit('toggle-command', item)"
>
<v-icon size="22">mdi-play</v-icon>
<v-tooltip activator="parent" location="top">{{ tm('tooltips.enable') }}</v-tooltip>
<v-icon size="22">
mdi-play
</v-icon>
<v-tooltip
activator="parent"
location="top"
>
{{ tm('tooltips.enable') }}
</v-tooltip>
</v-btn>
<v-btn
v-else
@@ -210,28 +241,68 @@ const getRowProps = ({ item }: { item: CommandItem }) => {
color="error"
@click="emit('toggle-command', item)"
>
<v-icon size="22">mdi-pause</v-icon>
<v-tooltip activator="parent" location="top">{{ tm('tooltips.disable') }}</v-tooltip>
<v-icon size="22">
mdi-pause
</v-icon>
<v-tooltip
activator="parent"
location="top"
>
{{ tm('tooltips.disable') }}
</v-tooltip>
</v-btn>
<v-btn icon size="small" color="warning" @click="emit('rename', item)">
<v-icon size="22">mdi-pencil</v-icon>
<v-tooltip activator="parent" location="top">{{ tm('tooltips.rename') }}</v-tooltip>
<v-btn
icon
size="small"
color="warning"
@click="emit('rename', item)"
>
<v-icon size="22">
mdi-pencil
</v-icon>
<v-tooltip
activator="parent"
location="top"
>
{{ tm('tooltips.rename') }}
</v-tooltip>
</v-btn>
<v-btn icon size="small" @click="emit('view-details', item)">
<v-icon size="22">mdi-information</v-icon>
<v-tooltip activator="parent" location="top">{{ tm('tooltips.viewDetails') }}</v-tooltip>
<v-btn
icon
size="small"
@click="emit('view-details', item)"
>
<v-icon size="22">
mdi-information
</v-icon>
<v-tooltip
activator="parent"
location="top"
>
{{ tm('tooltips.viewDetails') }}
</v-tooltip>
</v-btn>
</v-btn-group>
</div>
</template>
<template v-slot:no-data>
<template #no-data>
<div class="text-center pa-8">
<v-icon size="64" color="info" class="mb-4">mdi-console-line</v-icon>
<div class="text-h5 mb-2">{{ tm('empty.noCommands') }}</div>
<div class="text-body-1 mb-4">{{ tm('empty.noCommandsDesc') }}</div>
<v-icon
size="64"
color="info"
class="mb-4"
>
mdi-console-line
</v-icon>
<div class="text-h5 mb-2">
{{ tm('empty.noCommands') }}
</div>
<div class="text-body-1 mb-4">
{{ tm('empty.noCommandsDesc') }}
</div>
</div>
</template>
</v-data-table>

View File

@@ -46,54 +46,86 @@ const getPermissionLabel = (permission: string): string => {
</script>
<template>
<v-dialog :model-value="show" @update:model-value="emit('update:show', $event)" max-width="500">
<v-dialog
:model-value="show"
max-width="500"
@update:model-value="emit('update:show', $event)"
>
<v-card v-if="command">
<v-card-title class="text-h5">{{ tm('dialogs.details.title') }}</v-card-title>
<v-card-title class="text-h5">
{{ tm('dialogs.details.title') }}
</v-card-title>
<v-card-text>
<v-list density="compact">
<v-list-item>
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.type') }}</v-list-item-title>
<v-list-item-title class="font-weight-bold">
{{ tm('dialogs.details.type') }}
</v-list-item-title>
<v-list-item-subtitle>
<v-chip
:color="getTypeInfo(command.type).color"
size="small"
variant="tonal"
>
<v-icon start size="14">{{ getTypeInfo(command.type).icon }}</v-icon>
<v-icon
start
size="14"
>
{{ getTypeInfo(command.type).icon }}
</v-icon>
{{ getTypeInfo(command.type).text }}
</v-chip>
</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.handler') }}</v-list-item-title>
<v-list-item-title class="font-weight-bold">
{{ tm('dialogs.details.handler') }}
</v-list-item-title>
<v-list-item-subtitle><code>{{ command.handler_name }}</code></v-list-item-subtitle>
</v-list-item>
<v-list-item>
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.module') }}</v-list-item-title>
<v-list-item-title class="font-weight-bold">
{{ tm('dialogs.details.module') }}
</v-list-item-title>
<v-list-item-subtitle><code>{{ command.module_path }}</code></v-list-item-subtitle>
</v-list-item>
<v-list-item>
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.originalCommand') }}</v-list-item-title>
<v-list-item-title class="font-weight-bold">
{{ tm('dialogs.details.originalCommand') }}
</v-list-item-title>
<v-list-item-subtitle><code>{{ command.original_command }}</code></v-list-item-subtitle>
</v-list-item>
<v-list-item>
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.effectiveCommand') }}</v-list-item-title>
<v-list-item-title class="font-weight-bold">
{{ tm('dialogs.details.effectiveCommand') }}
</v-list-item-title>
<v-list-item-subtitle><code>{{ command.effective_command }}</code></v-list-item-subtitle>
</v-list-item>
<v-list-item v-if="command.parent_signature">
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.parentGroup') }}</v-list-item-title>
<v-list-item-title class="font-weight-bold">
{{ tm('dialogs.details.parentGroup') }}
</v-list-item-title>
<v-list-item-subtitle><code>{{ command.parent_signature }}</code></v-list-item-subtitle>
</v-list-item>
<v-list-item v-if="command.aliases.length > 0">
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.aliases') }}</v-list-item-title>
<v-list-item-title class="font-weight-bold">
{{ tm('dialogs.details.aliases') }}
</v-list-item-title>
<v-list-item-subtitle>
<v-chip v-for="alias in command.aliases" :key="alias" size="small" class="mr-1">
<v-chip
v-for="alias in command.aliases"
:key="alias"
size="small"
class="mr-1"
>
{{ alias }}
</v-chip>
</v-list-item-subtitle>
</v-list-item>
<v-list-item v-if="command.is_group && command.sub_commands?.length > 0">
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.subCommands') }}</v-list-item-title>
<v-list-item-title class="font-weight-bold">
{{ tm('dialogs.details.subCommands') }}
</v-list-item-title>
<v-list-item-subtitle>
<div class="d-flex flex-wrap ga-1 mt-1">
<v-chip
@@ -108,24 +140,40 @@ const getPermissionLabel = (permission: string): string => {
</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.permission') }}</v-list-item-title>
<v-list-item-title class="font-weight-bold">
{{ tm('dialogs.details.permission') }}
</v-list-item-title>
<v-list-item-subtitle>
<v-chip :color="getPermissionColor(command.permission)" size="small">
<v-chip
:color="getPermissionColor(command.permission)"
size="small"
>
{{ getPermissionLabel(command.permission) }}
</v-chip>
</v-list-item-subtitle>
</v-list-item>
<v-list-item v-if="command.has_conflict">
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.conflictStatus') }}</v-list-item-title>
<v-list-item-title class="font-weight-bold">
{{ tm('dialogs.details.conflictStatus') }}
</v-list-item-title>
<v-list-item-subtitle>
<v-chip color="warning" size="small">{{ tm('status.conflict') }}</v-chip>
<v-chip
color="warning"
size="small"
>
{{ tm('status.conflict') }}
</v-chip>
</v-list-item-subtitle>
</v-list-item>
</v-list>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn color="primary" variant="text" @click="emit('update:show', false)">
<v-btn
color="primary"
variant="text"
@click="emit('update:show', false)"
>
{{ t('core.actions.close') }}
</v-btn>
</v-card-actions>

View File

@@ -57,21 +57,31 @@ watch(showAliasEditor, (open) => {
</script>
<template>
<v-dialog :model-value="show" @update:model-value="emit('update:show', $event)" max-width="500">
<v-dialog
:model-value="show"
max-width="500"
@update:model-value="emit('update:show', $event)"
>
<v-card>
<v-card-title class="text-h5">{{ tm('dialogs.rename.title') }}</v-card-title>
<v-card-title class="text-h5">
{{ tm('dialogs.rename.title') }}
</v-card-title>
<v-card-text>
<v-text-field
:model-value="newName"
@update:model-value="emit('update:newName', $event)"
:label="tm('dialogs.rename.newName')"
variant="outlined"
density="compact"
autofocus
class="mb-2"
@update:model-value="emit('update:newName', $event)"
/>
<v-card variant="outlined" class="mt-2" elevation="0">
<v-card
variant="outlined"
class="mt-2"
elevation="0"
>
<div
class="d-flex align-center justify-space-between px-4 py-3"
role="button"
@@ -80,22 +90,40 @@ watch(showAliasEditor, (open) => {
@keydown.enter.prevent="showAliasEditor = !showAliasEditor"
@keydown.space.prevent="showAliasEditor = !showAliasEditor"
>
<div class="text-subtitle-1">{{ tm('dialogs.rename.aliases') }}</div>
<v-icon size="20">{{ showAliasEditor ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
<div class="text-subtitle-1">
{{ tm('dialogs.rename.aliases') }}
</div>
<v-icon size="20">
{{ showAliasEditor ? 'mdi-chevron-up' : 'mdi-chevron-down' }}
</v-icon>
</div>
<v-divider v-if="showAliasEditor" />
<v-slide-y-transition>
<div v-if="aliasEditorEverOpened" v-show="showAliasEditor" class="px-4 py-3">
<div v-for="(alias, index) in aliases" :key="index" class="d-flex align-center mb-2">
<div
v-if="aliasEditorEverOpened"
v-show="showAliasEditor"
class="px-4 py-3"
>
<div
v-for="(alias, index) in aliases"
:key="index"
class="d-flex align-center mb-2"
>
<v-text-field
:model-value="alias"
@update:model-value="updateAlias(index, $event)"
variant="outlined"
density="compact"
hide-details
class="flex-grow-1 mr-2"
@update:model-value="updateAlias(index, $event)"
/>
<v-btn
icon="mdi-delete"
variant="text"
color="error"
density="compact"
@click="removeAlias(index)"
/>
<v-btn icon="mdi-delete" variant="text" color="error" density="compact" @click="removeAlias(index)" />
</div>
<v-btn
prepend-icon="mdi-plus"
@@ -114,7 +142,11 @@ watch(showAliasEditor, (open) => {
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn color="grey" variant="text" @click="emit('update:show', false)">
<v-btn
color="grey"
variant="text"
@click="emit('update:show', false)"
>
{{ tm('dialogs.rename.cancel') }}
</v-btn>
<v-btn

View File

@@ -40,39 +40,71 @@ const isInternal = (tool: ToolItem) => tool.source === 'internal';
:loading="props.loading"
>
<template #item.name="{ item }">
<div class="d-flex align-center py-2" :class="{ 'internal-tool-row': isInternal(item) }">
<v-icon :color="isInternal(item) ? 'grey' : 'primary'" class="mr-2" size="18">
<div
class="d-flex align-center py-2"
:class="{ 'internal-tool-row': isInternal(item) }"
>
<v-icon
:color="isInternal(item) ? 'grey' : 'primary'"
class="mr-2"
size="18"
>
{{ isInternal(item) ? 'mdi-lock-outline' : (item.name.includes(':') ? 'mdi-server-network' : 'mdi-function-variant') }}
</v-icon>
<div>
<div class="text-subtitle-1 font-weight-medium">{{ item.name }}</div>
<div class="text-subtitle-1 font-weight-medium">
{{ item.name }}
</div>
</div>
</div>
</template>
<template #item.description="{ item }">
<div class="text-body-2 text-medium-emphasis" style="max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
<div
class="text-body-2 text-medium-emphasis"
style="max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
>
{{ item.description || '-' }}
</div>
</template>
<template #item.origin="{ item }">
<v-chip size="small" variant="tonal" color="info" class="text-caption font-weight-medium">
<v-chip
size="small"
variant="tonal"
color="info"
class="text-caption font-weight-medium"
>
{{ item.origin || '-' }}
</v-chip>
</template>
<template #item.origin_name="{ item }">
<div class="text-body-2 text-medium-emphasis" style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
<div
class="text-body-2 text-medium-emphasis"
style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
>
{{ item.origin_name || '-' }}
</div>
</template>
<template #item.active="{ item }">
<v-chip v-if="isInternal(item)" color="grey" size="small" class="font-weight-medium" variant="tonal">
<v-chip
v-if="isInternal(item)"
color="grey"
size="small"
class="font-weight-medium"
variant="tonal"
>
内置
</v-chip>
<v-chip v-else :color="item.active ? 'success' : 'error'" size="small" class="font-weight-medium" :variant="item.active ? 'flat' : 'outlined'">
<v-chip
v-else
:color="item.active ? 'success' : 'error'"
size="small"
class="font-weight-medium"
:variant="item.active ? 'flat' : 'outlined'"
>
{{ item.active ? tmCommand('status.enabled') : tmCommand('status.disabled') }}
</v-chip>
</template>
@@ -87,23 +119,47 @@ const isInternal = (tool: ToolItem) => tool.source === 'internal';
inset
@update:model-value="emit('toggle-tool', item)"
/>
<span v-else class="text-caption text-grey"></span>
<span
v-else
class="text-caption text-grey"
></span>
</template>
<template #no-data>
<div class="text-center pa-8">
<v-icon size="64" color="info" class="mb-4">mdi-function-variant</v-icon>
<div class="text-h5 mb-2">{{ tmTool('functionTools.empty') }}</div>
<v-icon
size="64"
color="info"
class="mb-4"
>
mdi-function-variant
</v-icon>
<div class="text-h5 mb-2">
{{ tmTool('functionTools.empty') }}
</div>
</div>
</template>
<template #expanded-row="{ item }">
<td :colspan="toolHeaders.length + 1" class="pa-4">
<td
:colspan="toolHeaders.length + 1"
class="pa-4"
>
<div class="d-flex align-start ga-4">
<v-icon size="20" color="primary">mdi-code-json</v-icon>
<v-icon
size="20"
color="primary"
>
mdi-code-json
</v-icon>
<div class="flex-1">
<div class="text-subtitle-2 font-weight-medium mb-2">{{ tmTool('functionTools.parameters') }}</div>
<div v-if="parameterEntries(item).length === 0" class="text-caption text-medium-emphasis">
<div class="text-subtitle-2 font-weight-medium mb-2">
{{ tmTool('functionTools.parameters') }}
</div>
<div
v-if="parameterEntries(item).length === 0"
class="text-caption text-medium-emphasis"
>
{{ tmTool('functionTools.noParameters') }}
</div>
<v-table
@@ -113,20 +169,40 @@ const isInternal = (tool: ToolItem) => tool.source === 'internal';
>
<thead>
<tr>
<th class="text-left text-caption text-medium-emphasis">{{ tmTool('functionTools.table.paramName') }}</th>
<th class="text-left text-caption text-medium-emphasis" style="width: 140px;">{{ tmTool('functionTools.table.type') }}</th>
<th class="text-left text-caption text-medium-emphasis">{{ tmTool('functionTools.table.description') }}</th>
<th class="text-left text-caption text-medium-emphasis">
{{ tmTool('functionTools.table.paramName') }}
</th>
<th
class="text-left text-caption text-medium-emphasis"
style="width: 140px;"
>
{{ tmTool('functionTools.table.type') }}
</th>
<th class="text-left text-caption text-medium-emphasis">
{{ tmTool('functionTools.table.description') }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="([paramName, param]) in parameterEntries(item)" :key="paramName">
<td class="font-weight-medium text-body-2">{{ paramName }}</td>
<tr
v-for="([paramName, param]) in parameterEntries(item)"
:key="paramName"
>
<td class="font-weight-medium text-body-2">
{{ paramName }}
</td>
<td class="text-body-2">
<v-chip size="x-small" color="primary" class="text-caption">
<v-chip
size="x-small"
color="primary"
class="text-caption"
>
{{ param?.type || '-' }}
</v-chip>
</td>
<td class="text-body-2 text-medium-emphasis">{{ param?.description || '-' }}</td>
<td class="text-body-2 text-medium-emphasis">
{{ param?.description || '-' }}
</td>
</tr>
</tbody>
</v-table>

View File

@@ -24,7 +24,7 @@ export function useComponentData() {
/**
* 显示 Toast 消息
*/
const toast = (message: string, color: string = 'success') => {
const toast = (message: string, color = 'success') => {
snackbar.message = message;
snackbar.color = color;
snackbar.show = true;

View File

@@ -155,16 +155,35 @@ watch(viewMode, async (mode) => {
<template>
<v-row>
<v-col cols="12">
<v-card variant="flat" style="background-color: transparent">
<v-card
variant="flat"
style="background-color: transparent"
>
<v-card-text style="padding: 20px 12px; padding-top: 0px;">
<div class="d-flex justify-space-between align-center mb-6 flex-wrap ga-3">
<v-btn-toggle v-model="viewMode" color="primary" variant="outlined" density="comfortable" mandatory>
<v-btn-toggle
v-model="viewMode"
color="primary"
variant="outlined"
density="comfortable"
mandatory
>
<v-btn value="commands">
<v-icon size="18" class="mr-1">mdi-console-line</v-icon>
<v-icon
size="18"
class="mr-1"
>
mdi-console-line
</v-icon>
{{ tm('type.command') }}
</v-btn>
<v-btn value="tools">
<v-icon size="18" class="mr-1">mdi-function-variant</v-icon>
<v-icon
size="18"
class="mr-1"
>
mdi-function-variant
</v-icon>
{{ tmTool('functionTools.title') }}
</v-btn>
</v-btn-toggle>
@@ -185,30 +204,46 @@ watch(viewMode, async (mode) => {
<div v-if="viewMode === 'commands'">
<CommandFilters
:plugin-filter="pluginFilter"
@update:plugin-filter="pluginFilter = $event"
:type-filter="typeFilter"
@update:type-filter="typeFilter = $event"
:permission-filter="permissionFilter"
@update:permission-filter="permissionFilter = $event"
:status-filter="statusFilter"
@update:status-filter="statusFilter = $event"
:show-system-plugins="showSystemPlugins"
@update:show-system-plugins="showSystemPlugins = $event"
:search-query="searchQuery"
@update:search-query="searchQuery = $event"
:available-plugins="availablePlugins"
:has-system-plugin-conflict="hasSystemPluginConflict"
:effective-show-system-plugins="effectiveShowSystemPlugins"
@update:plugin-filter="pluginFilter = $event"
@update:type-filter="typeFilter = $event"
@update:permission-filter="permissionFilter = $event"
@update:status-filter="statusFilter = $event"
@update:show-system-plugins="showSystemPlugins = $event"
@update:search-query="searchQuery = $event"
>
<template #stats>
<div class="d-flex align-center">
<v-icon size="18" color="primary" class="mr-1">mdi-console-line</v-icon>
<v-icon
size="18"
color="primary"
class="mr-1"
>
mdi-console-line
</v-icon>
<span class="text-body-2 text-medium-emphasis mr-1">{{ tm('summary.total') }}:</span>
<span class="text-body-1 font-weight-bold text-primary">{{ filteredCommands.length }}</span>
</div>
<v-divider vertical class="mx-1" style="height: 20px;" />
<v-divider
vertical
class="mx-1"
style="height: 20px;"
/>
<div class="d-flex align-center">
<v-icon size="18" color="error" class="mr-1">mdi-close-circle-outline</v-icon>
<v-icon
size="18"
color="error"
class="mr-1"
>
mdi-close-circle-outline
</v-icon>
<span class="text-body-2 text-medium-emphasis mr-1">{{ tm('summary.disabled') }}:</span>
<span class="text-body-1 font-weight-bold text-error">{{ summary.disabled }}</span>
</div>
@@ -223,8 +258,10 @@ watch(viewMode, async (mode) => {
prominent
border="start"
>
<template v-slot:prepend>
<v-icon size="28">mdi-alert-circle</v-icon>
<template #prepend>
<v-icon size="28">
mdi-alert-circle
</v-icon>
</template>
<v-alert-title class="text-subtitle-1 font-weight-bold">
{{ tm('conflictAlert.title') }}
@@ -233,7 +270,12 @@ watch(viewMode, async (mode) => {
{{ tm('conflictAlert.description', { count: summary.conflicts }) }}
</div>
<div class="text-body-2 mt-2">
<v-icon size="16" class="mr-1">mdi-lightbulb-outline</v-icon>
<v-icon
size="16"
class="mr-1"
>
mdi-lightbulb-outline
</v-icon>
{{ tm('conflictAlert.hint') }}
</div>
</v-alert>
@@ -255,24 +297,40 @@ watch(viewMode, async (mode) => {
<div style="min-width: 240px; max-width: 380px; flex: 1;">
<v-text-field
:model-value="toolSearch"
@update:model-value="toolSearch = normalizeTextInput($event)"
prepend-inner-icon="mdi-magnify"
:label="tmTool('functionTools.search')"
variant="outlined"
density="compact"
hide-details
clearable
@update:model-value="toolSearch = normalizeTextInput($event)"
/>
</div>
<div class="d-flex align-center ga-2">
<div class="d-flex align-center">
<v-icon size="18" color="primary" class="mr-1">mdi-function-variant</v-icon>
<v-icon
size="18"
color="primary"
class="mr-1"
>
mdi-function-variant
</v-icon>
<span class="text-body-2 text-medium-emphasis mr-1">{{ tm('summary.total') }}:</span>
<span class="text-body-1 font-weight-bold text-primary">{{ filteredTools.length }}</span>
</div>
<v-divider vertical class="mx-1" style="height: 20px;" />
<v-divider
vertical
class="mx-1"
style="height: 20px;"
/>
<div class="d-flex align-center">
<v-icon size="18" color="success" class="mr-1">mdi-check-circle-outline</v-icon>
<v-icon
size="18"
color="success"
class="mr-1"
>
mdi-check-circle-outline
</v-icon>
<span class="text-body-2 text-medium-emphasis mr-1">{{ tm('status.enabled') }}:</span>
<span class="text-body-1 font-weight-bold text-success">{{ filteredTools.filter(t => t.active).length }}</span>
</div>
@@ -293,25 +351,30 @@ watch(viewMode, async (mode) => {
<!-- 重命名对话框 -->
<RenameDialog
:show="renameDialog.show"
@update:show="renameDialog.show = $event"
:new-name="renameDialog.newName"
@update:new-name="renameDialog.newName = $event"
:aliases="renameDialog.aliases"
@update:aliases="renameDialog.aliases = $event"
:command="renameDialog.command"
:loading="renameDialog.loading"
@update:show="renameDialog.show = $event"
@update:new-name="renameDialog.newName = $event"
@update:aliases="renameDialog.aliases = $event"
@confirm="handleConfirmRename"
/>
<!-- 详情对话框 -->
<DetailsDialog
:show="detailsDialog.show"
@update:show="detailsDialog.show = $event"
:command="detailsDialog.command"
@update:show="detailsDialog.show = $event"
/>
<!-- Snackbar -->
<v-snackbar :timeout="2000" elevation="24" :color="snackbar.color" v-model="snackbar.show">
<v-snackbar
v-model="snackbar.show"
:timeout="2000"
elevation="24"
:color="snackbar.color"
>
{{ snackbar.message }}
</v-snackbar>
</template>

View File

@@ -1,31 +1,60 @@
<template>
<v-dialog v-model="showDialog" max-width="450px">
<v-card>
<v-card-title>
<v-icon class="mr-2">mdi-folder-plus</v-icon>
{{ labels.title }}
</v-card-title>
<v-card-text>
<v-form ref="form" v-model="formValid">
<v-text-field v-model="formData.name" :label="mergedLabels.nameLabel"
:rules="[(v: any) => !!v || mergedLabels.nameRequired]" variant="outlined"
density="comfortable" autofocus class="mb-3" />
<v-dialog
v-model="showDialog"
max-width="450px"
>
<v-card>
<v-card-title>
<v-icon class="mr-2">
mdi-folder-plus
</v-icon>
{{ labels.title }}
</v-card-title>
<v-card-text>
<v-form
ref="form"
v-model="formValid"
>
<v-text-field
v-model="formData.name"
:label="mergedLabels.nameLabel"
:rules="[(v: any) => !!v || mergedLabels.nameRequired]"
variant="outlined"
density="comfortable"
autofocus
class="mb-3"
/>
<v-textarea v-model="formData.description" :label="labels.descriptionLabel" variant="outlined"
rows="3" density="comfortable" hide-details />
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="closeDialog">
{{ labels.cancelButton }}
</v-btn>
<v-btn color="primary" variant="flat" @click="submitForm" :loading="loading" :disabled="!formValid">
{{ labels.createButton }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-textarea
v-model="formData.description"
:label="labels.descriptionLabel"
variant="outlined"
rows="3"
density="comfortable"
hide-details
/>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
variant="text"
@click="closeDialog"
>
{{ labels.cancelButton }}
</v-btn>
<v-btn
color="primary"
variant="flat"
:loading="loading"
:disabled="!formValid"
@click="submitForm"
>
{{ labels.createButton }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script lang="ts">

View File

@@ -1,19 +1,38 @@
<template>
<v-breadcrumbs :items="computedItems" class="base-folder-breadcrumb pa-0">
<template v-slot:prepend>
<v-icon size="small" class="mr-1">mdi-folder-outline</v-icon>
</template>
<template v-slot:item="{ item }">
<v-breadcrumbs-item :disabled="(item as any).disabled" @click="!(item as any).disabled && handleClick((item as any).folderId)"
:class="{ 'breadcrumb-link': !(item as any).disabled }">
<v-icon v-if="(item as any).isRoot" size="small" class="mr-1">mdi-home</v-icon>
{{ (item as any).title }}
</v-breadcrumbs-item>
</template>
<template v-slot:divider>
<v-icon size="small">mdi-chevron-right</v-icon>
</template>
</v-breadcrumbs>
<v-breadcrumbs
:items="computedItems"
class="base-folder-breadcrumb pa-0"
>
<template #prepend>
<v-icon
size="small"
class="mr-1"
>
mdi-folder-outline
</v-icon>
</template>
<template #item="{ item }">
<v-breadcrumbs-item
:disabled="(item as any).disabled"
:class="{ 'breadcrumb-link': !(item as any).disabled }"
@click="!(item as any).disabled && handleClick((item as any).folderId)"
>
<v-icon
v-if="(item as any).isRoot"
size="small"
class="mr-1"
>
mdi-home
</v-icon>
{{ (item as any).title }}
</v-breadcrumbs-item>
</template>
<template #divider>
<v-icon size="small">
mdi-chevron-right
</v-icon>
</template>
</v-breadcrumbs>
</template>
<script lang="ts">

View File

@@ -1,48 +1,89 @@
<template>
<v-card class="base-folder-card" :class="{ 'drag-over': isDragOver }" rounded="lg" @click="$emit('click')" @contextmenu.prevent="$emit('contextmenu', $event)"
elevation="1" hover @dragover.prevent="handleDragOver" @dragleave="handleDragLeave" @drop.prevent="handleDrop">
<v-card-text class="d-flex align-center pa-3">
<v-icon size="40" color="amber-darken-2" class="mr-3">mdi-folder</v-icon>
<div class="folder-info flex-grow-1 overflow-hidden">
<div class="text-subtitle-1 font-weight-medium text-truncate">{{ folder.name }}</div>
<div v-if="folder.description" class="text-body-2 text-medium-emphasis text-truncate">
{{ folder.description }}
</div>
</div>
<v-menu offset-y>
<template v-slot:activator="{ props }">
<v-btn icon="mdi-dots-vertical" variant="text" size="small" v-bind="props" @click.stop />
</template>
<v-list density="compact">
<v-list-item @click.stop="$emit('open')">
<template v-slot:prepend>
<v-icon size="small">mdi-folder-open</v-icon>
</template>
<v-list-item-title>{{ labels.open }}</v-list-item-title>
</v-list-item>
<v-list-item @click.stop="$emit('rename')">
<template v-slot:prepend>
<v-icon size="small">mdi-pencil</v-icon>
</template>
<v-list-item-title>{{ labels.rename }}</v-list-item-title>
</v-list-item>
<v-list-item @click.stop="$emit('move')">
<template v-slot:prepend>
<v-icon size="small">mdi-folder-move</v-icon>
</template>
<v-list-item-title>{{ labels.moveTo }}</v-list-item-title>
</v-list-item>
<v-divider class="my-1" />
<v-list-item @click.stop="$emit('delete')" class="text-error">
<template v-slot:prepend>
<v-icon size="small" color="error">mdi-delete</v-icon>
</template>
<v-list-item-title>{{ labels.delete }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-card-text>
</v-card>
<v-card
class="base-folder-card"
:class="{ 'drag-over': isDragOver }"
rounded="lg"
elevation="1"
hover
@click="$emit('click')"
@contextmenu.prevent="$emit('contextmenu', $event)"
@dragover.prevent="handleDragOver"
@dragleave="handleDragLeave"
@drop.prevent="handleDrop"
>
<v-card-text class="d-flex align-center pa-3">
<v-icon
size="40"
color="amber-darken-2"
class="mr-3"
>
mdi-folder
</v-icon>
<div class="folder-info flex-grow-1 overflow-hidden">
<div class="text-subtitle-1 font-weight-medium text-truncate">
{{ folder.name }}
</div>
<div
v-if="folder.description"
class="text-body-2 text-medium-emphasis text-truncate"
>
{{ folder.description }}
</div>
</div>
<v-menu offset-y>
<template #activator="{ props }">
<v-btn
icon="mdi-dots-vertical"
variant="text"
size="small"
v-bind="props"
@click.stop
/>
</template>
<v-list density="compact">
<v-list-item @click.stop="$emit('open')">
<template #prepend>
<v-icon size="small">
mdi-folder-open
</v-icon>
</template>
<v-list-item-title>{{ labels.open }}</v-list-item-title>
</v-list-item>
<v-list-item @click.stop="$emit('rename')">
<template #prepend>
<v-icon size="small">
mdi-pencil
</v-icon>
</template>
<v-list-item-title>{{ labels.rename }}</v-list-item-title>
</v-list-item>
<v-list-item @click.stop="$emit('move')">
<template #prepend>
<v-icon size="small">
mdi-folder-move
</v-icon>
</template>
<v-list-item-title>{{ labels.moveTo }}</v-list-item-title>
</v-list-item>
<v-divider class="my-1" />
<v-list-item
class="text-error"
@click.stop="$emit('delete')"
>
<template #prepend>
<v-icon
size="small"
color="error"
>
mdi-delete
</v-icon>
</template>
<v-list-item-title>{{ labels.delete }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-card-text>
</v-card>
</template>
<script lang="ts">

View File

@@ -1,165 +1,318 @@
<template>
<div class="folder-item-selector">
<!-- 触发按钮区域 -->
<div class="d-flex align-center justify-space-between">
<span v-if="!modelValue" style="color: rgb(var(--v-theme-primaryText));">
{{ labels.notSelected || '未选择' }}
</span>
<span v-else>
{{ displayValue }}
</span>
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
{{ labels.buttonText || '选择...' }}
</v-btn>
</div>
<!-- 选择对话框 -->
<v-dialog v-model="dialog" max-width="1000px" min-width="800px">
<v-card class="selector-dialog-card">
<v-card-title class="dialog-title d-flex align-center py-4 px-5">
<v-icon class="mr-3" color="primary">mdi-account-circle</v-icon>
<span>{{ labels.dialogTitle || '选择项目' }}</span>
</v-card-title>
<v-divider />
<v-card-text class="pa-0" style="height: 600px; max-height: 80vh; overflow: hidden;">
<div class="selector-layout">
<!-- 左侧文件夹树 -->
<div class="folder-sidebar">
<div class="sidebar-header pa-3 pb-2">
<span class="text-caption text-medium-emphasis font-weight-medium">
<v-icon size="small" class="mr-1">mdi-folder-multiple</v-icon>
文件夹
</span>
</div>
<v-list density="compact" nav class="tree-list pa-2" bg-color="transparent">
<!-- 根目录 -->
<v-list-item :active="currentFolderId === null" @click="navigateToFolder(null)"
rounded="lg" class="mb-1 root-item">
<template v-slot:prepend>
<v-icon size="20" :color="currentFolderId === null ? 'primary' : ''">mdi-home</v-icon>
</template>
<v-list-item-title class="text-body-2">{{ labels.rootFolder || '根目录' }}</v-list-item-title>
</v-list-item>
<!-- 文件夹树 -->
<template v-if="!treeLoading">
<BaseMoveTargetNode v-for="folder in folderTree" :key="folder.folder_id"
:folder="folder" :depth="0" :selected-folder-id="currentFolderId"
:disabled-folder-ids="[]" @select="navigateToFolder" />
</template>
<div v-if="treeLoading" class="text-center pa-4">
<v-progress-circular indeterminate size="20" color="primary" />
</div>
</v-list>
</div>
<!-- 右侧项目列表 -->
<div class="items-panel">
<!-- 面包屑导航 -->
<div class="breadcrumb-bar px-4 py-3">
<v-breadcrumbs :items="breadcrumbItems" density="compact" class="pa-0">
<template v-slot:item="{ item }">
<v-breadcrumbs-item :disabled="(item as any).disabled"
@click="!(item as any).disabled && navigateToFolder((item as any).folderId)"
:class="{ 'breadcrumb-link': !(item as any).disabled }">
<v-icon v-if="(item as any).isRoot" size="small"
class="mr-1">mdi-home</v-icon>
{{ item.title }}
</v-breadcrumbs-item>
</template>
<template v-slot:divider>
<v-icon size="small" color="grey">mdi-chevron-right</v-icon>
</template>
</v-breadcrumbs>
</div>
<v-divider />
<!-- 项目列表 -->
<div class="items-list">
<v-progress-linear v-if="itemsLoading" indeterminate
color="primary" height="2"></v-progress-linear>
<!-- 子文件夹 -->
<v-list v-if="!itemsLoading" lines="two" class="pa-3 items-content">
<template v-if="currentSubFolders.length > 0">
<div class="section-label text-caption text-medium-emphasis mb-2 px-2">子文件夹</div>
<v-list-item v-for="folder in currentSubFolders" :key="'folder-' + folder.folder_id"
@click="navigateToFolder(folder.folder_id)" rounded="lg" class="mb-1 folder-item">
<template v-slot:prepend>
<v-avatar size="36" color="amber-lighten-4" class="mr-3">
<v-icon color="amber-darken-2" size="20">mdi-folder</v-icon>
</v-avatar>
</template>
<v-list-item-title class="font-weight-medium">{{ folder.name }}</v-list-item-title>
<template v-slot:append>
<v-icon size="20" color="grey">mdi-chevron-right</v-icon>
</template>
</v-list-item>
</template>
<!-- 项目列表 -->
<template v-if="currentItems.length > 0">
<div class="section-label text-caption text-medium-emphasis mb-2 px-2" :class="{ 'mt-4': currentSubFolders.length > 0 }">可选项目</div>
<v-list-item v-for="item in currentItems" :key="'item-' + getItemId(item)"
:value="getItemId(item)" @click="selectItem(item)"
:active="selectedItemId === getItemId(item)" rounded="lg" class="mb-1 persona-item"
:class="{ 'selected-item': selectedItemId === getItemId(item) }">
<template v-slot:prepend>
<v-avatar size="36" :color="selectedItemId === getItemId(item) ? 'primary-lighten-4' : 'grey-lighten-3'" class="mr-3">
<v-icon :color="selectedItemId === getItemId(item) ? 'primary' : 'grey-darken-1'" size="20">mdi-account</v-icon>
</v-avatar>
</template>
<v-list-item-title class="font-weight-medium">{{ getItemName(item) }}</v-list-item-title>
<v-list-item-subtitle v-if="getItemDescription(item)" class="text-truncate">
{{ truncateText(getItemDescription(item), 80) }}
</v-list-item-subtitle>
<template v-slot:append>
<div class="d-flex align-center ga-1">
<v-btn v-if="showEditButton && !isDefaultItem(item)"
icon="mdi-pencil"
size="small"
variant="text"
@click.stop="handleEditItem(item)"
:title="labels.editButton || 'Edit'"
/>
<v-icon v-if="selectedItemId === getItemId(item)"
color="primary" size="22">mdi-check-circle</v-icon>
</div>
</template>
</v-list-item>
</template>
<!-- 空状态 -->
<div v-if="currentSubFolders.length === 0 && currentItems.length === 0"
class="empty-state text-center py-12">
<v-icon size="64" color="grey-lighten-2">mdi-folder-open-outline</v-icon>
<p class="text-grey mt-4 text-body-2">{{ labels.emptyFolder || labels.noItems || '此文件夹为空' }}</p>
</div>
</v-list>
</div>
</div>
</div>
</v-card-text>
<v-card-actions class="pa-4">
<v-btn v-if="showCreateButton" variant="text" color="primary" prepend-icon="mdi-plus"
@click="$emit('create')">
{{ labels.createButton || '新建' }}
</v-btn>
<v-spacer></v-spacer>
<v-btn variant="text" @click="cancelSelection">{{ labels.cancelButton || '取消' }}</v-btn>
<v-btn color="primary" @click="confirmSelection" :disabled="!selectedItemId">
{{ labels.confirmButton || '确认' }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<div class="folder-item-selector">
<!-- 触发按钮区域 -->
<div class="d-flex align-center justify-space-between">
<span
v-if="!modelValue"
style="color: rgb(var(--v-theme-primaryText));"
>
{{ labels.notSelected || '未选择' }}
</span>
<span v-else>
{{ displayValue }}
</span>
<v-btn
size="small"
color="primary"
variant="tonal"
@click="openDialog"
>
{{ labels.buttonText || '选择...' }}
</v-btn>
</div>
<!-- 选择对话框 -->
<v-dialog
v-model="dialog"
max-width="1000px"
min-width="800px"
>
<v-card class="selector-dialog-card">
<v-card-title class="dialog-title d-flex align-center py-4 px-5">
<v-icon
class="mr-3"
color="primary"
>
mdi-account-circle
</v-icon>
<span>{{ labels.dialogTitle || '选择项目' }}</span>
</v-card-title>
<v-divider />
<v-card-text
class="pa-0"
style="height: 600px; max-height: 80vh; overflow: hidden;"
>
<div class="selector-layout">
<!-- 左侧文件夹树 -->
<div class="folder-sidebar">
<div class="sidebar-header pa-3 pb-2">
<span class="text-caption text-medium-emphasis font-weight-medium">
<v-icon
size="small"
class="mr-1"
>mdi-folder-multiple</v-icon>
文件夹
</span>
</div>
<v-list
density="compact"
nav
class="tree-list pa-2"
bg-color="transparent"
>
<!-- 根目录 -->
<v-list-item
:active="currentFolderId === null"
rounded="lg"
class="mb-1 root-item"
@click="navigateToFolder(null)"
>
<template #prepend>
<v-icon
size="20"
:color="currentFolderId === null ? 'primary' : ''"
>
mdi-home
</v-icon>
</template>
<v-list-item-title class="text-body-2">
{{ labels.rootFolder || '根目录' }}
</v-list-item-title>
</v-list-item>
<!-- 文件夹树 -->
<template v-if="!treeLoading">
<BaseMoveTargetNode
v-for="folder in folderTree"
:key="folder.folder_id"
:folder="folder"
:depth="0"
:selected-folder-id="currentFolderId"
:disabled-folder-ids="[]"
@select="navigateToFolder"
/>
</template>
<div
v-if="treeLoading"
class="text-center pa-4"
>
<v-progress-circular
indeterminate
size="20"
color="primary"
/>
</div>
</v-list>
</div>
<!-- 右侧项目列表 -->
<div class="items-panel">
<!-- 面包屑导航 -->
<div class="breadcrumb-bar px-4 py-3">
<v-breadcrumbs
:items="breadcrumbItems"
density="compact"
class="pa-0"
>
<template #item="{ item }">
<v-breadcrumbs-item
:disabled="(item as any).disabled"
:class="{ 'breadcrumb-link': !(item as any).disabled }"
@click="!(item as any).disabled && navigateToFolder((item as any).folderId)"
>
<v-icon
v-if="(item as any).isRoot"
size="small"
class="mr-1"
>
mdi-home
</v-icon>
{{ item.title }}
</v-breadcrumbs-item>
</template>
<template #divider>
<v-icon
size="small"
color="grey"
>
mdi-chevron-right
</v-icon>
</template>
</v-breadcrumbs>
</div>
<v-divider />
<!-- 项目列表 -->
<div class="items-list">
<v-progress-linear
v-if="itemsLoading"
indeterminate
color="primary"
height="2"
/>
<!-- 子文件夹 -->
<v-list
v-if="!itemsLoading"
lines="two"
class="pa-3 items-content"
>
<template v-if="currentSubFolders.length > 0">
<div class="section-label text-caption text-medium-emphasis mb-2 px-2">
子文件夹
</div>
<v-list-item
v-for="folder in currentSubFolders"
:key="'folder-' + folder.folder_id"
rounded="lg"
class="mb-1 folder-item"
@click="navigateToFolder(folder.folder_id)"
>
<template #prepend>
<v-avatar
size="36"
color="amber-lighten-4"
class="mr-3"
>
<v-icon
color="amber-darken-2"
size="20"
>
mdi-folder
</v-icon>
</v-avatar>
</template>
<v-list-item-title class="font-weight-medium">
{{ folder.name }}
</v-list-item-title>
<template #append>
<v-icon
size="20"
color="grey"
>
mdi-chevron-right
</v-icon>
</template>
</v-list-item>
</template>
<!-- 项目列表 -->
<template v-if="currentItems.length > 0">
<div
class="section-label text-caption text-medium-emphasis mb-2 px-2"
:class="{ 'mt-4': currentSubFolders.length > 0 }"
>
可选项目
</div>
<v-list-item
v-for="item in currentItems"
:key="'item-' + getItemId(item)"
:value="getItemId(item)"
:active="selectedItemId === getItemId(item)"
rounded="lg"
class="mb-1 persona-item"
:class="{ 'selected-item': selectedItemId === getItemId(item) }"
@click="selectItem(item)"
>
<template #prepend>
<v-avatar
size="36"
:color="selectedItemId === getItemId(item) ? 'primary-lighten-4' : 'grey-lighten-3'"
class="mr-3"
>
<v-icon
:color="selectedItemId === getItemId(item) ? 'primary' : 'grey-darken-1'"
size="20"
>
mdi-account
</v-icon>
</v-avatar>
</template>
<v-list-item-title class="font-weight-medium">
{{ getItemName(item) }}
</v-list-item-title>
<v-list-item-subtitle
v-if="getItemDescription(item)"
class="text-truncate"
>
{{ truncateText(getItemDescription(item), 80) }}
</v-list-item-subtitle>
<template #append>
<div class="d-flex align-center ga-1">
<v-btn
v-if="showEditButton && !isDefaultItem(item)"
icon="mdi-pencil"
size="small"
variant="text"
:title="labels.editButton || 'Edit'"
@click.stop="handleEditItem(item)"
/>
<v-icon
v-if="selectedItemId === getItemId(item)"
color="primary"
size="22"
>
mdi-check-circle
</v-icon>
</div>
</template>
</v-list-item>
</template>
<!-- 空状态 -->
<div
v-if="currentSubFolders.length === 0 && currentItems.length === 0"
class="empty-state text-center py-12"
>
<v-icon
size="64"
color="grey-lighten-2"
>
mdi-folder-open-outline
</v-icon>
<p class="text-grey mt-4 text-body-2">
{{ labels.emptyFolder || labels.noItems || '此文件夹为空' }}
</p>
</div>
</v-list>
</div>
</div>
</div>
</v-card-text>
<v-card-actions class="pa-4">
<v-btn
v-if="showCreateButton"
variant="text"
color="primary"
prepend-icon="mdi-plus"
@click="$emit('create')"
>
{{ labels.createButton || '新建' }}
</v-btn>
<v-spacer />
<v-btn
variant="text"
@click="cancelSelection"
>
{{ labels.cancelButton || '取消' }}
</v-btn>
<v-btn
color="primary"
:disabled="!selectedItemId"
@click="confirmSelection"
>
{{ labels.confirmButton || '确认' }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script lang="ts">

View File

@@ -1,74 +1,136 @@
<template>
<div class="base-folder-tree">
<!-- 搜索框 -->
<v-text-field v-model="searchQuery" :placeholder="labels.searchPlaceholder" prepend-inner-icon="mdi-magnify"
variant="outlined" density="compact" hide-details clearable class="mb-3" />
<div class="base-folder-tree">
<!-- 搜索框 -->
<v-text-field
v-model="searchQuery"
:placeholder="labels.searchPlaceholder"
prepend-inner-icon="mdi-magnify"
variant="outlined"
density="compact"
hide-details
clearable
class="mb-3"
/>
<!-- 根目录节点 -->
<v-list density="compact" nav class="tree-list" bg-color="transparent">
<v-list-item :active="currentFolderId === null" @click="handleFolderClick(null)" rounded="lg"
:class="['root-item', { 'drag-over': isRootDragOver }]"
@dragover.prevent="handleRootDragOver" @dragleave="handleRootDragLeave" @drop.prevent="handleRootDrop">
<template v-slot:prepend>
<v-icon>mdi-home</v-icon>
</template>
<v-list-item-title>{{ labels.rootFolder }}</v-list-item-title>
</v-list-item>
<!-- 根目录节点 -->
<v-list
density="compact"
nav
class="tree-list"
bg-color="transparent"
>
<v-list-item
:active="currentFolderId === null"
rounded="lg"
:class="['root-item', { 'drag-over': isRootDragOver }]"
@click="handleFolderClick(null)"
@dragover.prevent="handleRootDragOver"
@dragleave="handleRootDragLeave"
@drop.prevent="handleRootDrop"
>
<template #prepend>
<v-icon>mdi-home</v-icon>
</template>
<v-list-item-title>{{ labels.rootFolder }}</v-list-item-title>
</v-list-item>
<!-- 文件夹树 -->
<template v-if="!treeLoading">
<BaseFolderTreeNode v-for="folder in filteredFolderTree" :key="folder.folder_id" :folder="folder"
:depth="0" :current-folder-id="currentFolderId" :search-query="searchQuery"
:expanded-folder-ids="expandedFolderIds" :accept-drop-types="acceptDropTypes"
@folder-click="handleFolderClick" @folder-context-menu="handleContextMenu"
@item-dropped="$emit('item-dropped', $event)"
@toggle-expansion="$emit('toggle-expansion', $event)"
@set-expansion="$emit('set-expansion', $event)" />
</template>
<!-- 文件夹树 -->
<template v-if="!treeLoading">
<BaseFolderTreeNode
v-for="folder in filteredFolderTree"
:key="folder.folder_id"
:folder="folder"
:depth="0"
:current-folder-id="currentFolderId"
:search-query="searchQuery"
:expanded-folder-ids="expandedFolderIds"
:accept-drop-types="acceptDropTypes"
@folder-click="handleFolderClick"
@folder-context-menu="handleContextMenu"
@item-dropped="$emit('item-dropped', $event)"
@toggle-expansion="$emit('toggle-expansion', $event)"
@set-expansion="$emit('set-expansion', $event)"
/>
</template>
<!-- 加载状态 -->
<div v-if="treeLoading" class="text-center pa-4">
<v-progress-circular indeterminate size="24" />
</div>
<!-- 加载状态 -->
<div
v-if="treeLoading"
class="text-center pa-4"
>
<v-progress-circular
indeterminate
size="24"
/>
</div>
<!-- 空状态 -->
<div v-if="!treeLoading && folderTree.length === 0" class="text-center pa-4 text-medium-emphasis">
<v-icon size="32" class="mb-2">mdi-folder-outline</v-icon>
<div class="text-body-2">{{ labels.noFolders }}</div>
</div>
</v-list>
<!-- 空状态 -->
<div
v-if="!treeLoading && folderTree.length === 0"
class="text-center pa-4 text-medium-emphasis"
>
<v-icon
size="32"
class="mb-2"
>
mdi-folder-outline
</v-icon>
<div class="text-body-2">
{{ labels.noFolders }}
</div>
</div>
</v-list>
<!-- 右键菜单 -->
<v-menu v-model="contextMenu.show" :target="contextMenu.target as any" location="end" :close-on-content-click="true">
<v-list density="compact">
<v-list-item @click="openFolder">
<template v-slot:prepend>
<v-icon size="small">mdi-folder-open</v-icon>
</template>
<v-list-item-title>{{ mergedLabels.contextMenu.open }}</v-list-item-title>
</v-list-item>
<v-list-item @click="$emit('rename-folder', contextMenu.folder)">
<template v-slot:prepend>
<v-icon size="small">mdi-pencil</v-icon>
</template>
<v-list-item-title>{{ mergedLabels.contextMenu.rename }}</v-list-item-title>
</v-list-item>
<v-list-item @click="$emit('move-folder', contextMenu.folder)">
<template v-slot:prepend>
<v-icon size="small">mdi-folder-move</v-icon>
</template>
<v-list-item-title>{{ mergedLabels.contextMenu.moveTo }}</v-list-item-title>
</v-list-item>
<v-divider class="my-1" />
<v-list-item @click="$emit('delete-folder', contextMenu.folder)" class="text-error">
<template v-slot:prepend>
<v-icon size="small" color="error">mdi-delete</v-icon>
</template>
<v-list-item-title>{{ mergedLabels.contextMenu.delete }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</div>
<!-- 右键菜单 -->
<v-menu
v-model="contextMenu.show"
:target="contextMenu.target as any"
location="end"
:close-on-content-click="true"
>
<v-list density="compact">
<v-list-item @click="openFolder">
<template #prepend>
<v-icon size="small">
mdi-folder-open
</v-icon>
</template>
<v-list-item-title>{{ mergedLabels.contextMenu.open }}</v-list-item-title>
</v-list-item>
<v-list-item @click="$emit('rename-folder', contextMenu.folder)">
<template #prepend>
<v-icon size="small">
mdi-pencil
</v-icon>
</template>
<v-list-item-title>{{ mergedLabels.contextMenu.rename }}</v-list-item-title>
</v-list-item>
<v-list-item @click="$emit('move-folder', contextMenu.folder)">
<template #prepend>
<v-icon size="small">
mdi-folder-move
</v-icon>
</template>
<v-list-item-title>{{ mergedLabels.contextMenu.moveTo }}</v-list-item-title>
</v-list-item>
<v-divider class="my-1" />
<v-list-item
class="text-error"
@click="$emit('delete-folder', contextMenu.folder)"
>
<template #prepend>
<v-icon
size="small"
color="error"
>
mdi-delete
</v-icon>
</template>
<v-list-item-title>{{ mergedLabels.contextMenu.delete }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</div>
</template>
<script lang="ts">

View File

@@ -1,36 +1,63 @@
<template>
<div class="base-folder-tree-node">
<v-list-item :active="currentFolderId === folder.folder_id" @click.stop="$emit('folder-click', folder.folder_id)"
@contextmenu.prevent="handleContextMenu" rounded="lg" :style="{ paddingLeft: `${(depth + 1) * 16}px` }"
:class="['folder-item', { 'drag-over': isDragOver }]"
@dragover.prevent="handleDragOver" @dragleave="handleDragLeave" @drop.prevent="handleDrop">
<template v-slot:prepend>
<v-btn v-if="hasChildren" icon variant="text" size="x-small" @click.stop="toggleExpand"
class="expand-btn">
<v-icon size="16">{{ isExpanded ? 'mdi-chevron-down' : 'mdi-chevron-right' }}</v-icon>
</v-btn>
<div v-else class="expand-placeholder"></div>
<v-icon :color="currentFolderId === folder.folder_id ? 'primary' : ''">
{{ isExpanded ? 'mdi-folder-open' : 'mdi-folder' }}
</v-icon>
</template>
<v-list-item-title class="text-truncate">{{ folder.name }}</v-list-item-title>
</v-list-item>
<div class="base-folder-tree-node">
<v-list-item
:active="currentFolderId === folder.folder_id"
rounded="lg"
:style="{ paddingLeft: `${(depth + 1) * 16}px` }"
:class="['folder-item', { 'drag-over': isDragOver }]"
@click.stop="$emit('folder-click', folder.folder_id)"
@contextmenu.prevent="handleContextMenu"
@dragover.prevent="handleDragOver"
@dragleave="handleDragLeave"
@drop.prevent="handleDrop"
>
<template #prepend>
<v-btn
v-if="hasChildren"
icon
variant="text"
size="x-small"
class="expand-btn"
@click.stop="toggleExpand"
>
<v-icon size="16">
{{ isExpanded ? 'mdi-chevron-down' : 'mdi-chevron-right' }}
</v-icon>
</v-btn>
<div
v-else
class="expand-placeholder"
/>
<v-icon :color="currentFolderId === folder.folder_id ? 'primary' : ''">
{{ isExpanded ? 'mdi-folder-open' : 'mdi-folder' }}
</v-icon>
</template>
<v-list-item-title class="text-truncate">
{{ folder.name }}
</v-list-item-title>
</v-list-item>
<!-- 子文件夹 -->
<v-expand-transition>
<div v-show="isExpanded && hasChildren">
<BaseFolderTreeNode v-for="child in folder.children" :key="child.folder_id" :folder="child" :depth="depth + 1"
:current-folder-id="currentFolderId" :search-query="searchQuery"
:expanded-folder-ids="expandedFolderIds" :accept-drop-types="acceptDropTypes"
@folder-click="$emit('folder-click', $event)"
@folder-context-menu="$emit('folder-context-menu', $event)"
@item-dropped="$emit('item-dropped', $event)"
@toggle-expansion="$emit('toggle-expansion', $event)"
@set-expansion="$emit('set-expansion', $event)" />
</div>
</v-expand-transition>
</div>
<!-- 子文件夹 -->
<v-expand-transition>
<div v-show="isExpanded && hasChildren">
<BaseFolderTreeNode
v-for="child in folder.children"
:key="child.folder_id"
:folder="child"
:depth="depth + 1"
:current-folder-id="currentFolderId"
:search-query="searchQuery"
:expanded-folder-ids="expandedFolderIds"
:accept-drop-types="acceptDropTypes"
@folder-click="$emit('folder-click', $event)"
@folder-context-menu="$emit('folder-context-menu', $event)"
@item-dropped="$emit('item-dropped', $event)"
@toggle-expansion="$emit('toggle-expansion', $event)"
@set-expansion="$emit('set-expansion', $event)"
/>
</div>
</v-expand-transition>
</div>
</template>
<script lang="ts">

View File

@@ -1,30 +1,55 @@
<template>
<div class="base-move-target-node">
<v-list-item :active="selectedFolderId === folder.folder_id" :disabled="isDisabled"
@click.stop="!isDisabled && $emit('select', folder.folder_id)" rounded="lg"
:style="{ paddingLeft: `${(depth + 1) * 16}px` }" class="folder-item">
<template v-slot:prepend>
<v-btn v-if="hasChildren" icon variant="text" size="x-small" @click.stop="toggleExpand"
class="expand-btn" :disabled="isDisabled">
<v-icon size="16">{{ isExpanded ? 'mdi-chevron-down' : 'mdi-chevron-right' }}</v-icon>
</v-btn>
<div v-else class="expand-placeholder"></div>
<v-icon :color="isDisabled ? 'grey' : (selectedFolderId === folder.folder_id ? 'primary' : '')">
{{ isExpanded ? 'mdi-folder-open' : 'mdi-folder' }}
</v-icon>
</template>
<v-list-item-title class="text-truncate">{{ folder.name }}</v-list-item-title>
</v-list-item>
<div class="base-move-target-node">
<v-list-item
:active="selectedFolderId === folder.folder_id"
:disabled="isDisabled"
rounded="lg"
:style="{ paddingLeft: `${(depth + 1) * 16}px` }"
class="folder-item"
@click.stop="!isDisabled && $emit('select', folder.folder_id)"
>
<template #prepend>
<v-btn
v-if="hasChildren"
icon
variant="text"
size="x-small"
class="expand-btn"
:disabled="isDisabled"
@click.stop="toggleExpand"
>
<v-icon size="16">
{{ isExpanded ? 'mdi-chevron-down' : 'mdi-chevron-right' }}
</v-icon>
</v-btn>
<div
v-else
class="expand-placeholder"
/>
<v-icon :color="isDisabled ? 'grey' : (selectedFolderId === folder.folder_id ? 'primary' : '')">
{{ isExpanded ? 'mdi-folder-open' : 'mdi-folder' }}
</v-icon>
</template>
<v-list-item-title class="text-truncate">
{{ folder.name }}
</v-list-item-title>
</v-list-item>
<!-- 子文件夹 -->
<v-expand-transition>
<div v-show="isExpanded && hasChildren">
<BaseMoveTargetNode v-for="child in folder.children" :key="child.folder_id" :folder="child" :depth="depth + 1"
:selected-folder-id="selectedFolderId" :disabled-folder-ids="disabledFolderIds"
@select="$emit('select', $event)" />
</div>
</v-expand-transition>
</div>
<!-- 子文件夹 -->
<v-expand-transition>
<div v-show="isExpanded && hasChildren">
<BaseMoveTargetNode
v-for="child in folder.children"
:key="child.folder_id"
:folder="child"
:depth="depth + 1"
:selected-folder-id="selectedFolderId"
:disabled-folder-ids="disabledFolderIds"
@select="$emit('select', $event)"
/>
</div>
</v-expand-transition>
</div>
</template>
<script lang="ts">

View File

@@ -1,52 +1,86 @@
<template>
<v-dialog v-model="showDialog" max-width="500px" persistent>
<v-card>
<v-card-title>
<v-icon class="mr-2">mdi-folder-move</v-icon>
{{ labels.title }}
</v-card-title>
<v-card-text>
<p class="text-body-2 text-medium-emphasis mb-4">
{{ labels.description }}
</p>
<v-dialog
v-model="showDialog"
max-width="500px"
persistent
>
<v-card>
<v-card-title>
<v-icon class="mr-2">
mdi-folder-move
</v-icon>
{{ labels.title }}
</v-card-title>
<v-card-text>
<p class="text-body-2 text-medium-emphasis mb-4">
{{ labels.description }}
</p>
<!-- 文件夹选择树 -->
<div class="folder-select-tree">
<v-list density="compact" nav class="tree-list">
<!-- 根目录选项 -->
<v-list-item :active="selectedFolderId === null" @click="selectFolder(null)" rounded="lg"
class="mb-1">
<template v-slot:prepend>
<v-icon>mdi-home</v-icon>
</template>
<v-list-item-title>{{ labels.rootFolder }}</v-list-item-title>
</v-list-item>
<!-- 文件夹选择树 -->
<div class="folder-select-tree">
<v-list
density="compact"
nav
class="tree-list"
>
<!-- 根目录选项 -->
<v-list-item
:active="selectedFolderId === null"
rounded="lg"
class="mb-1"
@click="selectFolder(null)"
>
<template #prepend>
<v-icon>mdi-home</v-icon>
</template>
<v-list-item-title>{{ labels.rootFolder }}</v-list-item-title>
</v-list-item>
<!-- 文件夹树 -->
<template v-if="!treeLoading">
<BaseMoveTargetNode v-for="folder in folderTree" :key="folder.folder_id" :folder="folder"
:depth="0" :selected-folder-id="selectedFolderId" :disabled-folder-ids="disabledFolderIds"
@select="selectFolder" />
</template>
<!-- 文件夹树 -->
<template v-if="!treeLoading">
<BaseMoveTargetNode
v-for="folder in folderTree"
:key="folder.folder_id"
:folder="folder"
:depth="0"
:selected-folder-id="selectedFolderId"
:disabled-folder-ids="disabledFolderIds"
@select="selectFolder"
/>
</template>
<!-- 加载状态 -->
<div v-if="treeLoading" class="text-center pa-4">
<v-progress-circular indeterminate size="24" />
</div>
</v-list>
</div>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="closeDialog">
{{ labels.cancelButton }}
</v-btn>
<v-btn color="primary" variant="flat" @click="submitMove" :loading="loading">
{{ labels.moveButton }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 加载状态 -->
<div
v-if="treeLoading"
class="text-center pa-4"
>
<v-progress-circular
indeterminate
size="24"
/>
</div>
</v-list>
</div>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
variant="text"
@click="closeDialog"
>
{{ labels.cancelButton }}
</v-btn>
<v-btn
color="primary"
variant="flat"
:loading="loading"
@click="submitMove"
>
{{ labels.moveButton }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script lang="ts">

View File

@@ -1,11 +1,27 @@
<template>
<v-dialog v-model="showDialog" max-width="800px" max-height="90%" @after-enter="prepareData">
<v-dialog
v-model="showDialog"
max-width="800px"
max-height="90%"
@after-enter="prepareData"
>
<v-card
:title="updatingMode ? `${tm('dialog.edit')} ${updatingPlatformConfig.id} ${tm('dialog.adapter')}` : tm('dialog.addPlatform')">
<v-card-text ref="dialogScrollContainer" class="pa-4 ml-2" style="overflow-y: auto;">
<div class="d-flex align-start" style="width: 100%;">
:title="updatingMode ? `${tm('dialog.edit')} ${updatingPlatformConfig.id} ${tm('dialog.adapter')}` : tm('dialog.addPlatform')"
>
<v-card-text
ref="dialogScrollContainer"
class="pa-4 ml-2"
style="overflow-y: auto;"
>
<div
class="d-flex align-start"
style="width: 100%;"
>
<div>
<v-icon icon="mdi-numeric-1-circle" class="mr-3"></v-icon>
<v-icon
icon="mdi-numeric-1-circle"
class="mr-3"
/>
</div>
<div style="flex: 1;">
<h3>
@@ -13,52 +29,87 @@
</h3>
<small style="color: grey;">{{ tm('createDialog.step1Hint') }}</small>
<div>
<div v-if="!updatingMode">
<v-select v-model="selectedPlatformType" :items="Object.keys(platformTemplates)" item-title="name"
item-value="name" :label="tm('createDialog.platformTypeLabel')" variant="outlined" rounded="md" dense hide-details class="mt-6"
style="max-width: 30%; min-width: 300px;">
<template v-slot:item="{ props: itemProps, item }">
<v-select
v-model="selectedPlatformType"
:items="Object.keys(platformTemplates)"
item-title="name"
item-value="name"
:label="tm('createDialog.platformTypeLabel')"
variant="outlined"
rounded="md"
dense
hide-details
class="mt-6"
style="max-width: 30%; min-width: 300px;"
>
<template #item="{ props: itemProps, item }">
<v-list-item v-bind="itemProps">
<template v-slot:prepend>
<img :src="getPlatformIcon(platformTemplates[item.raw].type)"
style="width: 32px; height: 32px; object-fit: contain; margin-right: 16px;" />
<template #prepend>
<img
:src="getPlatformIcon(platformTemplates[item.raw].type)"
style="width: 32px; height: 32px; object-fit: contain; margin-right: 16px;"
>
</template>
</v-list-item>
</template>
</v-select>
<div class="mt-3" v-if="selectedPlatformConfig">
<v-btn color="info" variant="tonal" @click="openTutorial" class="mt-2">
<v-icon start>mdi-book-open-variant</v-icon>
<div
v-if="selectedPlatformConfig"
class="mt-3"
>
<v-btn
color="info"
variant="tonal"
class="mt-2"
@click="openTutorial"
>
<v-icon start>
mdi-book-open-variant
</v-icon>
{{ tm('dialog.viewTutorial') }}
</v-btn>
<div class="mt-2">
<AstrBotConfig :iterable="selectedPlatformConfig" :metadata="metadata['platform_group']?.metadata"
metadataKey="platform" />
<AstrBotConfig
:iterable="selectedPlatformConfig"
:metadata="metadata['platform_group']?.metadata"
metadata-key="platform"
/>
</div>
</div>
</div>
<div v-else>
<v-text-field :label="tm('createDialog.platformTypeLabel')" variant="outlined" rounded="md" dense hide-details class="mt-6"
style="max-width: 30%; min-width: 300px;" v-model="updatingPlatformConfig.type"
disabled></v-text-field>
<v-text-field
:model-value="updatingPlatformConfig.type"
:label="tm('createDialog.platformTypeLabel')"
variant="outlined"
rounded="md"
dense
hide-details
class="mt-6"
style="max-width: 30%; min-width: 300px;"
disabled
/>
<div class="mt-3">
<div class="mt-2">
<AstrBotConfig :iterable="updatingPlatformConfig" :metadata="metadata['platform_group']?.metadata"
metadataKey="platform" />
<AstrBotConfig
:iterable="updatingPlatformConfig"
:metadata="metadata['platform_group']?.metadata"
metadata-key="platform"
/>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex align-start mt-6">
<div>
<v-icon icon="mdi-numeric-2-circle" class="mr-3"></v-icon>
<v-icon
icon="mdi-numeric-2-circle"
class="mr-3"
/>
</div>
<div style="flex: 1;">
<div class="d-flex align-center justify-space-between">
@@ -67,46 +118,93 @@
<h3>
{{ tm('createDialog.configFileTitle') }}
</h3>
<v-chip size="x-small" color="primary" variant="tonal" rounded="sm" class="ml-2"
v-if="!updatingMode">{{ tm('createDialog.optional') }}</v-chip>
<v-chip
v-if="!updatingMode"
size="x-small"
color="primary"
variant="tonal"
rounded="sm"
class="ml-2"
>
{{ tm('createDialog.optional') }}
</v-chip>
</div>
<small style="color: grey;">{{ tm('createDialog.configHint') }}</small>
<small style="color: grey;" v-if="!updatingMode">{{ tm('createDialog.configDefaultHint') }}</small>
<small
v-if="!updatingMode"
style="color: grey;"
>{{ tm('createDialog.configDefaultHint') }}</small>
</div>
<div>
<v-btn variant="plain" icon @click="toggleConfigSection" class="mt-2">
<v-btn
variant="plain"
icon
class="mt-2"
@click="toggleConfigSection"
>
<v-icon>{{ showConfigSection ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
</v-btn>
</div>
</div>
<div v-if="showConfigSection">
<div v-if="!updatingMode">
<v-radio-group class="mt-2" v-model="aBConfigRadioVal" hide-details="true">
<v-radio-group
v-model="aBConfigRadioVal"
class="mt-2"
hide-details="true"
>
<v-radio value="0">
<template v-slot:label>
<template #label>
<span>{{ tm('createDialog.useExistingConfig') }}</span>
</template>
</v-radio>
<div class="d-flex align-center ml-10 my-2" v-if="aBConfigRadioVal === '0'">
<v-select v-model="selectedAbConfId" :items="configInfoList" item-title="name"
item-value="id" :label="tm('createDialog.selectConfigLabel')" variant="outlined" rounded="md" dense hide-details
style="max-width: 30%; min-width: 200px;">
</v-select>
<v-btn icon variant="text" density="comfortable" class="ml-2"
:disabled="!selectedAbConfId" @click="openConfigDrawer(selectedAbConfId)">
<div
v-if="aBConfigRadioVal === '0'"
class="d-flex align-center ml-10 my-2"
>
<v-select
v-model="selectedAbConfId"
:items="configInfoList"
item-title="name"
item-value="id"
:label="tm('createDialog.selectConfigLabel')"
variant="outlined"
rounded="md"
dense
hide-details
style="max-width: 30%; min-width: 200px;"
/>
<v-btn
icon
variant="text"
density="comfortable"
class="ml-2"
:disabled="!selectedAbConfId"
@click="openConfigDrawer(selectedAbConfId)"
>
<v-icon>mdi-arrow-top-right-thick</v-icon>
</v-btn>
</div>
<v-radio value="1" :label="tm('createDialog.createNewConfig')">
</v-radio>
<div class="d-flex align-center" v-if="aBConfigRadioVal === '1'">
<v-text-field v-model="selectedAbConfId" :label="tm('createDialog.newConfigNameLabel')" variant="outlined" rounded="md" dense
hide-details style="max-width: 30%; min-width: 200px;" class="ml-10 my-2">
</v-text-field>
<v-radio
value="1"
:label="tm('createDialog.createNewConfig')"
/>
<div
v-if="aBConfigRadioVal === '1'"
class="d-flex align-center"
>
<v-text-field
v-model="selectedAbConfId"
:label="tm('createDialog.newConfigNameLabel')"
variant="outlined"
rounded="md"
dense
hide-details
style="max-width: 30%; min-width: 200px;"
class="ml-10 my-2"
/>
</div>
</v-radio-group>
<!-- 现有配置文件预览区域 -->
@@ -126,132 +224,268 @@
</div> -->
<!-- 新配置文件编辑区域 -->
<div v-if="aBConfigRadioVal === '1'" class="mt-4">
<div v-if="newConfigLoading" class="d-flex justify-center py-4">
<v-progress-circular indeterminate color="primary"></v-progress-circular>
<div
v-if="aBConfigRadioVal === '1'"
class="mt-4"
>
<div
v-if="newConfigLoading"
class="d-flex justify-center py-4"
>
<v-progress-circular
indeterminate
color="primary"
/>
</div>
<div v-else-if="newConfigData && newConfigMetadata" class="config-preview-container">
<h4 class="mb-3">{{ tm('createDialog.newConfigTitle') }}</h4>
<AstrBotCoreConfigWrapper :metadata="newConfigMetadata" :config_data="newConfigData" />
<div
v-else-if="newConfigData && newConfigMetadata"
class="config-preview-container"
>
<h4 class="mb-3">
{{ tm('createDialog.newConfigTitle') }}
</h4>
<AstrBotCoreConfigWrapper
:metadata="newConfigMetadata"
:config_data="newConfigData"
/>
</div>
<div v-else class="text-center py-4 text-grey">
<div
v-else
class="text-center py-4 text-grey"
>
<v-icon>mdi-information-outline</v-icon>
<p class="mt-2">{{ tm('createDialog.newConfigLoadFailed') }}</p>
<p class="mt-2">
{{ tm('createDialog.newConfigLoadFailed') }}
</p>
</div>
</div>
</div>
<div v-else>
<div class="mb-3 d-flex align-center justify-space-between">
<div>
<v-btn v-if="isEditingRoutes" color="primary" variant="tonal" @click="addNewRoute" size="small">
<v-icon start>mdi-plus</v-icon>
<v-btn
v-if="isEditingRoutes"
color="primary"
variant="tonal"
size="small"
@click="addNewRoute"
>
<v-icon start>
mdi-plus
</v-icon>
{{ tm('createDialog.addRouteRule') }}
</v-btn>
</div>
<v-btn :color="isEditingRoutes ? 'grey' : 'primary'" variant="tonal" size="small"
@click="toggleEditMode">
<v-icon start>{{ isEditingRoutes ? 'mdi-eye' : 'mdi-pencil' }}</v-icon>
<v-btn
:color="isEditingRoutes ? 'grey' : 'primary'"
variant="tonal"
size="small"
@click="toggleEditMode"
>
<v-icon start>
{{ isEditingRoutes ? 'mdi-eye' : 'mdi-pencil' }}
</v-icon>
{{ isEditingRoutes ? tm('createDialog.viewMode') : tm('createDialog.editMode') }}
</v-btn>
</div>
<v-data-table :headers="routeTableHeaders" :items="platformRoutes" item-value="umop"
:no-data-text="tm('createDialog.noRouteRules')" hide-default-footer :items-per-page="-1" class="mt-2"
variant="outlined">
<template v-slot:item.source="{ item }">
<div class="d-flex align-center" style="min-width: 250px;">
<v-select v-if="isEditingRoutes" v-model="item.messageType" :items="messageTypeOptions"
item-title="label" item-value="value" variant="outlined" density="compact" hide-details
style="max-width: 140px;">
</v-select>
<v-data-table
:headers="routeTableHeaders"
:items="platformRoutes"
item-value="umop"
:no-data-text="tm('createDialog.noRouteRules')"
hide-default-footer
:items-per-page="-1"
class="mt-2"
variant="outlined"
>
<template #item.source="{ item }">
<div
class="d-flex align-center"
style="min-width: 250px;"
>
<v-select
v-if="isEditingRoutes"
v-model="item.messageType"
:items="messageTypeOptions"
item-title="label"
item-value="value"
variant="outlined"
density="compact"
hide-details
style="max-width: 140px;"
/>
<small v-else>{{ getMessageTypeLabel(item.messageType) }}</small>
<small class="mx-1">:</small>
<v-text-field v-if="isEditingRoutes" v-model="item.sessionId" variant="outlined" density="compact"
hide-details :placeholder="tm('createDialog.sessionIdPlaceholder')">
</v-text-field>
<v-text-field
v-if="isEditingRoutes"
v-model="item.sessionId"
variant="outlined"
density="compact"
hide-details
:placeholder="tm('createDialog.sessionIdPlaceholder')"
/>
<small v-else>{{ item.sessionId === '*' ? tm('createDialog.allSessions') : item.sessionId }}</small>
</div>
</template>
<template v-slot:item.configId="{ item }">
<template #item.configId="{ item }">
<div class="d-flex align-center">
<v-select v-if="isEditingRoutes" v-model="item.configId" :items="configInfoList"
item-title="name" item-value="id" variant="outlined" density="compact"
style="min-width: 200px;" hide-details>
</v-select>
<v-select
v-if="isEditingRoutes"
v-model="item.configId"
:items="configInfoList"
item-title="name"
item-value="id"
variant="outlined"
density="compact"
style="min-width: 200px;"
hide-details
/>
<div v-else>
<small>{{ getConfigName(item.configId) }}</small>
</div>
<v-btn icon variant="text" density="compact" class="ml-2"
:disabled="!item.configId" @click="openConfigDrawer(item.configId)">
<v-icon size="18">mdi-arrow-top-right-thick</v-icon>
<v-btn
icon
variant="text"
density="compact"
class="ml-2"
:disabled="!item.configId"
@click="openConfigDrawer(item.configId)"
>
<v-icon size="18">
mdi-arrow-top-right-thick
</v-icon>
</v-btn>
</div>
<small v-if="configInfoList.findIndex(c => c.id === item.configId) === -1" style="color: red;"
class="ml-2">{{ tm('createDialog.configMissing') }}</small>
<small
v-if="configInfoList.findIndex(c => c.id === item.configId) === -1"
style="color: red;"
class="ml-2"
>{{ tm('createDialog.configMissing') }}</small>
</template>
<template v-slot:item.actions="{ item, index }">
<div v-if="isEditingRoutes" class="d-flex align-center">
<v-btn icon size="x-small" variant="text" @click="moveRouteUp(index)" :disabled="index === 0">
<template #item.actions="{ item, index }">
<div
v-if="isEditingRoutes"
class="d-flex align-center"
>
<v-btn
icon
size="x-small"
variant="text"
:disabled="index === 0"
@click="moveRouteUp(index)"
>
<v-icon>mdi-arrow-up</v-icon>
</v-btn>
<v-btn icon size="x-small" variant="text" @click="moveRouteDown(index)"
:disabled="index === platformRoutes.length - 1">
<v-btn
icon
size="x-small"
variant="text"
:disabled="index === platformRoutes.length - 1"
@click="moveRouteDown(index)"
>
<v-icon>mdi-arrow-down</v-icon>
</v-btn>
<v-btn icon size="x-small" variant="text" color="error" @click="deleteRoute(index)">
<v-btn
icon
size="x-small"
variant="text"
color="error"
@click="deleteRoute(index)"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</div>
<span v-else class="text-grey">-</span>
<span
v-else
class="text-grey"
>-</span>
</template>
</v-data-table>
<small class="ml-2 mt-2 d-block" style="color: grey">{{ tm('createDialog.routeHint') }}</small>
<small
class="ml-2 mt-2 d-block"
style="color: grey"
>{{ tm('createDialog.routeHint') }}</small>
</div>
</div>
</div>
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click="closeDialog">{{ tm('dialog.cancel') }}</v-btn>
<v-btn :disabled="!canSave" color="primary" v-if="!updatingMode" @click="newPlatform" :loading="loading">{{
tm('dialog.save') }}</v-btn>
<v-btn :disabled="!selectedAbConfId" color="primary" v-else @click="newPlatform" :loading="loading">{{
tm('dialog.save') }}</v-btn>
<v-spacer />
<v-btn
text
@click="closeDialog"
>
{{ tm('dialog.cancel') }}
</v-btn>
<v-btn
v-if="!updatingMode"
:disabled="!canSave"
color="primary"
:loading="loading"
@click="newPlatform"
>
{{
tm('dialog.save') }}
</v-btn>
<v-btn
v-else
:disabled="!selectedAbConfId"
color="primary"
:loading="loading"
@click="newPlatform"
>
{{
tm('dialog.save') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- ID冲突确认对话框 -->
<v-dialog v-model="showIdConflictDialog" max-width="450" persistent>
<v-dialog
v-model="showIdConflictDialog"
max-width="450"
persistent
>
<v-card>
<v-card-title class="text-h6 bg-warning d-flex align-center">
<v-icon start class="me-2">mdi-alert-circle-outline</v-icon>
<v-icon
start
class="me-2"
>
mdi-alert-circle-outline
</v-icon>
{{ tm('dialog.idConflict.title') }}
</v-card-title>
<v-card-text class="py-4 text-body-1 text-medium-emphasis">
{{ tm('dialog.idConflict.message', { id: conflictId }) }}
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" variant="text" @click="handleIdConflictConfirm(false)">{{ tm('dialog.idConflict.confirm')
}}</v-btn>
<v-spacer />
<v-btn
color="grey"
variant="text"
@click="handleIdConflictConfirm(false)"
>
{{ tm('dialog.idConflict.confirm')
}}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 安全警告对话框 -->
<v-dialog v-model="showOneBotEmptyTokenWarnDialog" max-width="600" persistent>
<v-dialog
v-model="showOneBotEmptyTokenWarnDialog"
max-width="600"
persistent
>
<v-card>
<v-card-title>
{{ tm('dialog.securityWarning.title') }}
@@ -259,15 +493,22 @@
<v-card-text class="py-4">
<p>{{ tm('dialog.securityWarning.aiocqhttpTokenMissing') }}</p>
<span><a
href="https://docs.astrbot.app/deploy/platform/aiocqhttp/napcat.html#%E9%99%84%E5%BD%95-%E5%A2%9E%E5%BC%BA%E8%BF%9E%E6%8E%A5%E5%AE%89%E5%85%A8%E6%80%A7"
target="_blank">{{ tm('dialog.securityWarning.learnMore') }}</a></span>
href="https://docs.astrbot.app/deploy/platform/aiocqhttp/napcat.html#%E9%99%84%E5%BD%95-%E5%A2%9E%E5%BC%BA%E8%BF%9E%E6%8E%A5%E5%AE%89%E5%85%A8%E6%80%A7"
target="_blank"
>{{ tm('dialog.securityWarning.learnMore') }}</a></span>
</v-card-text>
<v-card-actions class="px-4 pb-4">
<v-spacer></v-spacer>
<v-btn color="error" @click="handleOneBotEmptyTokenWarningDismiss(true)">
<v-spacer />
<v-btn
color="error"
@click="handleOneBotEmptyTokenWarningDismiss(true)"
>
{{ tm('createDialog.warningContinue') }}
</v-btn>
<v-btn color="primary" @click="handleOneBotEmptyTokenWarningDismiss(false)">
<v-btn
color="primary"
@click="handleOneBotEmptyTokenWarningDismiss(false)"
>
{{ tm('createDialog.warningEditAgain') }}
</v-btn>
</v-card-actions>
@@ -282,21 +523,34 @@
:scrim="true"
@click:outside="closeConfigDrawer"
>
<v-card class="config-drawer-card" elevation="12">
<v-card
class="config-drawer-card"
elevation="12"
>
<div class="config-drawer-header">
<div>
<span class="text-h6">{{ tm('createDialog.configDrawerTitle') }}</span>
<div v-if="configDrawerTargetId" class="text-caption text-grey">
<div
v-if="configDrawerTargetId"
class="text-caption text-grey"
>
{{ tm('createDialog.configDrawerIdLabel') }}: {{ configDrawerTargetId }}
</div>
</div>
<v-btn icon variant="text" @click="closeConfigDrawer">
<v-btn
icon
variant="text"
@click="closeConfigDrawer"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
<v-divider></v-divider>
<v-divider />
<div class="config-drawer-content">
<ConfigPage v-if="showConfigDrawer" :initial-config-id="configDrawerTargetId" />
<ConfigPage
v-if="showConfigDrawer"
:initial-config-id="configDrawerTargetId"
/>
</div>
</v-card>
</v-overlay>
@@ -314,7 +568,6 @@ import ConfigPage from '@/views/ConfigPage.vue';
export default {
name: 'AddNewPlatform',
components: { AstrBotConfig, AstrBotCoreConfigWrapper, ConfigPage },
emits: ['update:show', 'show-toast', 'refresh-config'],
props: {
show: {
type: Boolean,
@@ -337,6 +590,11 @@ export default {
default: null
}
},
emits: ['update:show', 'show-toast', 'refresh-config'],
setup() {
const { tm } = useModuleI18n('features/platform');
return { tm };
},
data() {
return {
selectedPlatformType: null,
@@ -384,10 +642,6 @@ export default {
originalUpdatingPlatformId: null,
};
},
setup() {
const { tm } = useModuleI18n('features/platform');
return { tm };
},
computed: {
showDialog: {
get() {

View File

@@ -1,72 +1,138 @@
<template>
<v-dialog v-model="showDialog" max-width="1100px" min-height="95%">
<v-card :title="tm('dialogs.addProvider.title')">
<v-card-text style="overflow-y: auto;">
<v-tabs v-model="activeProviderTab" grow>
<v-tab value="agent_runner" class="font-weight-medium px-3">
<v-icon start>mdi-cogs</v-icon>
{{ tm('dialogs.addProvider.tabs.agentRunner') }}
</v-tab>
<v-tab value="speech_to_text" class="font-weight-medium px-3">
<v-icon start>mdi-microphone-message</v-icon>
{{ tm('dialogs.addProvider.tabs.speechToText') }}
</v-tab>
<v-tab value="text_to_speech" class="font-weight-medium px-3">
<v-icon start>mdi-volume-high</v-icon>
{{ tm('dialogs.addProvider.tabs.textToSpeech') }}
</v-tab>
<v-tab value="embedding" class="font-weight-medium px-3">
<v-icon start>mdi-code-json</v-icon>
{{ tm('dialogs.addProvider.tabs.embedding') }}
</v-tab>
<v-tab value="rerank" class="font-weight-medium px-3">
<v-icon start>mdi-compare-vertical</v-icon>
{{ tm('dialogs.addProvider.tabs.rerank') }}
</v-tab>
</v-tabs>
<v-dialog
v-model="showDialog"
max-width="1100px"
min-height="95%"
>
<v-card :title="tm('dialogs.addProvider.title')">
<v-card-text style="overflow-y: auto;">
<v-tabs
v-model="activeProviderTab"
grow
>
<v-tab
value="agent_runner"
class="font-weight-medium px-3"
>
<v-icon start>
mdi-cogs
</v-icon>
{{ tm('dialogs.addProvider.tabs.agentRunner') }}
</v-tab>
<v-tab
value="speech_to_text"
class="font-weight-medium px-3"
>
<v-icon start>
mdi-microphone-message
</v-icon>
{{ tm('dialogs.addProvider.tabs.speechToText') }}
</v-tab>
<v-tab
value="text_to_speech"
class="font-weight-medium px-3"
>
<v-icon start>
mdi-volume-high
</v-icon>
{{ tm('dialogs.addProvider.tabs.textToSpeech') }}
</v-tab>
<v-tab
value="embedding"
class="font-weight-medium px-3"
>
<v-icon start>
mdi-code-json
</v-icon>
{{ tm('dialogs.addProvider.tabs.embedding') }}
</v-tab>
<v-tab
value="rerank"
class="font-weight-medium px-3"
>
<v-icon start>
mdi-compare-vertical
</v-icon>
{{ tm('dialogs.addProvider.tabs.rerank') }}
</v-tab>
</v-tabs>
<v-window v-model="activeProviderTab" class="mt-4">
<v-window-item
v-for="tabType in ['chat_completion', 'agent_runner', 'speech_to_text', 'text_to_speech', 'embedding', 'rerank']"
:key="tabType" :value="tabType">
<v-row class="mt-1">
<v-col v-for="(template, name) in getTemplatesByType(tabType)" :key="name" cols="12" sm="6"
md="4">
<v-card variant="outlined" hover class="provider-card"
@click="selectProviderTemplate(name)">
<div class="provider-card-content">
<div class="provider-card-text">
<v-card-title class="provider-card-title">{{ name }}</v-card-title>
<v-card-text
class="text-caption text-medium-emphasis provider-card-description">
{{ getProviderDescription(template, name) }}
</v-card-text>
</div>
<div class="provider-card-logo">
<img :src="getProviderIcon(template.provider)"
v-if="getProviderIcon(template.provider)" class="provider-logo-img">
<div v-else class="provider-logo-fallback">
{{ name[0].toUpperCase() }}
</div>
</div>
</div>
</v-card>
</v-col>
<v-col v-if="Object.keys(getTemplatesByType(tabType)).length === 0" cols="12">
<v-alert type="info" variant="tonal">
{{ tm('dialogs.addProvider.noTemplates') }}
</v-alert>
</v-col>
</v-row>
</v-window-item>
</v-window>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click="closeDialog">{{ tm('dialogs.config.cancel') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-window
v-model="activeProviderTab"
class="mt-4"
>
<v-window-item
v-for="tabType in ['chat_completion', 'agent_runner', 'speech_to_text', 'text_to_speech', 'embedding', 'rerank']"
:key="tabType"
:value="tabType"
>
<v-row class="mt-1">
<v-col
v-for="(template, name) in getTemplatesByType(tabType)"
:key="name"
cols="12"
sm="6"
md="4"
>
<v-card
variant="outlined"
hover
class="provider-card"
@click="selectProviderTemplate(name)"
>
<div class="provider-card-content">
<div class="provider-card-text">
<v-card-title class="provider-card-title">
{{ name }}
</v-card-title>
<v-card-text
class="text-caption text-medium-emphasis provider-card-description"
>
{{ getProviderDescription(template, name) }}
</v-card-text>
</div>
<div class="provider-card-logo">
<img
v-if="getProviderIcon(template.provider)"
:src="getProviderIcon(template.provider)"
class="provider-logo-img"
>
<div
v-else
class="provider-logo-fallback"
>
{{ name[0].toUpperCase() }}
</div>
</div>
</div>
</v-card>
</v-col>
<v-col
v-if="Object.keys(getTemplatesByType(tabType)).length === 0"
cols="12"
>
<v-alert
type="info"
variant="tonal"
>
{{ tm('dialogs.addProvider.noTemplates') }}
</v-alert>
</v-col>
</v-row>
</v-window-item>
</v-window>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
text
@click="closeDialog"
>
{{ tm('dialogs.config.cancel') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>

View File

@@ -1,8 +1,13 @@
<template>
<div class="mt-4">
<div class="d-flex align-center ga-2 mb-2">
<h3 class="text-h5 font-weight-bold mb-0">{{ tm('models.configured') }}</h3>
<small style="color: grey;" v-if="availableCount">{{ tm('models.available') }} {{ availableCount }}</small>
<h3 class="text-h5 font-weight-bold mb-0">
{{ tm('models.configured') }}
</h3>
<small
v-if="availableCount"
style="color: grey;"
>{{ tm('models.available') }} {{ availableCount }}</small>
<v-text-field
v-model="modelSearchProxy"
density="compact"
@@ -15,14 +20,14 @@
style="max-width: 240px;"
:placeholder="tm('models.searchPlaceholder')"
/>
<v-spacer></v-spacer>
<v-spacer />
<v-btn
color="primary"
prepend-icon="mdi-download"
:loading="loadingModels"
@click="emit('fetch-models')"
variant="tonal"
size="small"
@click="emit('fetch-models')"
>
{{ isSourceModified ? tm('providerSources.saveAndFetchModels') : tm('providerSources.fetchModels') }}
</v-btn>
@@ -44,8 +49,15 @@
style="max-height: 520px; overflow-y: auto; font-family:system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;"
>
<template v-if="entries.length > 0">
<template v-for="entry in entries" :key="entry.type === 'configured' ? `provider-${entry.provider.id}` : `model-${entry.model}`">
<v-tooltip location="top" max-width="400" v-if="entry.type === 'configured'">
<template
v-for="entry in entries"
:key="entry.type === 'configured' ? `provider-${entry.provider.id}` : `model-${entry.model}`"
>
<v-tooltip
v-if="entry.type === 'configured'"
location="top"
max-width="400"
>
<template #activator="{ props }">
<v-list-item
v-bind="props"
@@ -55,63 +67,93 @@
<v-list-item-title class="font-weight-medium text-truncate">
{{ entry.provider.id }}
</v-list-item-title>
<v-list-item-subtitle class="text-caption text-grey d-flex align-center ga-1" style="font-family: monospace;">
<span>{{ entry.provider.model }}</span>
<v-icon v-if="supportsImageInput(entry.metadata)" size="14" color="grey">
mdi-eye-outline
</v-icon>
<v-icon v-if="supportsToolCall(entry.metadata)" size="14" color="grey">
mdi-wrench
</v-icon>
<v-icon v-if="supportsReasoning(entry.metadata)" size="14" color="grey">
mdi-brain
</v-icon>
<span v-if="formatContextLimit(entry.metadata)">
{{ formatContextLimit(entry.metadata) }}
</span>
</v-list-item-subtitle>
<template #append>
<div class="d-flex align-center ga-1" @click.stop>
<v-switch
v-model="entry.provider.enable"
density="compact"
inset
hide-details
color="primary"
class="mr-1"
@update:modelValue="emit('toggle-provider-enable', entry.provider, $event)"
></v-switch>
<v-tooltip location="top" max-width="300">
{{ tm('availability.test') }}
<template #activator="{ props }">
<v-list-item-subtitle
class="text-caption text-grey d-flex align-center ga-1"
style="font-family: monospace;"
>
<span>{{ entry.provider.model }}</span>
<v-icon
v-if="supportsImageInput(entry.metadata)"
size="14"
color="grey"
>
mdi-eye-outline
</v-icon>
<v-icon
v-if="supportsToolCall(entry.metadata)"
size="14"
color="grey"
>
mdi-wrench
</v-icon>
<v-icon
v-if="supportsReasoning(entry.metadata)"
size="14"
color="grey"
>
mdi-brain
</v-icon>
<span v-if="formatContextLimit(entry.metadata)">
{{ formatContextLimit(entry.metadata) }}
</span>
</v-list-item-subtitle>
<template #append>
<div
class="d-flex align-center ga-1"
@click.stop
>
<v-switch
v-model="entry.provider.enable"
density="compact"
inset
hide-details
color="primary"
class="mr-1"
@update:modelValue="emit('toggle-provider-enable', entry.provider, $event)"
/>
<v-tooltip
location="top"
max-width="300"
>
{{ tm('availability.test') }}
<template #activator="{ props }">
<v-btn
icon="mdi-connection"
size="small"
variant="text"
:disabled="!entry.provider.enable"
:loading="isProviderTesting(entry.provider.id)"
v-bind="props"
@click.stop="emit('test-provider', entry.provider)"
/>
</template>
</v-tooltip>
<v-tooltip
location="top"
max-width="300"
>
{{ tm('models.configure') }}
<template #activator="{ props }">
<v-btn
icon="mdi-cog"
size="small"
variant="text"
v-bind="props"
@click.stop="emit('open-provider-edit', entry.provider)"
/>
</template>
</v-tooltip>
<v-btn
icon="mdi-connection"
icon="mdi-delete"
size="small"
variant="text"
:disabled="!entry.provider.enable"
:loading="isProviderTesting(entry.provider.id)"
v-bind="props"
@click.stop="emit('test-provider', entry.provider)"
></v-btn>
</template>
</v-tooltip>
<v-tooltip location="top" max-width="300">
{{ tm('models.configure') }}
<template #activator="{ props }">
<v-btn
icon="mdi-cog"
size="small"
variant="text"
v-bind="props"
@click.stop="emit('open-provider-edit', entry.provider)"
></v-btn>
</template>
</v-tooltip>
<v-btn icon="mdi-delete" size="small" variant="text" color="error" @click.stop="emit('delete-provider', entry.provider)"></v-btn>
</div>
</template>
color="error"
@click.stop="emit('delete-provider', entry.provider)"
/>
</div>
</template>
</v-list-item>
</template>
<div>
@@ -120,27 +162,52 @@
</div>
</v-tooltip>
<v-tooltip location="top" max-width="400" v-else>
<v-tooltip
v-else
location="top"
max-width="400"
>
<template #activator="{ props }">
<v-list-item v-bind="props" class="cursor-pointer" @click="emit('add-model-provider', entry.model)">
<v-list-item
v-bind="props"
class="cursor-pointer"
@click="emit('add-model-provider', entry.model)"
>
<v-list-item-title>{{ entry.model }}</v-list-item-title>
<v-list-item-subtitle class="text-caption text-grey d-flex align-center ga-1">
<span>{{ entry.model }}</span>
<v-icon v-if="supportsImageInput(entry.metadata)" size="14" color="grey">
mdi-eye-outline
</v-icon>
<v-icon v-if="supportsToolCall(entry.metadata)" size="14" color="grey">
mdi-wrench
</v-icon>
<v-icon v-if="supportsReasoning(entry.metadata)" size="14" color="grey">
mdi-brain
</v-icon>
<span v-if="formatContextLimit(entry.metadata)">
{{ formatContextLimit(entry.metadata) }}
</span>
</v-list-item-subtitle>
<v-list-item-subtitle class="text-caption text-grey d-flex align-center ga-1">
<span>{{ entry.model }}</span>
<v-icon
v-if="supportsImageInput(entry.metadata)"
size="14"
color="grey"
>
mdi-eye-outline
</v-icon>
<v-icon
v-if="supportsToolCall(entry.metadata)"
size="14"
color="grey"
>
mdi-wrench
</v-icon>
<v-icon
v-if="supportsReasoning(entry.metadata)"
size="14"
color="grey"
>
mdi-brain
</v-icon>
<span v-if="formatContextLimit(entry.metadata)">
{{ formatContextLimit(entry.metadata) }}
</span>
</v-list-item-subtitle>
<template #append>
<v-btn icon="mdi-plus" size="small" variant="text" color="primary"></v-btn>
<v-btn
icon="mdi-plus"
size="small"
variant="text"
color="primary"
/>
</template>
</v-list-item>
</template>
@@ -152,8 +219,15 @@
</template>
<template v-else>
<div class="text-center pa-4 text-medium-emphasis">
<v-icon size="48" color="grey-lighten-1">mdi-package-variant</v-icon>
<p class="text-grey mt-2">{{ tm('models.empty') }}</p>
<v-icon
size="48"
color="grey-lighten-1"
>
mdi-package-variant
</v-icon>
<p class="text-grey mt-2">
{{ tm('models.empty') }}
</p>
</div>
</template>
</v-list>

View File

@@ -1,8 +1,13 @@
<template>
<v-card class="provider-sources-panel h-100" elevation="0">
<v-card
class="provider-sources-panel h-100"
elevation="0"
>
<div class="d-flex align-center justify-space-between px-4 pt-4 pb-2">
<div class="d-flex align-center ga-2">
<h3 class="mb-0">{{ tm('providerSources.title') }}</h3>
<h3 class="mb-0">
{{ tm('providerSources.title') }}
</h3>
</div>
<StyledMenu>
<template #activator="{ props }">
@@ -24,9 +29,23 @@
@click="emitAddSource(sourceType.value)"
>
<template #prepend>
<v-avatar size="18" rounded="0" class="me-2">
<v-img v-if="sourceType.icon" :src="sourceType.icon" alt="provider icon" cover></v-img>
<v-icon v-else size="16">mdi-shape-outline</v-icon>
<v-avatar
size="18"
rounded="0"
class="me-2"
>
<v-img
v-if="sourceType.icon"
:src="sourceType.icon"
alt="provider icon"
cover
/>
<v-icon
v-else
size="16"
>
mdi-shape-outline
</v-icon>
</v-avatar>
</template>
<v-list-item-title>{{ sourceType.label }}</v-list-item-title>
@@ -34,7 +53,10 @@
</StyledMenu>
</div>
<div v-if="isMobile && displayedProviderSources.length > 0" class="px-4 pb-3">
<div
v-if="isMobile && displayedProviderSources.length > 0"
class="px-4 pb-3"
>
<div class="d-flex align-center ga-2">
<v-select
:model-value="selectedId"
@@ -52,9 +74,23 @@
<template #item="{ props: itemProps, item }">
<v-list-item v-bind="itemProps">
<template #prepend>
<v-avatar size="18" rounded="0" class="me-2">
<v-img v-if="item.raw.icon" :src="item.raw.icon" alt="provider icon" cover></v-img>
<v-icon v-else size="16">mdi-shape-outline</v-icon>
<v-avatar
size="18"
rounded="0"
class="me-2"
>
<v-img
v-if="item.raw.icon"
:src="item.raw.icon"
alt="provider icon"
cover
/>
<v-icon
v-else
size="16"
>
mdi-shape-outline
</v-icon>
</v-avatar>
</template>
</v-list-item>
@@ -67,12 +103,17 @@
size="small"
color="error"
@click.stop="emitDeleteSource(selectedProviderSource)"
></v-btn>
/>
</div>
</div>
<div v-else-if="displayedProviderSources.length > 0">
<v-list class="provider-source-list" nav density="compact" lines="two">
<v-list
class="provider-source-list"
nav
density="compact"
lines="two"
>
<v-list-item
v-for="source in displayedProviderSources"
:key="source.isPlaceholder ? `template-${source.templateKey}` : source.id"
@@ -83,13 +124,34 @@
@click="emitSelectSource(source)"
>
<template #prepend>
<v-avatar size="32" class="bg-grey-lighten-4" rounded="0">
<v-img v-if="source?.provider" :src="resolveSourceIcon(source)" alt="logo" cover></v-img>
<v-icon v-else size="32">mdi-creation</v-icon>
<v-avatar
size="32"
class="bg-grey-lighten-4"
rounded="0"
>
<v-img
v-if="source?.provider"
:src="resolveSourceIcon(source)"
alt="logo"
cover
/>
<v-icon
v-else
size="32"
>
mdi-creation
</v-icon>
</v-avatar>
</template>
<v-list-item-title class="font-weight-bold mb-1" style="font-family: Arial, Helvetica, sans-serif; font-size: 16px;">{{ getSourceDisplayName(source) }}</v-list-item-title>
<v-list-item-subtitle class="text-truncate">{{ source.api_base || 'N/A' }}</v-list-item-subtitle>
<v-list-item-title
class="font-weight-bold mb-1"
style="font-family: Arial, Helvetica, sans-serif; font-size: 16px;"
>
{{ getSourceDisplayName(source) }}
</v-list-item-title>
<v-list-item-subtitle class="text-truncate">
{{ source.api_base || 'N/A' }}
</v-list-item-subtitle>
<template #append>
<div class="d-flex align-center ga-1">
<v-btn
@@ -99,15 +161,25 @@
size="x-small"
color="error"
@click.stop="emitDeleteSource(source)"
></v-btn>
/>
</div>
</template>
</v-list-item>
</v-list>
</div>
<div v-else class="text-center py-8 px-4">
<v-icon size="48" color="grey-lighten-1">mdi-api-off</v-icon>
<p class="text-grey mt-2">{{ tm('providerSources.empty') }}</p>
<div
v-else
class="text-center py-8 px-4"
>
<v-icon
size="48"
color="grey-lighten-1"
>
mdi-api-off
</v-icon>
<p class="text-grey mt-2">
{{ tm('providerSources.empty') }}
</p>
</div>
</v-card>
</template>

View File

@@ -155,6 +155,10 @@ function getItemPath(key) {
return props.pathPrefix ? `${props.pathPrefix}.${key}` : key
}
function setIterableValue(key, value) {
Reflect.set(props.iterable, key, value)
}
function hasVisibleItemsAfter(items, currentIndex) {
const itemEntries = Object.entries(items)
@@ -172,19 +176,28 @@ function hasVisibleItemsAfter(items, currentIndex) {
</script>
<template>
<div class="config-section" v-if="iterable && metadata[metadataKey]?.type === 'object'">
<div
v-if="iterable && metadata[metadataKey]?.type === 'object'"
class="config-section"
>
<v-list-item-title class="config-title">
{{ translateIfKey(metadata[metadataKey]?.description) }} <span class="metadata-key">({{ metadataKey }})</span>
</v-list-item-title>
<v-list-item-subtitle class="config-hint">
<span v-if="metadata[metadataKey]?.obvious_hint && metadata[metadataKey]?.hint" class="important-hint"></span>
<span
v-if="metadata[metadataKey]?.obvious_hint && metadata[metadataKey]?.hint"
class="important-hint"
></span>
{{ translateIfKey(metadata[metadataKey]?.hint) }}
</v-list-item-subtitle>
</div>
<v-card-text class="px-0 py-1">
<!-- Object Type Configuration -->
<div v-if="metadata[metadataKey]?.type === 'object' || metadata[metadataKey]?.config_template" class="object-config">
<div
v-if="metadata[metadataKey]?.type === 'object' || metadata[metadataKey]?.config_template"
class="object-config"
>
<!-- Provider-level hint -->
<v-alert
v-if="providerHint"
@@ -197,26 +210,41 @@ function hasVisibleItemsAfter(items, currentIndex) {
{{ translateIfKey(providerHint) }}
</v-alert>
<div v-for="(val, key, index) in filteredIterable" :key="key" class="config-item">
<div
v-for="(val, key, index) in filteredIterable"
:key="key"
class="config-item"
>
<!-- Nested Object -->
<div v-if="metadata[metadataKey].items[key]?.type === 'object'" class="nested-object">
<div v-if="metadata[metadataKey].items[key] && !metadata[metadataKey].items[key]?.invisible && shouldShowItem(metadata[metadataKey].items[key], key)" class="nested-container">
<div
v-if="metadata[metadataKey].items[key]?.type === 'object'"
class="nested-object"
>
<div
v-if="metadata[metadataKey].items[key] && !metadata[metadataKey].items[key]?.invisible && shouldShowItem(metadata[metadataKey].items[key], key)"
class="nested-container"
>
<v-expand-transition>
<AstrBotConfig
:metadata="metadata[metadataKey].items"
:iterable="iterable[key]"
:metadataKey="key"
:pluginName="pluginName"
:pathPrefix="getItemPath(key)"
>
</AstrBotConfig>
:metadata-key="key"
:plugin-name="pluginName"
:path-prefix="getItemPath(key)"
/>
</v-expand-transition>
</div>
</div>
<!-- Template List -->
<div v-else-if="metadata[metadataKey].items[key]?.type === 'template_list'" class="nested-object w-100">
<div v-if="!metadata[metadataKey].items[key]?.invisible && shouldShowItem(metadata[metadataKey].items[key], key)" class="nested-container">
<div
v-else-if="metadata[metadataKey].items[key]?.type === 'template_list'"
class="nested-object w-100"
>
<div
v-if="!metadata[metadataKey].items[key]?.invisible && shouldShowItem(metadata[metadataKey].items[key], key)"
class="nested-container"
>
<div class="config-section mb-2">
<v-list-item-title class="config-title">
<span v-if="metadata[metadataKey].items[key]?.description">
@@ -226,22 +254,34 @@ function hasVisibleItemsAfter(items, currentIndex) {
<span v-else>{{ key }}</span>
</v-list-item-title>
<v-list-item-subtitle class="config-hint">
<span v-if="metadata[metadataKey].items[key]?.obvious_hint && metadata[metadataKey].items[key]?.hint" class="important-hint"></span>
<span
v-if="metadata[metadataKey].items[key]?.obvious_hint && metadata[metadataKey].items[key]?.hint"
class="important-hint"
></span>
{{ translateIfKey(metadata[metadataKey].items[key]?.hint) }}
</v-list-item-subtitle>
</div>
<!-- eslint-disable-next-line vue/no-mutating-props -->
<TemplateListEditor
v-model="iterable[key]"
:model-value="iterable[key]"
:templates="metadata[metadataKey].items[key]?.templates || {}"
class="config-field"
@update:model-value="setIterableValue(key, $event)"
/>
</div>
</div>
<!-- Regular Property -->
<template v-else>
<v-row v-if="!metadata[metadataKey].items[key]?.invisible && shouldShowItem(metadata[metadataKey].items[key], key)" class="config-row">
<v-col cols="12" sm="6" class="property-info">
<v-row
v-if="!metadata[metadataKey].items[key]?.invisible && shouldShowItem(metadata[metadataKey].items[key], key)"
class="config-row"
>
<v-col
cols="12"
sm="6"
class="property-info"
>
<v-list-item density="compact">
<v-list-item-title class="property-name">
<span v-if="metadata[metadataKey].items[key]?.description">
@@ -252,21 +292,29 @@ function hasVisibleItemsAfter(items, currentIndex) {
</v-list-item-title>
<v-list-item-subtitle class="property-hint">
<span v-if="metadata[metadataKey].items[key]?.obvious_hint && getItemHint(key, metadata[metadataKey].items[key])"
class="important-hint"></span>
<span
v-if="metadata[metadataKey].items[key]?.obvious_hint && getItemHint(key, metadata[metadataKey].items[key])"
class="important-hint"
></span>
{{ translateIfKey(getItemHint(key, metadata[metadataKey].items[key])) }}
</v-list-item-subtitle>
</v-list-item>
</v-col>
<v-col cols="12" sm="6" class="config-input">
<v-col
cols="12"
sm="6"
class="config-input"
>
<!-- eslint-disable-next-line vue/no-mutating-props -->
<ConfigItemRenderer
v-model="iterable[key]"
:model-value="iterable[key]"
:item-meta="metadata[metadataKey].items[key] || null"
:plugin-name="pluginName"
:config-key="getItemPath(key)"
:loading="loadingEmbeddingDim"
:show-fullscreen-btn="!!metadata[metadataKey].items[key]?.editor_mode"
@update:model-value="setIterableValue(key, $event)"
@get-embedding-dim="getEmbeddingDimensions(iterable)"
@open-fullscreen="openEditorDialog(key, iterable, metadata[metadataKey].items[key]?.editor_theme, metadata[metadataKey].items[key]?.editor_language)"
/>
@@ -276,15 +324,22 @@ function hasVisibleItemsAfter(items, currentIndex) {
<v-divider
v-if="hasVisibleItemsAfter(filteredIterable, index) && !metadata[metadataKey].items[key]?.invisible && shouldShowItem(metadata[metadataKey].items[key], key)"
class="config-divider"
></v-divider>
/>
</template>
</div>
</div>
<!-- Simple Value Configuration -->
<div v-else class="simple-config">
<div
v-else
class="simple-config"
>
<v-row class="config-row">
<v-col cols="12" sm="7" class="property-info">
<v-col
cols="12"
sm="7"
class="property-info"
>
<v-list-item density="compact">
<v-list-item-title class="property-name">
{{ metadata[metadataKey]?.description }}
@@ -292,54 +347,80 @@ function hasVisibleItemsAfter(items, currentIndex) {
</v-list-item-title>
<v-list-item-subtitle class="property-hint">
<span v-if="metadata[metadataKey]?.obvious_hint && metadata[metadataKey]?.hint" class="important-hint"></span>
<span
v-if="metadata[metadataKey]?.obvious_hint && metadata[metadataKey]?.hint"
class="important-hint"
></span>
{{ metadata[metadataKey]?.hint }}
</v-list-item-subtitle>
</v-list-item>
</v-col>
<v-col cols="12" sm="5" class="config-input">
<v-col
cols="12"
sm="5"
class="config-input"
>
<!-- eslint-disable-next-line vue/no-mutating-props -->
<TemplateListEditor
v-if="metadata[metadataKey]?.type === 'template_list' && !metadata[metadataKey]?.invisible"
v-model="iterable[metadataKey]"
:model-value="iterable[metadataKey]"
:templates="metadata[metadataKey]?.templates || {}"
class="config-field"
@update:model-value="setIterableValue(metadataKey, $event)"
/>
<!-- eslint-disable-next-line vue/no-mutating-props -->
<ConfigItemRenderer
v-else
v-model="iterable[metadataKey]"
:model-value="iterable[metadataKey]"
:item-meta="metadata[metadataKey]"
:plugin-name="pluginName"
:config-key="getItemPath(metadataKey)"
@update:model-value="setIterableValue(metadataKey, $event)"
/>
</v-col>
</v-row>
<v-divider class="my-2 config-divider"></v-divider>
<v-divider class="my-2 config-divider" />
</div>
</v-card-text>
<!-- Full Screen Editor Dialog -->
<v-dialog v-model="dialog" fullscreen transition="dialog-bottom-transition" scrollable>
<v-dialog
v-model="dialog"
fullscreen
transition="dialog-bottom-transition"
scrollable
>
<v-card>
<v-toolbar color="primary" dark>
<v-btn icon @click="dialog = false">
<v-toolbar
color="primary"
dark
>
<v-btn
icon
@click="dialog = false"
>
<v-icon>mdi-close</v-icon>
</v-btn>
<v-toolbar-title>{{ t('core.common.editor.editingTitle') }} - {{ currentEditingKey }}</v-toolbar-title>
<v-spacer></v-spacer>
<v-spacer />
<v-toolbar-items>
<v-btn variant="text" @click="saveEditedContent">{{ t('core.common.save') }}</v-btn>
<v-btn
variant="text"
@click="saveEditedContent"
>
{{ t('core.common.save') }}
</v-btn>
</v-toolbar-items>
</v-toolbar>
<v-card-text class="pa-0">
<VueMonacoEditor
v-model:value="currentEditingKeyIterable[currentEditingKey]"
:theme="currentEditingTheme"
:language="currentEditingLanguage"
style="height: calc(100vh - 64px);"
v-model:value="currentEditingKeyIterable[currentEditingKey]"
>
</VueMonacoEditor>
/>
</v-card-text>
</v-card>
</v-dialog>

View File

@@ -206,28 +206,51 @@ function getSpecialSubtype(value) {
</script>
<template>
<v-card v-if="shouldShowSection()" style="margin-bottom: 16px; padding-bottom: 8px; background-color: rgb(var(--v-theme-background));"
rounded="md" variant="outlined">
<v-card-text class="config-section" v-if="metadata[metadataKey]?.type === 'object'" style="padding-bottom: 8px;">
<v-card
v-if="shouldShowSection()"
style="margin-bottom: 16px; padding-bottom: 8px; background-color: rgb(var(--v-theme-background));"
rounded="md"
variant="outlined"
>
<v-card-text
v-if="metadata[metadataKey]?.type === 'object'"
class="config-section"
style="padding-bottom: 8px;"
>
<v-list-item-title class="config-title">
{{ translateIfKey(metadata[metadataKey]?.description) }}
</v-list-item-title>
<v-list-item-subtitle class="config-hint">
<span v-if="metadata[metadataKey]?.obvious_hint && metadata[metadataKey]?.hint" class="important-hint"></span>
<span v-html="renderHint(metadata[metadataKey]?.hint)"></span>
<span
v-if="metadata[metadataKey]?.obvious_hint && metadata[metadataKey]?.hint"
class="important-hint"
></span>
<span v-html="renderHint(metadata[metadataKey]?.hint)" />
</v-list-item-subtitle>
</v-card-text>
<!-- Object Type Configuration with JSON Selector Support -->
<div v-if="metadata[metadataKey]?.type === 'object'" class="object-config">
<div v-for="(itemMeta, itemKey, index) in metadata[metadataKey].items" :key="itemKey" class="config-item">
<div
v-if="metadata[metadataKey]?.type === 'object'"
class="object-config"
>
<div
v-for="(itemMeta, itemKey, index) in metadata[metadataKey].items"
:key="itemKey"
class="config-item"
>
<!-- Check if itemKey is a JSON selector -->
<template v-if="shouldShowItem(itemMeta, itemKey)">
<!-- JSON Selector Property -->
<v-row v-if="!itemMeta?.invisible" class="config-row">
<v-col cols="12" sm="6" class="property-info">
<v-row
v-if="!itemMeta?.invisible"
class="config-row"
>
<v-col
cols="12"
sm="6"
class="property-info"
>
<v-list-item density="compact">
<v-list-item-title class="property-name">
{{ translateIfKey(itemMeta?.description) || itemKey }}
@@ -235,12 +258,19 @@ function getSpecialSubtype(value) {
</v-list-item-title>
<v-list-item-subtitle class="property-hint">
<span v-if="itemMeta?.obvious_hint && itemMeta?.hint" class="important-hint"></span>
<span v-html="renderHint(itemMeta?.hint)"></span>
<span
v-if="itemMeta?.obvious_hint && itemMeta?.hint"
class="important-hint"
></span>
<span v-html="renderHint(itemMeta?.hint)" />
</v-list-item-subtitle>
</v-list-item>
</v-col>
<v-col cols="12" sm="6" class="config-input">
<v-col
cols="12"
sm="6"
class="config-input"
>
<TemplateListEditor
v-if="itemMeta?.type === 'template_list'"
v-model="createSelectorModel(itemKey).value"
@@ -258,17 +288,30 @@ function getSpecialSubtype(value) {
</v-row>
<!-- Plugin Set Selector 全宽显示区域 -->
<v-row v-if="!itemMeta?.invisible && itemMeta?._special === 'select_plugin_set'"
class="plugin-set-display-row">
<v-col cols="12" class="plugin-set-display">
<div v-if="createSelectorModel(itemKey).value && createSelectorModel(itemKey).value.length > 0"
class="selected-plugins-full-width">
<v-row
v-if="!itemMeta?.invisible && itemMeta?._special === 'select_plugin_set'"
class="plugin-set-display-row"
>
<v-col
cols="12"
class="plugin-set-display"
>
<div
v-if="createSelectorModel(itemKey).value && createSelectorModel(itemKey).value.length > 0"
class="selected-plugins-full-width"
>
<div class="plugins-header">
<small class="text-grey">{{ t('core.shared.pluginSetSelector.selectedPluginsLabel') }}</small>
</div>
<div class="d-flex flex-wrap ga-2 mt-2">
<v-chip v-for="plugin in (createSelectorModel(itemKey).value || [])" :key="plugin" size="small" label
color="primary" variant="outlined">
<v-chip
v-for="plugin in (createSelectorModel(itemKey).value || [])"
:key="plugin"
size="small"
label
color="primary"
variant="outlined"
>
{{ plugin === '*' ? t('core.shared.pluginSetSelector.allPluginsLabel') : plugin }}
</v-chip>
</div>
@@ -281,35 +324,58 @@ function getSpecialSubtype(value) {
v-if="!itemMeta?.invisible && itemMeta?._special === 'select_persona' && itemKey === 'provider_settings.default_personality'"
class="persona-preview-row"
>
<v-col cols="12" class="persona-preview-display">
<v-col
cols="12"
class="persona-preview-display"
>
<PersonaQuickPreview :model-value="createSelectorModel(itemKey).value" />
</v-col>
</v-row>
</template>
<v-divider class="config-divider"
v-if="shouldShowItem(itemMeta, itemKey) && hasVisibleItemsAfter(metadata[metadataKey].items, index)"></v-divider>
<v-divider
v-if="shouldShowItem(itemMeta, itemKey) && hasVisibleItemsAfter(metadata[metadataKey].items, index)"
class="config-divider"
/>
</div>
</div>
</v-card>
<!-- Full Screen Editor Dialog -->
<v-dialog v-model="dialog" fullscreen transition="dialog-bottom-transition" scrollable>
<v-dialog
v-model="dialog"
fullscreen
transition="dialog-bottom-transition"
scrollable
>
<v-card>
<v-toolbar color="primary" dark>
<v-btn icon @click="dialog = false">
<v-toolbar
color="primary"
dark
>
<v-btn
icon
@click="dialog = false"
>
<v-icon>mdi-close</v-icon>
</v-btn>
<v-toolbar-title>{{ t('core.common.editor.editingTitle') }} - {{ currentEditingKey }}</v-toolbar-title>
<v-spacer></v-spacer>
<v-spacer />
<v-toolbar-items>
<v-btn variant="text" @click="saveEditedContent">{{ t('core.common.save') }}</v-btn>
<v-btn
variant="text"
@click="saveEditedContent"
>
{{ t('core.common.save') }}
</v-btn>
</v-toolbar-items>
</v-toolbar>
<v-card-text class="pa-0">
<VueMonacoEditor :theme="currentEditingTheme" :language="currentEditingLanguage"
style="height: calc(100vh - 64px);" v-model:value="currentEditingKeyIterable[currentEditingKey]">
</VueMonacoEditor>
<VueMonacoEditor
v-model:value="currentEditingKeyIterable[currentEditingKey]"
:theme="currentEditingTheme"
:language="currentEditingLanguage"
style="height: calc(100vh - 64px);"
/>
</v-card-text>
</v-card>
</v-dialog>

File diff suppressed because it is too large Load Diff

View File

@@ -139,15 +139,19 @@ getCurrentVersion();
<template>
<v-dialog
:model-value="dialog"
@update:model-value="dialog = $event"
:width="$vuetify.display.smAndDown ? '100%' : '800'"
:fullscreen="$vuetify.display.xs"
max-width="1000"
:fullscreen="$vuetify.display.xs"
max-width="1000"
@update:model-value="dialog = $event"
>
<v-card>
<v-card-title class="d-flex justify-space-between align-center">
<span class="text-h3">{{ t('core.navigation.changelogDialog.title') }}</span>
<v-btn icon @click="dialog = false" flat>
<v-btn
icon
flat
@click="dialog = false"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
@@ -163,16 +167,26 @@ getCurrentVersion();
density="compact"
@update:model-value="onVersionChange"
>
<template v-slot:item="{ item, props }">
<v-list-item v-bind="props" :title="`v${item.value}`">
<template v-slot:append v-if="item.value === changelogVersion">
<v-chip size="x-small" color="primary" variant="tonal">
<template #item="{ item, props }">
<v-list-item
v-bind="props"
:title="`v${item.value}`"
>
<template
v-if="item.value === changelogVersion"
#append
>
<v-chip
size="x-small"
color="primary"
variant="tonal"
>
{{ t('core.navigation.changelogDialog.current') }}
</v-chip>
</template>
</v-list-item>
</template>
<template v-slot:selection="{ item }">
<template #selection="{ item }">
<span>v{{ item.value }}</span>
</template>
</v-select>
@@ -180,21 +194,45 @@ getCurrentVersion();
<!-- 更新日志内容 -->
<div style="max-height: 70vh; overflow-y: auto;">
<div v-if="changelogLoading" class="text-center py-8">
<v-progress-circular indeterminate color="primary"></v-progress-circular>
<div class="mt-4">{{ t('core.navigation.changelogDialog.loading') }}</div>
<div
v-if="changelogLoading"
class="text-center py-8"
>
<v-progress-circular
indeterminate
color="primary"
/>
<div class="mt-4">
{{ t('core.navigation.changelogDialog.loading') }}
</div>
</div>
<v-alert v-else-if="changelogError" type="error" variant="tonal" border="start">
<v-alert
v-else-if="changelogError"
type="error"
variant="tonal"
border="start"
>
{{ changelogError }}
</v-alert>
<div v-else-if="changelogContent" class="changelog-content">
<MarkdownRender :content="changelogContent" :typewriter="false" class="markdown-content" />
<div
v-else-if="changelogContent"
class="changelog-content"
>
<MarkdownRender
:content="changelogContent"
:typewriter="false"
class="markdown-content"
/>
</div>
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue-darken-1" variant="text" @click="dialog = false">
<v-spacer />
<v-btn
color="blue-darken-1"
variant="text"
@click="dialog = false"
>
{{ t('core.common.close') }}
</v-btn>
</v-card-actions>

View File

@@ -2,45 +2,74 @@
<div class="w-100">
<!-- Special handling for specific metadata types -->
<template v-if="itemMeta?._special === 'select_provider'">
<ProviderSelector :model-value="modelValue" @update:model-value="emitUpdate" :provider-type="'chat_completion'" />
<ProviderSelector
:model-value="modelValue"
:provider-type="'chat_completion'"
@update:model-value="emitUpdate"
/>
</template>
<template v-else-if="itemMeta?._special === 'select_provider_stt'">
<ProviderSelector :model-value="modelValue" @update:model-value="emitUpdate" :provider-type="'speech_to_text'" />
<ProviderSelector
:model-value="modelValue"
:provider-type="'speech_to_text'"
@update:model-value="emitUpdate"
/>
</template>
<template v-else-if="itemMeta?._special === 'select_provider_tts'">
<ProviderSelector :model-value="modelValue" @update:model-value="emitUpdate" :provider-type="'text_to_speech'" />
<ProviderSelector
:model-value="modelValue"
:provider-type="'text_to_speech'"
@update:model-value="emitUpdate"
/>
</template>
<template v-else-if="itemMeta?._special === 'select_providers'">
<ProviderSelector
:model-value="modelValue"
@update:model-value="emitUpdate"
:provider-type="'chat_completion'"
:multiple="true"
@update:model-value="emitUpdate"
/>
</template>
<template v-else-if="getSpecialName(itemMeta?._special) === 'select_agent_runner_provider'">
<ProviderSelector
:model-value="modelValue"
@update:model-value="emitUpdate"
:provider-type="'agent_runner'"
:provider-subtype="getSpecialSubtype(itemMeta?._special)"
@update:model-value="emitUpdate"
/>
</template>
<template v-else-if="itemMeta?._special === 'provider_pool'">
<ProviderSelector :model-value="modelValue" @update:model-value="emitUpdate" :provider-type="'chat_completion'"
:button-text="t('core.shared.providerSelector.selectProviderPool')" />
<ProviderSelector
:model-value="modelValue"
:provider-type="'chat_completion'"
:button-text="t('core.shared.providerSelector.selectProviderPool')"
@update:model-value="emitUpdate"
/>
</template>
<template v-else-if="itemMeta?._special === 'select_persona'">
<PersonaSelector :model-value="modelValue" @update:model-value="emitUpdate" />
<PersonaSelector
:model-value="modelValue"
@update:model-value="emitUpdate"
/>
</template>
<template v-else-if="itemMeta?._special === 'persona_pool'">
<PersonaSelector :model-value="modelValue" @update:model-value="emitUpdate" :button-text="t('core.shared.personaSelector.selectPersonaPool')" />
<PersonaSelector
:model-value="modelValue"
:button-text="t('core.shared.personaSelector.selectPersonaPool')"
@update:model-value="emitUpdate"
/>
</template>
<template v-else-if="itemMeta?._special === 'select_knowledgebase'">
<KnowledgeBaseSelector :model-value="modelValue" @update:model-value="emitUpdate" />
<KnowledgeBaseSelector
:model-value="modelValue"
@update:model-value="emitUpdate"
/>
</template>
<template v-else-if="itemMeta?._special === 'select_plugin_set'">
<PluginSetSelector :model-value="modelValue" @update:model-value="emitUpdate" />
<PluginSetSelector
:model-value="modelValue"
@update:model-value="emitUpdate"
/>
</template>
<template v-else-if="itemMeta?._special === 't2i_template'">
<T2ITemplateEditor />
@@ -49,20 +78,20 @@
<div class="d-flex align-center gap-2">
<v-text-field
:model-value="modelValue"
@update:model-value="emitUpdate"
density="compact"
variant="outlined"
class="config-field"
type="number"
hide-details
></v-text-field>
@update:model-value="emitUpdate"
/>
<v-btn
color="primary"
variant="tonal"
size="small"
@click="$emit('get-embedding-dim')"
:loading="loading"
class="ml-2"
@click="$emit('get-embedding-dim')"
>
{{ t('core.common.autoDetect') }}
</v-btn>
@@ -77,19 +106,18 @@
v-for="(option, optionIndex) in itemMeta.options"
:key="optionIndex"
:model-value="modelValue"
@update:model-value="emitUpdate"
:label="getLabel(itemMeta, optionIndex, option)"
:value="option"
class="mr-2"
color="primary"
hide-details
></v-checkbox>
@update:model-value="emitUpdate"
/>
</div>
<v-combobox
v-else-if="itemMeta?.type === 'list' && itemMeta?.options"
:model-value="modelValue"
@update:model-value="emitUpdate"
:items="itemMeta.options"
:disabled="itemMeta?.readonly"
density="compact"
@@ -98,32 +126,42 @@
hide-details
chips
multiple
></v-combobox>
@update:model-value="emitUpdate"
/>
<v-select
v-else-if="itemMeta?.options"
:model-value="modelValue"
@update:model-value="emitUpdate"
:items="getSelectItems(itemMeta)"
:disabled="itemMeta?.readonly"
density="compact"
variant="outlined"
class="config-field"
hide-details
></v-select>
@update:model-value="emitUpdate"
/>
<div v-else-if="itemMeta?.editor_mode" class="editor-container">
<div
v-else-if="itemMeta?.editor_mode"
class="editor-container"
>
<VueMonacoEditor
:theme="itemMeta?.editor_theme || 'vs-light'"
:language="itemMeta?.editor_language || 'json'"
style="min-height: 100px; flex-grow: 1; border: 1px solid rgba(0, 0, 0, 0.1);"
:value="modelValue"
@update:value="emitUpdate"
>
</VueMonacoEditor>
<v-btn v-if="showFullscreenBtn" icon size="small" variant="text" color="primary" class="editor-fullscreen-btn"
/>
<v-btn
v-if="showFullscreenBtn"
icon
size="small"
variant="text"
color="primary"
class="editor-fullscreen-btn"
:title="t('core.common.editor.fullscreen')"
@click="$emit('open-fullscreen')"
:title="t('core.common.editor.fullscreen')">
>
<v-icon>mdi-fullscreen</v-icon>
</v-btn>
</div>
@@ -131,12 +169,12 @@
<v-text-field
v-else-if="itemMeta?.type === 'string'"
:model-value="modelValue"
@update:model-value="emitUpdate"
density="compact"
variant="outlined"
class="config-field"
hide-details
></v-text-field>
@update:model-value="emitUpdate"
/>
<div
v-else-if="itemMeta?.type === 'int' || itemMeta?.type === 'float'"
@@ -145,7 +183,6 @@
<v-slider
v-if="itemMeta?.slider"
:model-value="toNumber(modelValue)"
@update:model-value="val => emitUpdate(toNumber(val))"
:min="itemMeta?.slider?.min ?? 0"
:max="itemMeta?.slider?.max ?? 100"
:step="itemMeta?.slider?.step ?? 1"
@@ -153,38 +190,39 @@
density="compact"
hide-details
style="flex: 1"
></v-slider>
@update:model-value="val => emitUpdate(toNumber(val))"
/>
<v-text-field
:model-value="modelValue"
@update:model-value="val => emitUpdate(toNumber(val))"
density="compact"
variant="outlined"
class="config-field"
type="number"
hide-details
style="flex: 1"
></v-text-field>
@update:model-value="val => emitUpdate(toNumber(val))"
/>
</div>
<v-textarea
v-else-if="itemMeta?.type === 'text'"
:model-value="modelValue"
@update:model-value="emitUpdate"
variant="outlined"
rows="3"
class="config-field"
hide-details
></v-textarea>
@update:model-value="emitUpdate"
/>
<v-switch
v-else-if="itemMeta?.type === 'bool'"
:model-value="modelValue"
@update:model-value="emitUpdate"
color="primary"
inset
density="compact"
hide-details
></v-switch>
@update:model-value="emitUpdate"
/>
<FileConfigItem
v-else-if="itemMeta?.type === 'file'"
@@ -192,34 +230,34 @@
:item-meta="itemMeta"
:plugin-name="pluginName"
:config-key="configKey"
@update:model-value="emitUpdate"
class="config-field"
@update:model-value="emitUpdate"
/>
<ListConfigItem
v-else-if="itemMeta?.type === 'list'"
:model-value="modelValue"
@update:model-value="emitUpdate"
class="config-field"
@update:model-value="emitUpdate"
/>
<ObjectEditor
v-else-if="itemMeta?.type === 'dict'"
:model-value="modelValue"
:item-meta="itemMeta"
@update:model-value="emitUpdate"
class="config-field"
@update:model-value="emitUpdate"
/>
<v-text-field
v-else
:model-value="modelValue"
@update:model-value="emitUpdate"
density="compact"
variant="outlined"
class="config-field"
hide-details
></v-text-field>
@update:model-value="emitUpdate"
/>
</div>
</template>

View File

@@ -6,23 +6,50 @@ import { EventSourcePolyfill } from 'event-source-polyfill';
<template>
<div>
<div class="filter-controls mb-2" v-if="showLevelBtns">
<v-chip-group v-model="selectedLevels" column multiple>
<v-chip v-for="level in logLevels" :key="level" :color="getLevelColor(level)" filter variant="flat" size="small"
:text-color="level === 'DEBUG' || level === 'INFO' ? 'black' : 'white'" class="font-weight-medium">
<div
v-if="showLevelBtns"
class="filter-controls mb-2"
>
<v-chip-group
v-model="selectedLevels"
column
multiple
>
<v-chip
v-for="level in logLevels"
:key="level"
:color="getLevelColor(level)"
filter
variant="flat"
size="small"
:text-color="level === 'DEBUG' || level === 'INFO' ? 'black' : 'white'"
class="font-weight-medium"
>
{{ level }}
</v-chip>
</v-chip-group>
</div>
<div id="term" style="background-color: #1e1e1e; padding: 16px; border-radius: 8px; overflow-y:auto; height: 100%">
</div>
<div
id="term"
style="background-color: #1e1e1e; padding: 16px; border-radius: 8px; overflow-y:auto; height: 100%"
/>
</div>
</template>
<script>
export default {
name: 'ConsoleDisplayer',
props: {
historyNum: {
type: String,
default: "-1"
},
showLevelBtns: {
type: Boolean,
default: true
}
},
data() {
return {
autoScroll: true,
@@ -59,16 +86,6 @@ export default {
return useCommonStore();
},
},
props: {
historyNum: {
type: String,
default: "-1"
},
showLevelBtns: {
type: Boolean,
default: true
}
},
watch: {
selectedLevels: {
handler() {

View File

@@ -165,13 +165,16 @@ const viewChangelog = () => {
location="top"
:text="
extension.display_name?.length &&
extension.display_name !== extension.name
extension.display_name !== extension.name
? `${extension.display_name} (${extension.name})`
: extension.name
"
>
<template v-slot:activator="{ props: titleTooltipProps }">
<span v-bind="titleTooltipProps" class="extension-title__text">{{
<template #activator="{ props: titleTooltipProps }">
<span
v-bind="titleTooltipProps"
class="extension-title__text"
>{{
extension.display_name?.length
? extension.display_name
: extension.name
@@ -179,10 +182,10 @@ const viewChangelog = () => {
</template>
</v-tooltip>
<v-tooltip
location="top"
v-if="extension?.has_update && !marketMode"
location="top"
>
<template v-slot:activator="{ props: tooltipProps }">
<template #activator="{ props: tooltipProps }">
<v-icon
v-bind="tooltipProps"
color="warning"
@@ -191,19 +194,21 @@ const viewChangelog = () => {
size="small"
style="cursor: pointer"
@click.stop="updateExtension"
></v-icon>
/>
</template>
<span
>{{ tm("card.status.hasUpdate") }}:
{{ extension.online_version }}</span
>
<span>{{ tm("card.status.hasUpdate") }}:
{{ extension.online_version }}</span>
</v-tooltip>
</p>
<template v-if="!marketMode">
<v-tooltip location="left">
<template v-slot:activator="{ props: tooltipProps }">
<div v-bind="tooltipProps" class="extension-switch-wrap" @click.stop>
<template #activator="{ props: tooltipProps }">
<div
v-bind="tooltipProps"
class="extension-switch-wrap"
@click.stop
>
<v-switch
:model-value="extension.activated"
color="success"
@@ -211,7 +216,7 @@ const viewChangelog = () => {
hide-details
inset
@update:model-value="toggleActivation"
></v-switch>
/>
</div>
</template>
<span>{{
@@ -222,27 +227,32 @@ const viewChangelog = () => {
<template v-else>
<div class="extension-market-menu-wrap">
<v-menu offset-y>
<template v-slot:activator="{ props: menuProps }">
<template #activator="{ props: menuProps }">
<v-btn
v-if="extension?.repo"
icon
variant="text"
aria-label="more"
v-if="extension?.repo"
:href="extension?.repo"
target="_blank"
>
<v-icon icon="mdi-github"></v-icon>
<v-icon icon="mdi-github" />
</v-btn>
<v-btn v-bind="menuProps" icon variant="text" aria-label="more">
<v-icon icon="mdi-dots-vertical"></v-icon>
<v-btn
v-bind="menuProps"
icon
variant="text"
aria-label="more"
>
<v-icon icon="mdi-dots-vertical" />
</v-btn>
</template>
<v-list>
<v-list-item @click="viewReadme">
<v-list-item-title
>📄 {{ tm("buttons.viewDocs") }}</v-list-item-title
>
<v-list-item-title>
📄 {{ tm("buttons.viewDocs") }}
</v-list-item-title>
</v-list-item>
<v-list-item
@@ -250,14 +260,16 @@ const viewChangelog = () => {
@click="installExtension"
>
<v-list-item-title>
{{ tm("buttons.install") }}</v-list-item-title
>
{{ tm("buttons.install") }}
</v-list-item-title>
</v-list-item>
<v-list-item v-if="marketMode && extension?.installed">
<v-list-item-title class="text--disabled">{{
tm("status.installed")
}}</v-list-item-title>
<v-list-item-title class="text--disabled">
{{
tm("status.installed")
}}
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
@@ -272,13 +284,20 @@ const viewChangelog = () => {
:alt="extension.name"
class="extension-logo"
@error="logoLoadFailed = true"
/>
>
</div>
<div class="extension-meta-group">
<div class="extension-chip-group d-flex flex-wrap">
<v-chip color="primary" label size="small">
<v-icon icon="mdi-source-branch" start></v-icon>
<v-chip
color="primary"
label
size="small"
>
<v-icon
icon="mdi-source-branch"
start
/>
{{ extension.version }}
</v-chip>
<v-chip
@@ -289,7 +308,10 @@ const viewChangelog = () => {
style="cursor: pointer"
@click="updateExtension"
>
<v-icon icon="mdi-arrow-up-bold" start></v-icon>
<v-icon
icon="mdi-arrow-up-bold"
start
/>
{{ extension.online_version }}
</v-chip>
<v-chip
@@ -297,10 +319,13 @@ const viewChangelog = () => {
color="primary"
label
size="small"
@click="viewHandlers"
style="cursor: pointer"
@click="viewHandlers"
>
<v-icon icon="mdi-cogs" start></v-icon>
<v-icon
icon="mdi-cogs"
start
/>
{{ extension.handlers?.length
}}{{ tm("card.status.handlersCount") }}
</v-chip>
@@ -337,11 +362,17 @@ const viewChangelog = () => {
</div>
</v-card-text>
<v-card-actions class="extension-actions" @click.stop>
<v-card-actions
class="extension-actions"
@click.stop
>
<template v-if="!marketMode">
<v-spacer></v-spacer>
<v-tooltip location="top" :text="tm('buttons.viewDocs')">
<template v-slot:activator="{ props: actionProps }">
<v-spacer />
<v-tooltip
location="top"
:text="tm('buttons.viewDocs')"
>
<template #activator="{ props: actionProps }">
<v-btn
v-bind="actionProps"
icon="mdi-book-open-page-variant"
@@ -349,12 +380,15 @@ const viewChangelog = () => {
variant="tonal"
color="info"
@click="viewReadme"
></v-btn>
/>
</template>
</v-tooltip>
<v-tooltip location="top" :text="tm('card.actions.pluginConfig')">
<template v-slot:activator="{ props: actionProps }">
<v-tooltip
location="top"
:text="tm('card.actions.pluginConfig')"
>
<template #activator="{ props: actionProps }">
<v-btn
v-bind="actionProps"
icon="mdi-cog"
@@ -362,12 +396,16 @@ const viewChangelog = () => {
variant="tonal"
color="primary"
@click="configure"
></v-btn>
/>
</template>
</v-tooltip>
<v-tooltip v-if="extension?.repo" location="top" :text="tm('buttons.viewRepo')">
<template v-slot:activator="{ props: actionProps }">
<v-tooltip
v-if="extension?.repo"
location="top"
:text="tm('buttons.viewRepo')"
>
<template #activator="{ props: actionProps }">
<v-btn
v-bind="actionProps"
icon="mdi-github"
@@ -376,12 +414,15 @@ const viewChangelog = () => {
color="secondary"
:href="extension.repo"
target="_blank"
></v-btn>
/>
</template>
</v-tooltip>
<v-tooltip location="top" :text="tm('card.actions.reloadPlugin')">
<template v-slot:activator="{ props: actionProps }">
<v-tooltip
location="top"
:text="tm('card.actions.reloadPlugin')"
>
<template #activator="{ props: actionProps }">
<v-btn
v-bind="actionProps"
icon="mdi-refresh"
@@ -389,11 +430,14 @@ const viewChangelog = () => {
variant="tonal"
color="primary"
@click="reloadExtension"
></v-btn>
/>
</template>
</v-tooltip>
<StyledMenu location="top end" offset="8">
<StyledMenu
location="top end"
offset="8"
>
<template #activator="{ props: menuProps }">
<v-btn
v-bind="menuProps"
@@ -401,28 +445,48 @@ const viewChangelog = () => {
size="small"
variant="tonal"
color="secondary"
></v-btn>
/>
</template>
<v-list-item class="styled-menu-item" prepend-icon="mdi-information" @click="viewHandlers">
<v-list-item
class="styled-menu-item"
prepend-icon="mdi-information"
@click="viewHandlers"
>
<v-list-item-title>{{ tm("buttons.viewInfo") }}</v-list-item-title>
</v-list-item>
<v-list-item class="styled-menu-item" prepend-icon="mdi-update" @click="updateExtension">
<v-list-item-title>{{
extension.has_update
? tm("card.actions.updateTo") + " " + extension.online_version
: tm("card.actions.reinstall")
}}</v-list-item-title>
<v-list-item
class="styled-menu-item"
prepend-icon="mdi-update"
@click="updateExtension"
>
<v-list-item-title>
{{
extension.has_update
? tm("card.actions.updateTo") + " " + extension.online_version
: tm("card.actions.reinstall")
}}
</v-list-item-title>
</v-list-item>
<v-list-item class="styled-menu-item" prepend-icon="mdi-delete" @click="uninstallExtension">
<v-list-item-title class="text-error">{{ tm("card.actions.uninstallPlugin") }}</v-list-item-title>
<v-list-item
class="styled-menu-item"
prepend-icon="mdi-delete"
@click="uninstallExtension"
>
<v-list-item-title class="text-error">
{{ tm("card.actions.uninstallPlugin") }}
</v-list-item-title>
</v-list-item>
</StyledMenu>
</template>
<template v-else>
<v-btn color="primary" size="small" @click="viewReadme">
<v-btn
color="primary"
size="small"
@click="viewReadme"
>
{{ tm("buttons.viewDocs") }}
</v-btn>
</template>

View File

@@ -1,7 +1,12 @@
<template>
<div class="file-config-item">
<div class="d-flex align-center gap-2">
<v-btn size="small" color="primary" variant="tonal" @click="dialog = true">
<v-btn
size="small"
color="primary"
variant="tonal"
@click="dialog = true"
>
{{ tm('fileUpload.button') }}
</v-btn>
<span class="text-caption text-medium-emphasis ml-2">
@@ -9,61 +14,123 @@
</span>
</div>
<v-dialog v-model="dialog" max-width="700">
<v-card class="file-dialog-card" variant="flat">
<v-dialog
v-model="dialog"
max-width="700"
>
<v-card
class="file-dialog-card"
variant="flat"
>
<v-card-title class="d-flex align-center">
<span class="text-h3">{{ tm('fileUpload.dialogTitle') }}</span>
<v-spacer />
<v-btn icon="mdi-close" variant="text" @click="dialog = false" />
<v-btn
icon="mdi-close"
variant="text"
@click="dialog = false"
/>
</v-card-title>
<v-card-text class="file-dialog-body">
<div v-if="mergedFileItems.length === 0" class="empty-text">
<div
v-if="mergedFileItems.length === 0"
class="empty-text"
>
{{ tm('fileUpload.empty') }}
</div>
<v-list density="compact" lines="one">
<v-list-item v-for="item in mergedFileItems" :key="item.path">
<v-list
density="compact"
lines="one"
>
<v-list-item
v-for="item in mergedFileItems"
:key="item.path"
>
<template #prepend>
<v-icon size="18">mdi-file</v-icon>
<v-icon size="18">
mdi-file
</v-icon>
</template>
<v-list-item-title class="file-name">
{{ getDisplayName(item.path) }}
</v-list-item-title>
<template #append>
<div class="d-flex align-center gap-1">
<v-chip v-if="item.status !== 'ok'" size="x-small" :color="getStatusColor(item.status)"
variant="tonal">
<v-chip
v-if="item.status !== 'ok'"
size="x-small"
:color="getStatusColor(item.status)"
variant="tonal"
>
{{ getStatusText(item.status) }}
</v-chip>
<v-btn v-if="item.status === 'unconfigured'" icon="mdi-plus" size="x-small" variant="text"
@click="addToConfig(item.path)" />
<v-btn icon="mdi-delete" size="x-small" variant="text"
@click="item.status === 'unconfigured' ? deletePhysicalFile(item.path) : deleteFile(item.path)" />
<v-btn
v-if="item.status === 'unconfigured'"
icon="mdi-plus"
size="x-small"
variant="text"
@click="addToConfig(item.path)"
/>
<v-btn
icon="mdi-delete"
size="x-small"
variant="text"
@click="item.status === 'unconfigured' ? deletePhysicalFile(item.path) : deleteFile(item.path)"
/>
</div>
</template>
</v-list-item>
<v-divider v-if="mergedFileItems.length > 0" class="my-2" />
<v-divider
v-if="mergedFileItems.length > 0"
class="my-2"
/>
<v-list-item class="upload-item" :class="{ dragover: isDragging }" @drop.prevent="handleDrop"
@dragover.prevent="isDragging = true" @dragleave="isDragging = false" @click="openFilePicker">
<v-list-item
class="upload-item"
:class="{ dragover: isDragging }"
@drop.prevent="handleDrop"
@dragover.prevent="isDragging = true"
@dragleave="isDragging = false"
@click="openFilePicker"
>
<template #prepend>
<v-icon size="18" color="primary">mdi-plus</v-icon>
<v-icon
size="18"
color="primary"
>
mdi-plus
</v-icon>
</template>
<v-list-item-title>{{ tm('fileUpload.dropzone') }}</v-list-item-title>
<v-list-item-subtitle v-if="allowedTypesText" class="upload-hint">
<v-list-item-subtitle
v-if="allowedTypesText"
class="upload-hint"
>
{{ tm('fileUpload.allowedTypes', { types: allowedTypesText }) }}
</v-list-item-subtitle>
</v-list-item>
</v-list>
<input ref="fileInput" type="file" multiple hidden :accept="acceptAttr" @change="handleFileSelect" />
<input
ref="fileInput"
type="file"
multiple
hidden
:accept="acceptAttr"
@change="handleFileSelect"
>
</v-card-text>
<v-card-actions class="file-dialog-actions">
<v-spacer />
<v-btn color="primary" variant="elevated" @click="dialog = false">
<v-btn
color="primary"
variant="elevated"
@click="dialog = false"
>
{{ tm('fileUpload.done') }}
</v-btn>
</v-card-actions>

View File

@@ -1,9 +1,16 @@
<template>
<v-card class="item-card hover-elevation" style="padding: 4px;" elevation="0">
<v-card
class="item-card hover-elevation"
style="padding: 4px;"
elevation="0"
>
<v-card-title class="d-flex justify-space-between align-center pb-1 pt-3">
<span class="text-h2 text-truncate" :title="getItemTitle()">{{ getItemTitle() }}</span>
<span
class="text-h2 text-truncate"
:title="getItemTitle()"
>{{ getItemTitle() }}</span>
<v-tooltip location="top">
<template v-slot:activator="{ props }">
<template #activator="{ props }">
<v-switch
color="primary"
hide-details
@@ -13,38 +20,41 @@
:disabled="loading || disableToggle"
v-bind="props"
@update:model-value="toggleEnabled"
></v-switch>
/>
</template>
<span>{{ getItemEnabled() ? t('core.common.itemCard.enabled') : t('core.common.itemCard.disabled') }}</span>
</v-tooltip>
</v-card-title>
<v-card-text>
<slot name="item-details" :item="item"></slot>
<slot
name="item-details"
:item="item"
/>
</v-card-text>
<v-card-actions style="margin: 8px;">
<v-btn
variant="outlined"
color="error"
size="small"
rounded="xl"
:disabled="loading || disableDelete"
@click="$emit('delete', item)"
>
{{ t('core.common.itemCard.delete') }}
</v-btn>
<v-btn
v-if="showEditButton"
variant="tonal"
color="primary"
size="small"
rounded="xl"
:disabled="loading"
@click="$emit('edit', item)"
>
{{ t('core.common.itemCard.edit') }}
</v-btn>
<v-card-actions style="margin: 8px;">
<v-btn
variant="outlined"
color="error"
size="small"
rounded="xl"
:disabled="loading || disableDelete"
@click="$emit('delete', item)"
>
{{ t('core.common.itemCard.delete') }}
</v-btn>
<v-btn
v-if="showEditButton"
variant="tonal"
color="primary"
size="small"
rounded="xl"
:disabled="loading"
@click="$emit('edit', item)"
>
{{ t('core.common.itemCard.edit') }}
</v-btn>
<v-btn
v-if="showCopyButton"
variant="tonal"
@@ -56,17 +66,24 @@
>
{{ t('core.common.itemCard.copy') }}
</v-btn>
<slot name="actions" :item="item"></slot>
<v-spacer></v-spacer>
<slot
name="actions"
:item="item"
/>
<v-spacer />
</v-card-actions>
<div class="d-flex justify-end align-center" style="position: absolute; bottom: 16px; right: 16px; opacity: 0.2;" v-if="bglogo">
<div
v-if="bglogo"
class="d-flex justify-end align-center"
style="position: absolute; bottom: 16px; right: 16px; opacity: 0.2;"
>
<v-img
:src="bglogo"
contain
width="120"
height="120"
></v-img>
/>
</div>
</v-card>
</template>
@@ -76,10 +93,6 @@ import { useI18n } from '@/i18n/composables';
export default {
name: 'ItemCard',
setup() {
const { t } = useI18n();
return { t };
},
props: {
item: {
type: Object,
@@ -119,6 +132,10 @@ export default {
}
},
emits: ['toggle-enabled', 'delete', 'edit', 'copy'],
setup() {
const { t } = useI18n();
return { t };
},
methods: {
getItemTitle() {
return this.item[this.titleField];

View File

@@ -1,20 +1,47 @@
<template>
<div>
<v-row v-if="items.length === 0">
<v-col cols="12" class="text-center pa-8">
<v-icon size="64" color="grey-lighten-1">{{ emptyIcon }}</v-icon>
<p class="text-grey mt-4">{{ displayEmptyText }}</p>
<v-col
cols="12"
class="text-center pa-8"
>
<v-icon
size="64"
color="grey-lighten-1"
>
{{ emptyIcon }}
</v-icon>
<p class="text-grey mt-4">
{{ displayEmptyText }}
</p>
</v-col>
</v-row>
<v-row v-else>
<v-col v-for="(item, index) in items" :key="index" cols="12" md="6" lg="4" xl="3">
<v-card class="item-card hover-elevation" style="padding: 4px;" elevation="0">
<div class="item-status-indicator" :class="{'active': getItemEnabled(item)}"></div>
<v-col
v-for="(item, index) in items"
:key="index"
cols="12"
md="6"
lg="4"
xl="3"
>
<v-card
class="item-card hover-elevation"
style="padding: 4px;"
elevation="0"
>
<div
class="item-status-indicator"
:class="{'active': getItemEnabled(item)}"
/>
<v-card-title class="d-flex justify-space-between align-center pb-1 pt-3">
<span class="text-h2 text-truncate" :title="getItemTitle(item)">{{ getItemTitle(item) }}</span>
<span
class="text-h2 text-truncate"
:title="getItemTitle(item)"
>{{ getItemTitle(item) }}</span>
<v-tooltip location="top">
<template v-slot:activator="{ props }">
<template #activator="{ props }">
<v-switch
color="primary"
hide-details
@@ -22,14 +49,17 @@
:model-value="getItemEnabled(item)"
v-bind="props"
@update:model-value="toggleEnabled(item)"
></v-switch>
/>
</template>
<span>{{ getItemEnabled(item) ? t('core.common.itemCard.enabled') : t('core.common.itemCard.disabled') }}</span>
</v-tooltip>
</v-card-title>
<v-card-text>
<slot name="item-details" :item="item"></slot>
<slot
name="item-details"
:item="item"
/>
</v-card-text>
<v-card-actions style="margin: 8px;">
@@ -49,19 +79,22 @@
>
{{ t('core.common.itemCard.edit') }}
</v-btn>
<v-spacer></v-spacer>
<v-spacer />
</v-card-actions>
<div class="d-flex justify-end align-center" style="position: absolute; bottom: 16px; right: 16px; opacity: 0.2;" v-if="bglogo">
<div
v-if="bglogo"
class="d-flex justify-end align-center"
style="position: absolute; bottom: 16px; right: 16px; opacity: 0.2;"
>
<v-img
:src="bglogo"
contain
width="120"
height="120"
class="rounded-circle"
></v-img>
/>
</div>
</v-card>
</v-col>
</v-row>
@@ -73,10 +106,6 @@ import { useI18n } from '@/i18n/composables';
export default {
name: 'ItemCardGrid',
setup() {
const { t } = useI18n();
return { t };
},
props: {
items: {
type: Array,
@@ -104,6 +133,10 @@ export default {
}
},
emits: ['toggle-enabled', 'delete', 'edit'],
setup() {
const { t } = useI18n();
return { t };
},
computed: {
displayEmptyText() {
return this.emptyText || this.t('core.common.itemCard.noData');

View File

@@ -1,11 +1,19 @@
<template>
<div class="d-flex align-center justify-space-between" style="gap: 8px;">
<div
class="d-flex align-center justify-space-between"
style="gap: 8px;"
>
<div style="flex: 1; min-width: 0; overflow: hidden;">
<span v-if="!modelValue || (Array.isArray(modelValue) && modelValue.length === 0)"
style="color: rgb(var(--v-theme-primaryText));">
<span
v-if="!modelValue || (Array.isArray(modelValue) && modelValue.length === 0)"
style="color: rgb(var(--v-theme-primaryText));"
>
{{ tm('knowledgeBaseSelector.notSelected') }}
</span>
<div v-else class="d-flex flex-wrap gap-1">
<div
v-else
class="d-flex flex-wrap gap-1"
>
<v-chip
v-for="name in modelValue"
:key="name"
@@ -13,39 +21,66 @@
color="primary"
variant="tonal"
closable
style="max-width: 100%;"
@click:close="removeKnowledgeBase(name)"
style="max-width: 100%;">
<span class="text-truncate" style="max-width: 200px;">{{ name }}</span>
>
<span
class="text-truncate"
style="max-width: 200px;"
>{{ name }}</span>
</v-chip>
</div>
</div>
<v-btn size="small" color="primary" variant="tonal" @click="openDialog" style="flex-shrink: 0;">
<v-btn
size="small"
color="primary"
variant="tonal"
style="flex-shrink: 0;"
@click="openDialog"
>
{{ buttonText || tm('knowledgeBaseSelector.buttonText') }}
</v-btn>
</div>
<!-- Knowledge Base Selection Dialog -->
<v-dialog v-model="dialog" max-width="600px">
<v-dialog
v-model="dialog"
max-width="600px"
>
<v-card>
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
<v-card-title
class="text-h3 py-4"
style="font-weight: normal;"
>
{{ tm('knowledgeBaseSelector.dialogTitle') }}
</v-card-title>
<v-card-text class="pa-0" style="max-height: 400px; overflow-y: auto;">
<v-progress-linear v-if="loading" indeterminate color="primary"></v-progress-linear>
<v-card-text
class="pa-0"
style="max-height: 400px; overflow-y: auto;"
>
<v-progress-linear
v-if="loading"
indeterminate
color="primary"
/>
<!-- 知识库列表 -->
<v-list v-if="!loading" density="compact">
<v-list
v-if="!loading"
density="compact"
>
<!-- 知识库选项 -->
<v-list-item
v-for="kb in knowledgeBaseList"
:key="kb.kb_id"
:value="kb.kb_name"
@click="selectKnowledgeBase(kb.kb_name)"
:active="isSelected(kb.kb_name)"
rounded="md"
class="ma-1">
<template v-slot:prepend>
class="ma-1"
@click="selectKnowledgeBase(kb.kb_name)"
>
<template #prepend>
<span class="emoji-icon">{{ kb.emoji || '📚' }}</span>
</template>
<v-list-item-title>{{ kb.kb_name }}</v-list-item-title>
@@ -55,21 +90,41 @@
<span v-if="kb.chunk_count !== undefined"> - {{ tm('knowledgeBaseSelector.chunkCount', { count: kb.chunk_count }) }}</span>
</v-list-item-subtitle>
<template v-slot:append>
<v-icon v-if="isSelected(kb.kb_name)" color="primary">
<template #append>
<v-icon
v-if="isSelected(kb.kb_name)"
color="primary"
>
mdi-checkbox-marked
</v-icon>
<v-icon v-else color="grey-lighten-1">
<v-icon
v-else
color="grey-lighten-1"
>
mdi-checkbox-blank-outline
</v-icon>
</template>
</v-list-item>
<!-- 当没有知识库时显示创建提示 -->
<div v-if="knowledgeBaseList.length === 0" class="text-center py-8">
<v-icon size="64" color="grey-lighten-1">mdi-database-off</v-icon>
<p class="text-grey mt-4 mb-4">{{ tm('knowledgeBaseSelector.noKnowledgeBases') }}</p>
<v-btn color="primary" variant="tonal" @click="goToKnowledgeBasePage">
<div
v-if="knowledgeBaseList.length === 0"
class="text-center py-8"
>
<v-icon
size="64"
color="grey-lighten-1"
>
mdi-database-off
</v-icon>
<p class="text-grey mt-4 mb-4">
{{ tm('knowledgeBaseSelector.noKnowledgeBases') }}
</p>
<v-btn
color="primary"
variant="tonal"
@click="goToKnowledgeBasePage"
>
{{ tm('knowledgeBaseSelector.createKnowledgeBase') }}
</v-btn>
</div>
@@ -77,14 +132,23 @@
</v-card-text>
<v-card-actions class="pa-4">
<div v-if="selectedKnowledgeBases.length > 0" class="text-caption text-grey">
<div
v-if="selectedKnowledgeBases.length > 0"
class="text-caption text-grey"
>
{{ tm('knowledgeBaseSelector.selectedCount', { count: selectedKnowledgeBases.length }) }}
</div>
<v-spacer></v-spacer>
<v-btn variant="text" @click="cancelSelection">{{ tm('knowledgeBaseSelector.cancelSelection') }}</v-btn>
<v-spacer />
<v-btn
variant="text"
@click="cancelSelection"
>
{{ tm('knowledgeBaseSelector.cancelSelection') }}
</v-btn>
<v-btn
color="primary"
@click="confirmSelection">
@click="confirmSelection"
>
{{ tm('knowledgeBaseSelector.confirmSelection') }}
</v-btn>
</v-card-actions>

View File

@@ -1,6 +1,9 @@
<template>
<StyledMenu offset="12" location="bottom center">
<template v-slot:activator="{ props: activatorProps }">
<StyledMenu
offset="12"
location="bottom center"
>
<template #activator="{ props: activatorProps }">
<v-btn
v-bind="activatorProps"
:variant="(props.variant === 'header' || props.variant === 'chatbox') ? 'flat' : 'text'"
@@ -16,7 +19,10 @@
>
mdi-translate
</v-icon>
<v-tooltip activator="parent" location="top">
<v-tooltip
activator="parent"
location="top"
>
{{ t('core.common.language') }}
</v-tooltip>
</v-btn>
@@ -26,12 +32,12 @@
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"
@click="changeLanguage(lang.code)"
>
<template v-slot:prepend>
<template #prepend>
<span class="language-flag">{{ lang.flag }}</span>
</template>
<v-list-item-title>{{ lang.name }}</v-list-item-title>

View File

@@ -1,36 +1,67 @@
<template>
<div class="d-flex align-center justify-space-between ga-2">
<div v-if="isSingleItemMode" class="flex-grow-1 d-flex align-center ga-2">
<div
v-if="isSingleItemMode"
class="flex-grow-1 d-flex align-center ga-2"
>
<v-text-field
v-model="singleItemValue"
hide-details
variant="outlined"
density="compact"
class="flex-grow-1"
></v-text-field>
/>
</div>
<div v-else>
<span v-if="!modelValue || modelValue.length === 0" style="color: rgb(var(--v-theme-primaryText));">
<span
v-if="!modelValue || modelValue.length === 0"
style="color: rgb(var(--v-theme-primaryText));"
>
{{ t('core.common.list.noItems') }}
</span>
<div v-else class="d-flex flex-wrap ga-2">
<v-chip v-for="item in displayItems" :key="item" size="x-small" label color="primary">
<div
v-else
class="d-flex flex-wrap ga-2"
>
<v-chip
v-for="item in displayItems"
:key="item"
size="x-small"
label
color="primary"
>
{{ item.length > 20 ? item.slice(0, 20) + '...' : item }}
</v-chip>
<v-chip v-if="modelValue.length > maxDisplayItems" size="x-small" label color="grey-lighten-1">
<v-chip
v-if="modelValue.length > maxDisplayItems"
size="x-small"
label
color="grey-lighten-1"
>
+{{ modelValue.length - maxDisplayItems }}
</v-chip>
</div>
</div>
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
<v-btn
size="small"
color="primary"
variant="tonal"
@click="openDialog"
>
{{ preferSingleItem ? t('core.common.list.addMore') : (buttonText || t('core.common.list.modifyButton')) }}
</v-btn>
</div>
<!-- List Management Dialog -->
<v-dialog v-model="dialog" max-width="600px">
<v-dialog
v-model="dialog"
max-width="600px"
>
<v-card>
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
<v-card-title
class="text-h3 py-4"
style="font-weight: normal;"
>
{{ dialogTitle || t('core.common.list.editTitle') }}
</v-card-title>
@@ -40,42 +71,56 @@
<v-text-field
v-model="newItem"
:label="t('core.common.list.addItemPlaceholder')"
@keyup.enter="addItem"
clearable
hide-details
variant="outlined"
hide-details
variant="outlined"
density="compact"
:placeholder="t('core.common.list.inputPlaceholder')"
class="flex-grow-1">
</v-text-field>
:placeholder="t('core.common.list.inputPlaceholder')"
class="flex-grow-1"
@keyup.enter="addItem"
/>
<v-btn
@click="addItem"
variant="tonal"
color="primary"
size="small"
:disabled="!newItem.trim()">
:disabled="!newItem.trim()"
@click="addItem"
>
{{ t('core.common.list.addButton') }}
</v-btn>
<v-btn
@click="showBatchImport = true"
variant="tonal"
color="primary"
size="small">
<v-icon size="small">mdi-import</v-icon>
color="primary"
size="small"
@click="showBatchImport = true"
>
<v-icon size="small">
mdi-import
</v-icon>
{{ t('core.common.list.batchImport') }}
</v-btn>
</div>
</v-card-text>
<v-card-text class="pa-0" style="max-height: 400px; overflow-y: auto;">
<v-list v-if="localItems.length > 0" density="compact">
<v-card-text
class="pa-0"
style="max-height: 400px; overflow-y: auto;"
>
<v-list
v-if="localItems.length > 0"
density="compact"
>
<v-list-item
v-for="(item, index) in localItems"
:key="index"
rounded="md"
class="ma-1 list-item-clickable"
@click="startEdit(index, item)">
<v-list-item-title v-if="editIndex !== index" class="item-text">
@click="startEdit(index, item)"
>
<v-list-item-title
v-if="editIndex !== index"
class="item-text"
>
{{ item }}
</v-list-item-title>
<v-text-field
@@ -84,29 +129,31 @@
hide-details
variant="outlined"
density="compact"
@keyup.enter="saveEdit"
autofocus
@keyup.enter="saveEdit"
@keyup.esc="cancelEdit"
@click.stop
autofocus
></v-text-field>
/>
<template v-slot:append>
<template #append>
<div class="d-flex">
<v-btn
v-if="editIndex === index"
@click.stop="saveEdit"
variant="plain"
color="success"
icon
size="small">
size="small"
@click.stop="saveEdit"
>
<v-icon>mdi-check</v-icon>
</v-btn>
<v-btn
@click.stop="editIndex === index ? cancelEdit() : removeItem(index)"
variant="plain"
:color="editIndex === index ? 'error' : 'default'"
icon
size="small">
:color="editIndex === index ? 'error' : 'default'"
icon
size="small"
@click.stop="editIndex === index ? cancelEdit() : removeItem(index)"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
@@ -114,24 +161,50 @@
</v-list-item>
</v-list>
<div v-else class="text-center py-8">
<v-icon size="64" color="grey-lighten-1">mdi-format-list-bulleted</v-icon>
<p class="text-grey mt-4">{{ t('core.common.list.noItemsHint') }}</p>
<div
v-else
class="text-center py-8"
>
<v-icon
size="64"
color="grey-lighten-1"
>
mdi-format-list-bulleted
</v-icon>
<p class="text-grey mt-4">
{{ t('core.common.list.noItemsHint') }}
</p>
</div>
</v-card-text>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="cancelDialog">{{ t('core.common.cancel') }}</v-btn>
<v-btn color="primary" @click="confirmDialog">{{ t('core.common.confirm') }}</v-btn>
<v-spacer />
<v-btn
variant="text"
@click="cancelDialog"
>
{{ t('core.common.cancel') }}
</v-btn>
<v-btn
color="primary"
@click="confirmDialog"
>
{{ t('core.common.confirm') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Batch Import Dialog -->
<v-dialog v-model="showBatchImport" max-width="600px">
<v-dialog
v-model="showBatchImport"
max-width="600px"
>
<v-card>
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
<v-card-title
class="text-h3 py-4"
style="font-weight: normal;"
>
{{ t('core.common.list.batchImportTitle') }}
</v-card-title>
@@ -144,13 +217,21 @@
variant="outlined"
:hint="t('core.common.list.batchImportHint')"
persistent-hint
></v-textarea>
/>
</v-card-text>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="cancelBatchImport">{{ t('core.common.cancel') }}</v-btn>
<v-btn color="primary" @click="confirmBatchImport">
<v-spacer />
<v-btn
variant="text"
@click="cancelBatchImport"
>
{{ t('core.common.cancel') }}
</v-btn>
<v-btn
color="primary"
@click="confirmBatchImport"
>
{{ t('core.common.list.batchImportButton', { count: batchImportPreviewCount }) }}
</v-btn>
</v-card-actions>

View File

@@ -2,16 +2,24 @@
<div class="logo-container">
<div class="logo-content">
<div class="logo-image">
<img width="110" src="@/assets/images/astrbot_logo_mini.webp" alt="AstrBot Logo">
<img
width="110"
src="@/assets/images/astrbot_logo_mini.webp"
alt="AstrBot Logo"
>
</div>
<div class="logo-text">
<h2
:style="{ color: 'rgb(var(--v-theme-primary))' }"
v-html="formatTitle(title || t('core.header.logoTitle'))"
></h2>
/>
<!-- 父子组件传递css变量可能会出错暂时使用十六进制颜色值 -->
<h4 :style="{ color: 'rgba(var(--v-theme-on-surface), 0.72)' }"
class="hint-text">{{ subtitle || t('core.header.accountDialog.title') }}</h4>
<h4
:style="{ color: 'rgba(var(--v-theme-on-surface), 0.72)' }"
class="hint-text"
>
{{ subtitle || t('core.header.accountDialog.title') }}
</h4>
</div>
</div>
</div>

View File

@@ -1,112 +1,208 @@
<template>
<v-dialog v-model="isOpen" persistent max-width="600" max-height="80vh" scrollable>
<v-card>
<v-card-title>
{{ t('features.migration.dialog.title') }}
</v-card-title>
<v-dialog
v-model="isOpen"
persistent
max-width="600"
max-height="80vh"
scrollable
>
<v-card>
<v-card-title>
{{ t('features.migration.dialog.title') }}
</v-card-title>
<v-card-text class="pa-6">
<p class="mb-4">{{ t('features.migration.dialog.warning') }}</p>
<v-card-text class="pa-6">
<p class="mb-4">
{{ t('features.migration.dialog.warning') }}
</p>
<div v-if="migrationCompleted" class="text-center py-8">
<v-icon size="64" color="success" class="mb-4">mdi-check-circle</v-icon>
<h3 class="mb-4">{{ t('features.migration.dialog.completed') }}</h3>
<p class="mb-4">{{ migrationResult?.message || t('features.migration.dialog.success') }}</p>
<v-alert type="info" variant="tonal" class="mb-4">
<template v-slot:prepend>
<v-icon>mdi-information</v-icon>
</template>
{{ t('features.migration.dialog.restartRecommended') }}
</v-alert>
</div>
<div
v-if="migrationCompleted"
class="text-center py-8"
>
<v-icon
size="64"
color="success"
class="mb-4"
>
mdi-check-circle
</v-icon>
<h3 class="mb-4">
{{ t('features.migration.dialog.completed') }}
</h3>
<p class="mb-4">
{{ migrationResult?.message || t('features.migration.dialog.success') }}
</p>
<v-alert
type="info"
variant="tonal"
class="mb-4"
>
<template #prepend>
<v-icon>mdi-information</v-icon>
</template>
{{ t('features.migration.dialog.restartRecommended') }}
</v-alert>
</div>
<div v-else-if="migrating" class="migration-in-progress">
<div class="text-center py-4">
<v-progress-circular indeterminate color="primary" class="mb-4"></v-progress-circular>
<h3 class="mb-4">{{ t('features.migration.dialog.migrating') }}</h3>
<p class="mb-4">{{ t('features.migration.dialog.migratingSubtitle') }}</p>
</div>
<div class="console-container">
<ConsoleDisplayer ref="consoleDisplayer" :showLevelBtns="false" style="height: 300px;" />
</div>
</div>
<div
v-else-if="migrating"
class="migration-in-progress"
>
<div class="text-center py-4">
<v-progress-circular
indeterminate
color="primary"
class="mb-4"
/>
<h3 class="mb-4">
{{ t('features.migration.dialog.migrating') }}
</h3>
<p class="mb-4">
{{ t('features.migration.dialog.migratingSubtitle') }}
</p>
</div>
<div class="console-container">
<ConsoleDisplayer
ref="consoleDisplayer"
:show-level-btns="false"
style="height: 300px;"
/>
</div>
</div>
<div v-else-if="loading" class="text-center py-8">
<v-progress-circular indeterminate color="primary" class="mb-4"></v-progress-circular>
<p>{{ t('features.migration.dialog.loading') }}</p>
</div>
<div
v-else-if="loading"
class="text-center py-8"
>
<v-progress-circular
indeterminate
color="primary"
class="mb-4"
/>
<p>{{ t('features.migration.dialog.loading') }}</p>
</div>
<div v-else-if="error" class="text-center py-4">
<v-alert type="error" variant="tonal" class="mb-4">
<template v-slot:prepend>
<v-icon>mdi-alert</v-icon>
</template>
{{ error }}
</v-alert>
<v-btn color="primary" @click="loadPlatforms">
{{ t('features.migration.dialog.retry') }}
</v-btn>
</div>
<div
v-else-if="error"
class="text-center py-4"
>
<v-alert
type="error"
variant="tonal"
class="mb-4"
>
<template #prepend>
<v-icon>mdi-alert</v-icon>
</template>
{{ error }}
</v-alert>
<v-btn
color="primary"
@click="loadPlatforms"
>
{{ t('features.migration.dialog.retry') }}
</v-btn>
</div>
<div v-else>
<div v-if="platformGroups.length === 0" class="text-center py-4">
<v-alert type="info" variant="tonal">
<template v-slot:prepend>
<v-icon>mdi-information</v-icon>
</template>
{{ t('features.migration.dialog.noPlatforms') }}
</v-alert>
</div>
<div v-else>
<div
v-if="platformGroups.length === 0"
class="text-center py-4"
>
<v-alert
type="info"
variant="tonal"
>
<template #prepend>
<v-icon>mdi-information</v-icon>
</template>
{{ t('features.migration.dialog.noPlatforms') }}
</v-alert>
</div>
<div v-else>
<div v-for="group in platformGroups" :key="group.type" class="mb-6">
<v-card variant="outlined" v-if="group.platforms.length > 1">
<v-card-subtitle class="py-2">
{{ group.type }}
</v-card-subtitle>
<div v-else>
<div
v-for="group in platformGroups"
:key="group.type"
class="mb-6"
>
<v-card
v-if="group.platforms.length > 1"
variant="outlined"
>
<v-card-subtitle class="py-2">
{{ group.type }}
</v-card-subtitle>
<v-divider></v-divider>
<v-divider />
<v-card-text style="padding: 16px;">
<small>请选择该平台类型下您主要使用的平台适配器</small>
<v-radio-group v-model="selectedPlatforms[group.type]" :key="group.type"
hide-details>
<v-radio v-for="platform in group.platforms" :key="platform.id"
:value="platform.id" :label="getPlatformLabel(platform)" color="primary"
class="mb-1"></v-radio>
</v-radio-group>
</v-card-text>
</v-card>
</div>
</div>
</div>
</v-card-text>
<v-card-text style="padding: 16px;">
<small>请选择该平台类型下您主要使用的平台适配器</small>
<v-radio-group
:key="group.type"
v-model="selectedPlatforms[group.type]"
hide-details
>
<v-radio
v-for="platform in group.platforms"
:key="platform.id"
:value="platform.id"
:label="getPlatformLabel(platform)"
color="primary"
class="mb-1"
/>
</v-radio-group>
</v-card-text>
</v-card>
</div>
</div>
</div>
</v-card-text>
<v-card-actions class="px-6 py-4">
<v-spacer></v-spacer>
<template v-if="migrationCompleted">
<v-btn color="grey" variant="text" @click="handleClose">
{{ t('core.common.close') }}
</v-btn>
<v-btn color="primary" variant="elevated" @click="restartAstrBot">
{{ t('features.migration.dialog.restartNow') }}
</v-btn>
</template>
<template v-else>
<v-btn color="grey" variant="text" @click="handleCancel" :disabled="migrating">
{{ t('core.common.cancel') }}
</v-btn>
<v-btn color="primary" variant="elevated" @click="handleMigration" :disabled="!canMigrate || migrating"
:loading="migrating">
{{ t('features.migration.dialog.startMigration') }}
</v-btn>
</template>
</v-card-actions>
</v-card>
</v-dialog>
<v-card-actions class="px-6 py-4">
<v-spacer />
<template v-if="migrationCompleted">
<v-btn
color="grey"
variant="text"
@click="handleClose"
>
{{ t('core.common.close') }}
</v-btn>
<v-btn
color="primary"
variant="elevated"
@click="restartAstrBot"
>
{{ t('features.migration.dialog.restartNow') }}
</v-btn>
</template>
<template v-else>
<v-btn
color="grey"
variant="text"
:disabled="migrating"
@click="handleCancel"
>
{{ t('core.common.cancel') }}
</v-btn>
<v-btn
color="primary"
variant="elevated"
:disabled="!canMigrate || migrating"
:loading="migrating"
@click="handleMigration"
>
{{ t('features.migration.dialog.startMigration') }}
</v-btn>
</template>
</v-card-actions>
</v-card>
</v-dialog>
<WaitingForRestart ref="wfr"></WaitingForRestart>
<WaitingForRestart ref="wfr" />
</template>
<script setup>

View File

@@ -1,35 +1,74 @@
<template>
<div class="d-flex align-center justify-space-between">
<div>
<span v-if="!modelValue || Object.keys(modelValue).length === 0" style="color: rgb(var(--v-theme-primaryText));">
<span
v-if="!modelValue || Object.keys(modelValue).length === 0"
style="color: rgb(var(--v-theme-primaryText));"
>
{{ t('core.common.objectEditor.noItems') }}
</span>
<div v-else class="d-flex flex-wrap ga-2">
<v-chip v-for="key in displayKeys" :key="key" size="x-small" label color="primary">
<div
v-else
class="d-flex flex-wrap ga-2"
>
<v-chip
v-for="key in displayKeys"
:key="key"
size="x-small"
label
color="primary"
>
{{ key.length > 20 ? key.slice(0, 20) + '...' : key }}
</v-chip>
<v-chip v-if="Object.keys(modelValue).length > maxDisplayItems" size="x-small" label color="grey-lighten-1">
<v-chip
v-if="Object.keys(modelValue).length > maxDisplayItems"
size="x-small"
label
color="grey-lighten-1"
>
+{{ Object.keys(modelValue).length - maxDisplayItems }}
</v-chip>
</div>
</div>
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
<v-btn
size="small"
color="primary"
variant="tonal"
@click="openDialog"
>
{{ resolveButtonText }}
</v-btn>
</div>
<!-- Key-Value Management Dialog -->
<v-dialog v-model="dialog" max-width="600px">
<v-dialog
v-model="dialog"
max-width="600px"
>
<v-card>
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
<v-card-title
class="text-h3 py-4"
style="font-weight: normal;"
>
{{ resolveDialogTitle }}
</v-card-title>
<v-card-text class="pa-4" style="max-height: 400px; overflow-y: auto;">
<v-card-text
class="pa-4"
style="max-height: 400px; overflow-y: auto;"
>
<!-- Regular key-value pairs (non-template) -->
<div v-if="nonTemplatePairs.length > 0">
<div v-for="(pair, index) in nonTemplatePairs" :key="index" class="key-value-pair">
<v-row no-gutters align="center" class="mb-2">
<div
v-for="(pair, index) in nonTemplatePairs"
:key="index"
class="key-value-pair"
>
<v-row
no-gutters
align="center"
class="mb-2"
>
<v-col cols="4">
<v-text-field
v-model="pair.key"
@@ -38,9 +77,12 @@
hide-details
:placeholder="t('core.common.objectEditor.placeholders.keyName')"
@blur="updateKey(index, pair.key)"
></v-text-field>
/>
</v-col>
<v-col cols="7" class="pl-2 d-flex align-center justify-end">
<v-col
cols="7"
class="pl-2 d-flex align-center justify-end"
>
<v-text-field
v-if="pair.type === 'string'"
v-model="pair.value"
@@ -48,12 +90,14 @@
variant="outlined"
hide-details
:placeholder="t('core.common.objectEditor.placeholders.stringValue')"
></v-text-field>
<div v-else-if="pair.type === 'number' || pair.type === 'float' || pair.type === 'int'" class="d-flex align-center gap-2 flex-grow-1">
/>
<div
v-else-if="pair.type === 'number' || pair.type === 'float' || pair.type === 'int'"
class="d-flex align-center gap-2 flex-grow-1"
>
<v-slider
v-if="pair.slider"
:model-value="Number(pair.value) || 0"
@update:model-value="pair.value = $event"
:min="pair.slider.min"
:max="pair.slider.max"
:step="pair.slider.step"
@@ -61,7 +105,8 @@
density="compact"
hide-details
class="flex-grow-1"
></v-slider>
@update:model-value="pair.value = $event"
/>
<v-text-field
v-model.number="pair.value"
type="number"
@@ -70,7 +115,7 @@
hide-details
:placeholder="t('core.common.objectEditor.placeholders.numberValue')"
:style="pair.slider ? 'max-width: 120px;' : ''"
></v-text-field>
/>
</div>
<v-switch
v-else-if="pair.type === 'boolean'"
@@ -78,7 +123,7 @@
density="compact"
hide-details
color="primary"
></v-switch>
/>
<v-text-field
v-if="pair.type === 'json'"
v-model="pair.value"
@@ -86,11 +131,14 @@
variant="outlined"
hide-details="auto"
:placeholder="t('core.common.objectEditor.placeholders.jsonValue')"
@blur="updateJSON(index, pair.value)"
:error-messages="pair.jsonError"
></v-text-field>
@blur="updateJSON(index, pair.value)"
/>
</v-col>
<v-col cols="1" class="pl-2">
<v-col
cols="1"
class="pl-2"
>
<v-btn
icon
variant="text"
@@ -106,32 +154,55 @@
</div>
<!-- Template schema fields -->
<div v-if="hasTemplateSchema" class="mt-4">
<v-divider class="mb-3"></v-divider>
<div class="text-caption text-grey mb-2">{{ t('core.common.objectEditor.presets') }}</div>
<div v-for="(template, templateKey) in templateSchema" :key="templateKey" class="template-field" :class="{ 'template-field-inactive': !isTemplateKeyAdded(templateKey) }">
<v-row no-gutters align="center" class="mb-2">
<div
v-if="hasTemplateSchema"
class="mt-4"
>
<v-divider class="mb-3" />
<div class="text-caption text-grey mb-2">
{{ t('core.common.objectEditor.presets') }}
</div>
<div
v-for="(template, templateKey) in templateSchema"
:key="templateKey"
class="template-field"
:class="{ 'template-field-inactive': !isTemplateKeyAdded(templateKey) }"
>
<v-row
no-gutters
align="center"
class="mb-2"
>
<v-col cols="4">
<div class="d-flex flex-column">
<span class="text-caption font-weight-medium">{{ getTemplateTitle(template, templateKey) }}</span>
<span v-if="template.hint" class="text-caption text-grey" style="font-size: 0.7rem;">{{ translateIfKey(template.hint) }}</span>
<span
v-if="template.hint"
class="text-caption text-grey"
style="font-size: 0.7rem;"
>{{ translateIfKey(template.hint) }}</span>
</div>
</v-col>
<v-col cols="7" class="pl-2 d-flex align-center justify-end">
<v-col
cols="7"
class="pl-2 d-flex align-center justify-end"
>
<v-text-field
v-if="template.type === 'string'"
:model-value="getTemplateValue(templateKey)"
@update:model-value="updateTemplateValue(templateKey, $event)"
density="compact"
variant="outlined"
hide-details
:placeholder="t('core.common.objectEditor.placeholders.stringValue')"
></v-text-field>
<div v-else-if="template.type === 'number' || template.type === 'float' || template.type === 'int'" class="d-flex align-center ga-4 flex-grow-1">
@update:model-value="updateTemplateValue(templateKey, $event)"
/>
<div
v-else-if="template.type === 'number' || template.type === 'float' || template.type === 'int'"
class="d-flex align-center ga-4 flex-grow-1"
>
<v-slider
v-if="template.slider"
:model-value="Number(getTemplateValue(templateKey)) || 0"
@update:model-value="updateTemplateValue(templateKey, $event)"
:min="template.slider.min"
:max="template.slider.max"
:step="template.slider.step"
@@ -139,28 +210,32 @@
density="compact"
hide-details
class="flex-grow-1"
></v-slider>
@update:model-value="updateTemplateValue(templateKey, $event)"
/>
<v-text-field
:model-value="getTemplateValue(templateKey)"
@update:model-value="updateTemplateValue(templateKey, $event)"
type="number"
density="compact"
variant="outlined"
hide-details
:placeholder="t('core.common.objectEditor.placeholders.numberValue')"
:style="template.slider ? 'max-width: 120px;' : ''"
></v-text-field>
@update:model-value="updateTemplateValue(templateKey, $event)"
/>
</div>
<v-switch
v-else-if="template.type === 'boolean' || template.type === 'bool'"
:model-value="getTemplateValue(templateKey)"
@update:model-value="updateTemplateValue(templateKey, $event)"
density="compact"
hide-details
color="primary"
></v-switch>
@update:model-value="updateTemplateValue(templateKey, $event)"
/>
</v-col>
<v-col cols="1" class="pl-2">
<v-col
cols="1"
class="pl-2"
>
<v-btn
v-if="isTemplateKeyAdded(templateKey)"
icon
@@ -176,9 +251,19 @@
</div>
</div>
<div v-if="localKeyValuePairs.length === 0 && !hasTemplateSchema" class="text-center py-8">
<v-icon size="64" color="grey-lighten-1">mdi-code-json</v-icon>
<p class="text-grey mt-4">{{ t('core.common.objectEditor.noParams') }}</p>
<div
v-if="localKeyValuePairs.length === 0 && !hasTemplateSchema"
class="text-center py-8"
>
<v-icon
size="64"
color="grey-lighten-1"
>
mdi-code-json
</v-icon>
<p class="text-grey mt-4">
{{ t('core.common.objectEditor.noParams') }}
</p>
</div>
</v-card-text>
@@ -192,7 +277,7 @@
variant="outlined"
hide-details
class="flex-grow-1"
></v-text-field>
/>
<v-select
v-model="newValueType"
:items="['string', 'number', 'boolean', 'json']"
@@ -201,8 +286,12 @@
variant="outlined"
hide-details
style="max-width: 120px;"
></v-select>
<v-btn @click="addKeyValuePair" variant="tonal" color="primary">
/>
<v-btn
variant="tonal"
color="primary"
@click="addKeyValuePair"
>
<v-icon>mdi-plus</v-icon>
{{ t('core.common.add') }}
</v-btn>
@@ -210,9 +299,19 @@
</v-card-text>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="cancelDialog">{{ t('core.common.cancel') }}</v-btn>
<v-btn color="primary" @click="confirmDialog">{{ t('core.common.confirm') }}</v-btn>
<v-spacer />
<v-btn
variant="text"
@click="cancelDialog"
>
{{ t('core.common.cancel') }}
</v-btn>
<v-btn
color="primary"
@click="confirmDialog"
>
{{ t('core.common.confirm') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>

View File

@@ -1,325 +1,584 @@
<template>
<v-dialog v-model="showDialog" :max-width="$vuetify.display.smAndDown ? undefined : '1200px'" scrollable>
<v-card class="persona-form-card" :class="{ 'persona-form-card-mobile': $vuetify.display.smAndDown }">
<v-card-title class="persona-form-title text-h2 px-6 pt-6 pl-6">
{{ editingPersona ? tm('dialog.edit.title') : tm('dialog.create.title') }}
</v-card-title>
<v-dialog
v-model="showDialog"
:max-width="$vuetify.display.smAndDown ? undefined : '1200px'"
scrollable
>
<v-card
class="persona-form-card"
:class="{ 'persona-form-card-mobile': $vuetify.display.smAndDown }"
>
<v-card-title class="persona-form-title text-h2 px-6 pt-6 pl-6">
{{ editingPersona ? tm('dialog.edit.title') : tm('dialog.create.title') }}
</v-card-title>
<v-card-text class="persona-form-content">
<!-- 创建位置提示 -->
<v-alert v-if="!editingPersona" type="info" variant="tonal" density="compact" class="mb-4"
icon="mdi-folder-outline">
{{ tm('form.createInFolder', { folder: folderDisplayName }) }}
</v-alert>
<v-card-text class="persona-form-content">
<!-- 创建位置提示 -->
<v-alert
v-if="!editingPersona"
type="info"
variant="tonal"
density="compact"
class="mb-4"
icon="mdi-folder-outline"
>
{{ tm('form.createInFolder', { folder: folderDisplayName }) }}
</v-alert>
<v-form ref="personaForm" v-model="formValid">
<v-row class="persona-form-layout">
<v-col cols="12" md="6" class="persona-basic-col">
<v-text-field v-model="personaForm.persona_id" :label="tm('form.personaId')"
:rules="personaIdRules" :disabled="editingPersona" variant="outlined"
density="comfortable" class="mb-4" />
<v-form
ref="personaForm"
v-model="formValid"
>
<v-row class="persona-form-layout">
<v-col
cols="12"
md="6"
class="persona-basic-col"
>
<v-text-field
v-model="personaForm.persona_id"
:label="tm('form.personaId')"
:rules="personaIdRules"
:disabled="editingPersona"
variant="outlined"
density="comfortable"
class="mb-4"
/>
<v-textarea v-model="personaForm.system_prompt" :label="tm('form.systemPrompt')"
:rules="systemPromptRules" variant="outlined" rows="16" class="mb-4" />
<v-textarea
v-model="personaForm.system_prompt"
:label="tm('form.systemPrompt')"
:rules="systemPromptRules"
variant="outlined"
rows="16"
class="mb-4"
/>
<v-textarea
v-model="personaForm.custom_error_message"
:label="tm('form.customErrorMessage')"
:hint="tm('form.customErrorMessageHelp')"
variant="outlined"
rows="4"
persistent-hint
clearable
class="mb-4"
/>
</v-col>
<v-textarea
v-model="personaForm.custom_error_message"
:label="tm('form.customErrorMessage')"
:hint="tm('form.customErrorMessageHelp')"
variant="outlined"
rows="4"
persistent-hint
clearable
class="mb-4"
/>
</v-col>
<v-col cols="12" md="6" class="persona-panels-col">
<v-expansion-panels v-model="expandedPanels" multiple>
<!-- 工具选择面板 -->
<v-expansion-panel value="tools">
<v-expansion-panel-title>
<v-icon class="mr-2">mdi-tools</v-icon>
{{ tm('form.tools') }}
<v-chip v-if="Array.isArray(personaForm.tools) && personaForm.tools.length > 0"
size="small" color="primary" variant="tonal" class="ml-2">
{{ personaForm.tools.length }}
<v-col
cols="12"
md="6"
class="persona-panels-col"
>
<v-expansion-panels
v-model="expandedPanels"
multiple
>
<!-- 工具选择面板 -->
<v-expansion-panel value="tools">
<v-expansion-panel-title>
<v-icon class="mr-2">
mdi-tools
</v-icon>
{{ tm('form.tools') }}
<v-chip
v-if="Array.isArray(personaForm.tools) && personaForm.tools.length > 0"
size="small"
color="primary"
variant="tonal"
class="ml-2"
>
{{ personaForm.tools.length }}
</v-chip>
</v-expansion-panel-title>
<v-expansion-panel-text>
<div class="mb-3">
<p class="text-body-2 text-medium-emphasis">
{{ tm('form.toolsHelp') }}
</p>
</div>
<v-radio-group
v-model="toolSelectValue"
class="mt-2"
hide-details="true"
>
<v-radio
label="默认使用全部函数工具"
value="0"
/>
<v-radio
label="选择指定函数工具"
value="1"
/>
</v-radio-group>
<div
v-if="toolSelectValue === '1'"
class="mt-3 selected-config-area"
>
<!-- 工具搜索 -->
<v-text-field
v-model="toolSearch"
:label="tm('form.searchTools')"
prepend-inner-icon="mdi-magnify"
variant="outlined"
density="compact"
hide-details
clearable
class="mb-3"
/>
<!-- MCP 服务器 -->
<div
v-if="mcpServers.length > 0"
class="mb-4"
>
<h4 class="text-subtitle-2 mb-2">
{{ tm('form.mcpServersQuickSelect') }}
</h4>
<div class="d-flex flex-wrap ga-2">
<v-chip
v-for="server in mcpServers"
:key="server.name"
:color="isServerSelected(server) ? 'primary' : 'default'"
:variant="isServerSelected(server) ? 'flat' : 'outlined'"
size="small"
clickable
:disabled="!server.tools || server.tools.length === 0"
@click="toggleMcpServer(server)"
>
<v-icon
start
size="small"
>
mdi-server
</v-icon>
{{ server.name }}
<v-chip-text
v-if="server.tools"
class="ml-1"
>
({{ server.tools.length }})
</v-chip-text>
</v-chip>
</div>
</div>
<!-- 工具选择列表 -->
<div
v-if="filteredTools.length > 0"
class="tools-selection"
>
<v-virtual-scroll
:items="filteredTools"
height="300"
item-height="72"
>
<template #default="{ item }">
<v-list-item
:key="item.name"
density="comfortable"
@click="toggleTool(item.name)"
>
<template #prepend>
<v-checkbox-btn
:model-value="isToolSelected(item.name)"
@click.stop="toggleTool(item.name)"
/>
</template>
<v-list-item-title>
{{ item.name }}
<v-chip
v-if="item.origin"
size="x-small"
color="info"
class="mr-2"
variant="tonal"
>
{{ item.origin }}
</v-chip>
</v-expansion-panel-title>
<v-expansion-panel-text>
<div class="mb-3">
<p class="text-body-2 text-medium-emphasis">
{{ tm('form.toolsHelp') }}
</p>
</div>
<v-radio-group class="mt-2" v-model="toolSelectValue" hide-details="true">
<v-radio label="默认使用全部函数工具" value="0"></v-radio>
<v-radio label="选择指定函数工具" value="1">
</v-radio>
</v-radio-group>
<div v-if="toolSelectValue === '1'" class="mt-3 selected-config-area">
<!-- 工具搜索 -->
<v-text-field v-model="toolSearch" :label="tm('form.searchTools')"
prepend-inner-icon="mdi-magnify" variant="outlined" density="compact"
hide-details clearable class="mb-3" />
<!-- MCP 服务器 -->
<div v-if="mcpServers.length > 0" class="mb-4">
<h4 class="text-subtitle-2 mb-2">{{ tm('form.mcpServersQuickSelect') }}</h4>
<div class="d-flex flex-wrap ga-2">
<v-chip v-for="server in mcpServers" :key="server.name"
:color="isServerSelected(server) ? 'primary' : 'default'"
:variant="isServerSelected(server) ? 'flat' : 'outlined'" size="small"
clickable @click="toggleMcpServer(server)"
:disabled="!server.tools || server.tools.length === 0">
<v-icon start size="small">mdi-server</v-icon>
{{ server.name }}
<v-chip-text v-if="server.tools" class="ml-1">
({{ server.tools.length }})
</v-chip-text>
</v-chip>
</div>
</div>
<!-- 工具选择列表 -->
<div v-if="filteredTools.length > 0" class="tools-selection">
<v-virtual-scroll :items="filteredTools" height="300" item-height="72">
<template v-slot:default="{ item }">
<v-list-item :key="item.name" density="comfortable"
@click="toggleTool(item.name)">
<template v-slot:prepend>
<v-checkbox-btn :model-value="isToolSelected(item.name)"
@click.stop="toggleTool(item.name)" />
</template>
<v-list-item-title>
{{ item.name }}
<v-chip v-if="item.origin" size="x-small" color="info" class="mr-2"
variant="tonal">
{{ item.origin }}
</v-chip>
<v-chip v-if="item.origin_name" size="x-small" color="info"
variant="outlined">
{{ item.origin_name }}
</v-chip>
</v-list-item-title>
<v-list-item-subtitle v-if="item.description">
{{ truncateText(item.description, 100) }}
</v-list-item-subtitle>
</v-list-item>
</template>
</v-virtual-scroll>
</div>
<div v-else-if="!loadingTools && availableTools.length === 0"
class="text-center pa-4">
<v-icon size="48" color="grey-lighten-2" class="mb-2">mdi-tools</v-icon>
<p class="text-body-2 text-medium-emphasis">{{ tm('form.noToolsAvailable')
}}
</p>
</div>
<div v-else-if="!loadingTools && filteredTools.length === 0"
class="text-center pa-4">
<v-icon size="48" color="grey-lighten-2" class="mb-2">mdi-magnify</v-icon>
<p class="text-body-2 text-medium-emphasis">{{ tm('form.noToolsFound') }}
</p>
</div>
<!-- 加载状态 -->
<div v-if="loadingTools" class="text-center pa-4">
<v-progress-circular indeterminate color="primary" />
<p class="text-body-2 text-medium-emphasis mt-2">{{ tm('form.loadingTools')
}}
</p>
</div>
<!-- 已选择的工具 -->
<div class="mt-4">
<h4 class="text-subtitle-2 mb-2">
{{ tm('form.selectedTools') }}
<span v-if="personaForm.tools === null" class="text-success">
({{ tm('form.allSelected') }})
</span>
<span v-else-if="Array.isArray(personaForm.tools)">
({{ personaForm.tools.length }})
</span>
</h4>
<div v-if="Array.isArray(personaForm.tools) && personaForm.tools.length > 0"
class="d-flex flex-wrap ga-1" style="max-height: 100px; overflow-y: auto;">
<v-chip v-for="toolName in personaForm.tools" :key="toolName" size="small"
color="primary" variant="tonal" closable
@click:close="removeTool(toolName)">
{{ toolName }}
</v-chip>
</div>
<div v-else class="text-body-2 text-medium-emphasis">
{{ tm('form.noToolsSelected') }}
</div>
</div>
</div>
</v-expansion-panel-text>
</v-expansion-panel>
<!-- Skills 选择面板 -->
<v-expansion-panel value="skills">
<v-expansion-panel-title>
<v-icon class="mr-2">mdi-lightning-bolt</v-icon>
{{ tm('form.skills') }}
<v-chip v-if="Array.isArray(personaForm.skills) && personaForm.skills.length > 0"
size="small" color="primary" variant="tonal" class="ml-2">
{{ personaForm.skills.length }}
<v-chip
v-if="item.origin_name"
size="x-small"
color="info"
variant="outlined"
>
{{ item.origin_name }}
</v-chip>
</v-expansion-panel-title>
</v-list-item-title>
<v-expansion-panel-text>
<div class="mb-3">
<p class="text-body-2 text-medium-emphasis">
{{ tm('form.skillsHelp') }}
</p>
</div>
<v-list-item-subtitle v-if="item.description">
{{ truncateText(item.description, 100) }}
</v-list-item-subtitle>
</v-list-item>
</template>
</v-virtual-scroll>
</div>
<v-radio-group class="mt-2" v-model="skillSelectValue" hide-details="true">
<v-radio :label="tm('form.skillsAllAvailable')" value="0"></v-radio>
<v-radio :label="tm('form.skillsSelectSpecific')" value="1"></v-radio>
</v-radio-group>
<div
v-else-if="!loadingTools && availableTools.length === 0"
class="text-center pa-4"
>
<v-icon
size="48"
color="grey-lighten-2"
class="mb-2"
>
mdi-tools
</v-icon>
<p class="text-body-2 text-medium-emphasis">
{{ tm('form.noToolsAvailable')
}}
</p>
</div>
<div v-if="skillSelectValue === '1'" class="mt-3 selected-config-area">
<v-text-field v-model="skillSearch" :label="tm('form.searchSkills')"
prepend-inner-icon="mdi-magnify" variant="outlined" density="compact"
hide-details clearable class="mb-3" />
<div
v-else-if="!loadingTools && filteredTools.length === 0"
class="text-center pa-4"
>
<v-icon
size="48"
color="grey-lighten-2"
class="mb-2"
>
mdi-magnify
</v-icon>
<p class="text-body-2 text-medium-emphasis">
{{ tm('form.noToolsFound') }}
</p>
</div>
<div v-if="filteredSkills.length > 0" class="skills-selection">
<v-virtual-scroll :items="filteredSkills" height="240" item-height="48">
<template v-slot:default="{ item }">
<v-list-item :key="item.name" density="comfortable"
@click="toggleSkill(item.name)">
<template v-slot:prepend>
<v-checkbox-btn :model-value="isSkillSelected(item.name)"
@click.stop="toggleSkill(item.name)" />
</template>
<v-list-item-title>
{{ item.name }}
</v-list-item-title>
<v-list-item-subtitle v-if="item.description">
{{ truncateText(item.description, 100) }}
</v-list-item-subtitle>
</v-list-item>
</template>
</v-virtual-scroll>
</div>
<!-- 加载状态 -->
<div
v-if="loadingTools"
class="text-center pa-4"
>
<v-progress-circular
indeterminate
color="primary"
/>
<p class="text-body-2 text-medium-emphasis mt-2">
{{ tm('form.loadingTools')
}}
</p>
</div>
<div v-else-if="!loadingSkills && availableSkills.length === 0"
class="text-center pa-4">
<v-icon size="48" color="grey-lighten-2"
class="mb-2">mdi-lightning-bolt</v-icon>
<p class="text-body-2 text-medium-emphasis">{{ tm('form.noSkillsAvailable') }}
</p>
</div>
<!-- 已选择的工具 -->
<div class="mt-4">
<h4 class="text-subtitle-2 mb-2">
{{ tm('form.selectedTools') }}
<span
v-if="personaForm.tools === null"
class="text-success"
>
({{ tm('form.allSelected') }})
</span>
<span v-else-if="Array.isArray(personaForm.tools)">
({{ personaForm.tools.length }})
</span>
</h4>
<div
v-if="Array.isArray(personaForm.tools) && personaForm.tools.length > 0"
class="d-flex flex-wrap ga-1"
style="max-height: 100px; overflow-y: auto;"
>
<v-chip
v-for="toolName in personaForm.tools"
:key="toolName"
size="small"
color="primary"
variant="tonal"
closable
@click:close="removeTool(toolName)"
>
{{ toolName }}
</v-chip>
</div>
<div
v-else
class="text-body-2 text-medium-emphasis"
>
{{ tm('form.noToolsSelected') }}
</div>
</div>
</div>
</v-expansion-panel-text>
</v-expansion-panel>
<div v-else-if="!loadingSkills && filteredSkills.length === 0"
class="text-center pa-4">
<v-icon size="48" color="grey-lighten-2" class="mb-2">mdi-magnify</v-icon>
<p class="text-body-2 text-medium-emphasis">{{ tm('form.noSkillsFound') }}
</p>
</div>
<!-- Skills 选择面板 -->
<v-expansion-panel value="skills">
<v-expansion-panel-title>
<v-icon class="mr-2">
mdi-lightning-bolt
</v-icon>
{{ tm('form.skills') }}
<v-chip
v-if="Array.isArray(personaForm.skills) && personaForm.skills.length > 0"
size="small"
color="primary"
variant="tonal"
class="ml-2"
>
{{ personaForm.skills.length }}
</v-chip>
</v-expansion-panel-title>
<div v-if="loadingSkills" class="text-center pa-4">
<v-progress-circular indeterminate color="primary" />
<p class="text-body-2 text-medium-emphasis mt-2">{{ tm('form.loadingSkills') }}
</p>
</div>
<v-expansion-panel-text>
<div class="mb-3">
<p class="text-body-2 text-medium-emphasis">
{{ tm('form.skillsHelp') }}
</p>
</div>
<div class="mt-4">
<h4 class="text-subtitle-2 mb-2">
{{ tm('form.selectedSkills') }}
<span v-if="personaForm.skills === null" class="text-success">
({{ tm('form.allSelected') }})
</span>
<span v-else-if="Array.isArray(personaForm.skills)">
({{ personaForm.skills.length }})
</span>
</h4>
<div v-if="Array.isArray(personaForm.skills) && personaForm.skills.length > 0"
class="d-flex flex-wrap ga-1" style="max-height: 100px; overflow-y: auto;">
<v-chip v-for="skillName in personaForm.skills" :key="skillName"
size="small" color="primary" variant="tonal" closable
@click:close="removeSkill(skillName)">
{{ skillName }}
</v-chip>
</div>
<div v-else class="text-body-2 text-medium-emphasis">
{{ tm('form.noSkillsSelected') }}
</div>
</div>
</div>
</v-expansion-panel-text>
</v-expansion-panel>
<v-radio-group
v-model="skillSelectValue"
class="mt-2"
hide-details="true"
>
<v-radio
:label="tm('form.skillsAllAvailable')"
value="0"
/>
<v-radio
:label="tm('form.skillsSelectSpecific')"
value="1"
/>
</v-radio-group>
<!-- 预设对话面板 -->
<v-expansion-panel value="dialogs">
<v-expansion-panel-title>
<v-icon class="mr-2">mdi-chat</v-icon>
{{ tm('form.presetDialogs') }}
<v-chip v-if="personaForm.begin_dialogs.length > 0" size="small" color="primary"
variant="tonal" class="ml-2">
{{ personaForm.begin_dialogs.length / 2 }}
</v-chip>
</v-expansion-panel-title>
<div
v-if="skillSelectValue === '1'"
class="mt-3 selected-config-area"
>
<v-text-field
v-model="skillSearch"
:label="tm('form.searchSkills')"
prepend-inner-icon="mdi-magnify"
variant="outlined"
density="compact"
hide-details
clearable
class="mb-3"
/>
<v-expansion-panel-text>
<div class="mb-3">
<p class="text-body-2 text-medium-emphasis">
{{ tm('form.presetDialogsHelp') }}
</p>
</div>
<div
v-if="filteredSkills.length > 0"
class="skills-selection"
>
<v-virtual-scroll
:items="filteredSkills"
height="240"
item-height="48"
>
<template #default="{ item }">
<v-list-item
:key="item.name"
density="comfortable"
@click="toggleSkill(item.name)"
>
<template #prepend>
<v-checkbox-btn
:model-value="isSkillSelected(item.name)"
@click.stop="toggleSkill(item.name)"
/>
</template>
<v-list-item-title>
{{ item.name }}
</v-list-item-title>
<v-list-item-subtitle v-if="item.description">
{{ truncateText(item.description, 100) }}
</v-list-item-subtitle>
</v-list-item>
</template>
</v-virtual-scroll>
</div>
<div v-for="(dialog, index) in personaForm.begin_dialogs" :key="index" class="mb-3">
<v-textarea v-model="personaForm.begin_dialogs[index]"
:label="index % 2 === 0 ? tm('form.userMessage') : tm('form.assistantMessage')"
:rules="getDialogRules(index)" variant="outlined" rows="2"
density="comfortable">
<template v-slot:append>
<v-btn icon="mdi-delete" variant="text" size="small" color="error"
@click="removeDialog(index)" />
</template>
</v-textarea>
</div>
<div
v-else-if="!loadingSkills && availableSkills.length === 0"
class="text-center pa-4"
>
<v-icon
size="48"
color="grey-lighten-2"
class="mb-2"
>
mdi-lightning-bolt
</v-icon>
<p class="text-body-2 text-medium-emphasis">
{{ tm('form.noSkillsAvailable') }}
</p>
</div>
<v-btn variant="outlined" prepend-icon="mdi-plus" @click="addDialogPair" block>
{{ tm('buttons.addDialogPair') }}
</v-btn>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-col>
</v-row>
</v-form>
</v-card-text>
<div
v-else-if="!loadingSkills && filteredSkills.length === 0"
class="text-center pa-4"
>
<v-icon
size="48"
color="grey-lighten-2"
class="mb-2"
>
mdi-magnify
</v-icon>
<p class="text-body-2 text-medium-emphasis">
{{ tm('form.noSkillsFound') }}
</p>
</div>
<v-card-actions class="persona-form-actions">
<v-btn v-if="editingPersona" color="error" variant="text" @click="deletePersona">
{{ tm('buttons.delete') }}
</v-btn>
<v-spacer />
<v-btn color="grey" variant="text" @click="closeDialog">
{{ tm('buttons.cancel') }}
</v-btn>
<v-btn color="primary" variant="flat" @click="savePersona" :loading="saving" :disabled="!formValid">
{{ tm('buttons.save') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<div
v-if="loadingSkills"
class="text-center pa-4"
>
<v-progress-circular
indeterminate
color="primary"
/>
<p class="text-body-2 text-medium-emphasis mt-2">
{{ tm('form.loadingSkills') }}
</p>
</div>
<div class="mt-4">
<h4 class="text-subtitle-2 mb-2">
{{ tm('form.selectedSkills') }}
<span
v-if="personaForm.skills === null"
class="text-success"
>
({{ tm('form.allSelected') }})
</span>
<span v-else-if="Array.isArray(personaForm.skills)">
({{ personaForm.skills.length }})
</span>
</h4>
<div
v-if="Array.isArray(personaForm.skills) && personaForm.skills.length > 0"
class="d-flex flex-wrap ga-1"
style="max-height: 100px; overflow-y: auto;"
>
<v-chip
v-for="skillName in personaForm.skills"
:key="skillName"
size="small"
color="primary"
variant="tonal"
closable
@click:close="removeSkill(skillName)"
>
{{ skillName }}
</v-chip>
</div>
<div
v-else
class="text-body-2 text-medium-emphasis"
>
{{ tm('form.noSkillsSelected') }}
</div>
</div>
</div>
</v-expansion-panel-text>
</v-expansion-panel>
<!-- 预设对话面板 -->
<v-expansion-panel value="dialogs">
<v-expansion-panel-title>
<v-icon class="mr-2">
mdi-chat
</v-icon>
{{ tm('form.presetDialogs') }}
<v-chip
v-if="personaForm.begin_dialogs.length > 0"
size="small"
color="primary"
variant="tonal"
class="ml-2"
>
{{ personaForm.begin_dialogs.length / 2 }}
</v-chip>
</v-expansion-panel-title>
<v-expansion-panel-text>
<div class="mb-3">
<p class="text-body-2 text-medium-emphasis">
{{ tm('form.presetDialogsHelp') }}
</p>
</div>
<div
v-for="(dialog, index) in personaForm.begin_dialogs"
:key="index"
class="mb-3"
>
<v-textarea
v-model="personaForm.begin_dialogs[index]"
:label="index % 2 === 0 ? tm('form.userMessage') : tm('form.assistantMessage')"
:rules="getDialogRules(index)"
variant="outlined"
rows="2"
density="comfortable"
>
<template #append>
<v-btn
icon="mdi-delete"
variant="text"
size="small"
color="error"
@click="removeDialog(index)"
/>
</template>
</v-textarea>
</div>
<v-btn
variant="outlined"
prepend-icon="mdi-plus"
block
@click="addDialogPair"
>
{{ tm('buttons.addDialogPair') }}
</v-btn>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-col>
</v-row>
</v-form>
</v-card-text>
<v-card-actions class="persona-form-actions">
<v-btn
v-if="editingPersona"
color="error"
variant="text"
@click="deletePersona"
>
{{ tm('buttons.delete') }}
</v-btn>
<v-spacer />
<v-btn
color="grey"
variant="text"
@click="closeDialog"
>
{{ tm('buttons.cancel') }}
</v-btn>
<v-btn
color="primary"
variant="flat"
:loading="saving"
:disabled="!formValid"
@click="savePersona"
>
{{ tm('buttons.save') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>

View File

@@ -4,24 +4,46 @@
<small>{{ tm('personaQuickPreview.title') }}</small>
</div>
<div v-if="loading" class="preview-loading">
<v-progress-circular indeterminate size="18" width="2" color="primary" class="mr-2" />
<div
v-if="loading"
class="preview-loading"
>
<v-progress-circular
indeterminate
size="18"
width="2"
color="primary"
class="mr-2"
/>
<small class="text-grey">{{ tm('personaQuickPreview.loading') }}</small>
</div>
<div v-else-if="!modelValue" class="preview-empty">
<div
v-else-if="!modelValue"
class="preview-empty"
>
<small class="text-grey">{{ tm('personaQuickPreview.noPersonaSelected') }}</small>
</div>
<div v-else-if="!personaData" class="preview-empty">
<div
v-else-if="!personaData"
class="preview-empty"
>
<small class="text-grey">{{ tm('personaQuickPreview.personaNotFound') }}</small>
</div>
<div v-else class="preview-content">
<div class="section-title">{{ tm('personaQuickPreview.systemPromptLabel') }}</div>
<div
v-else
class="preview-content"
>
<div class="section-title">
{{ tm('personaQuickPreview.systemPromptLabel') }}
</div>
<pre class="prompt-content">{{ personaData.system_prompt || '' }}</pre>
<div class="section-title mt-3">{{ tm('personaQuickPreview.toolsLabel') }}</div>
<div class="section-title mt-3">
{{ tm('personaQuickPreview.toolsLabel') }}
</div>
<div class="chip-wrap tools-wrap">
<v-chip
v-if="personaData.tools === null"
@@ -32,7 +54,12 @@
>
{{ tm('personaQuickPreview.allToolsWithCount', { count: allToolsCount }) }}
</v-chip>
<div v-for="tool in resolvedTools" v-else :key="tool.name" class="tool-item">
<div
v-for="tool in resolvedTools"
v-else
:key="tool.name"
class="tool-item"
>
<v-chip
size="small"
:color="tool.active === false ? 'warning' : 'primary'"
@@ -41,25 +68,39 @@
>
{{ tool.name }}
</v-chip>
<v-tooltip v-if="tool.active === false" location="top">
<template v-slot:activator="{ props: tooltipProps }">
<small class="text-warning tool-inactive" v-bind="tooltipProps">
<v-tooltip
v-if="tool.active === false"
location="top"
>
<template #activator="{ props: tooltipProps }">
<small
class="text-warning tool-inactive"
v-bind="tooltipProps"
>
{{ tm('personaQuickPreview.toolInactive') }}
</small>
</template>
{{ tm('personaQuickPreview.toolInactiveTooltip') }}
</v-tooltip>
<small v-if="tool.origin || tool.origin_name" class="text-grey tool-meta">
<small
v-if="tool.origin || tool.origin_name"
class="text-grey tool-meta"
>
<span v-if="tool.origin">{{ tm('personaQuickPreview.originLabel') }}: {{ tool.origin }}</span>
<span v-if="tool.origin_name"> | {{ tm('personaQuickPreview.originNameLabel') }}: {{ tool.origin_name }}</span>
</small>
</div>
<small v-if="personaData.tools !== null && normalizedTools.length === 0" class="text-grey">
<small
v-if="personaData.tools !== null && normalizedTools.length === 0"
class="text-grey"
>
{{ tm('personaQuickPreview.noTools') }}
</small>
</div>
<div class="section-title mt-3">{{ tm('personaQuickPreview.skillsLabel') }}</div>
<div class="section-title mt-3">
{{ tm('personaQuickPreview.skillsLabel') }}
</div>
<div class="chip-wrap">
<v-chip
v-if="personaData.skills === null"
@@ -81,7 +122,10 @@
>
{{ skillName }}
</v-chip>
<small v-if="personaData.skills !== null && normalizedSkills.length === 0" class="text-grey">
<small
v-if="personaData.skills !== null && normalizedSkills.length === 0"
class="text-grey"
>
{{ tm('personaQuickPreview.noSkills') }}
</small>
</div>

View File

@@ -1,7 +1,6 @@
<template>
<BaseFolderItemSelector
:model-value="modelValue"
@update:model-value="handleUpdate"
:folder-tree="folderTree"
:items="currentPersonas as any"
:tree-loading="treeLoading"
@@ -14,6 +13,7 @@
item-name-field="persona_id"
item-description-field="system_prompt"
:display-value-formatter="formatDisplayValue"
@update:model-value="handleUpdate"
@navigate="handleNavigate"
@create="openCreatePersona"
@edit="openEditPersona"
@@ -26,7 +26,8 @@
:current-folder-id="currentFolderId ?? undefined"
:current-folder-name="currentFolderName ?? undefined"
@saved="handlePersonaSaved"
@error="handleError" />
@error="handleError"
/>
</template>
<script setup lang="ts">

View File

@@ -45,9 +45,15 @@ const platformDetails = computed(() => {
:style="{ cursor: 'pointer', ...chipStyle }"
@click.stop="showMenu = !showMenu"
>
<div class="d-flex align-center" style="gap: 2px">
<div
class="d-flex align-center"
style="gap: 2px"
>
<!-- 显示图标最多 5 -->
<div class="d-flex align-center mr-1" v-if="platformDetails.some(p => p.icon)">
<div
v-if="platformDetails.some(p => p.icon)"
class="d-flex align-center mr-1"
>
<v-avatar
v-for="(platform, index) in platformDetails.slice(0, 5)"
:key="index"
@@ -55,8 +61,15 @@ const platformDetails = computed(() => {
class="platform-mini-icon"
:style="{ marginLeft: index > 0 ? '-4px' : '0', zIndex: 10 - index }"
>
<v-img v-if="platform.icon" :src="platform.icon"></v-img>
<v-icon v-else icon="mdi-circle-small" :size="size === 'x-small' ? 8 : 10"></v-icon>
<v-img
v-if="platform.icon"
:src="platform.icon"
/>
<v-icon
v-else
icon="mdi-circle-small"
:size="size === 'x-small' ? 8 : 10"
/>
</v-avatar>
</div>
@@ -72,7 +85,7 @@ const platformDetails = computed(() => {
:icon="showMenu ? 'mdi-chevron-up' : 'mdi-chevron-down'"
:size="size === 'x-small' ? 14 : 16"
class="ml-n1"
></v-icon>
/>
</div>
<v-menu
@@ -83,20 +96,37 @@ const platformDetails = computed(() => {
transition="scale-transition"
open-on-hover
>
<v-list density="compact" border elevation="12" class="rounded-lg pa-1">
<v-list
density="compact"
border
elevation="12"
class="rounded-lg pa-1"
>
<v-list-item
v-for="platform in platformDetails"
:key="platform.name"
min-height="24"
class="px-2"
>
<template v-slot:prepend>
<v-avatar size="14" class="mr-2" v-if="platform.icon">
<v-img :src="platform.icon"></v-img>
<template #prepend>
<v-avatar
v-if="platform.icon"
size="14"
class="mr-2"
>
<v-img :src="platform.icon" />
</v-avatar>
<v-icon v-else icon="mdi-platform" size="12" class="mr-2"></v-icon>
<v-icon
v-else
icon="mdi-platform"
size="12"
class="mr-2"
/>
</template>
<v-list-item-title class="text-caption font-weight-bold" style="font-size: 0.75rem !important">
<v-list-item-title
class="text-caption font-weight-bold"
style="font-size: 0.75rem !important"
>
{{ platform.name }}
</v-list-item-title>
</v-list-item>

View File

@@ -3,73 +3,113 @@
<!-- 顶部操作区域 -->
<div class="d-flex align-center justify-space-between mb-2">
<div class="flex-grow-1">
<span v-if="!modelValue || modelValue.length === 0" style="color: rgb(var(--v-theme-primaryText));">
<span
v-if="!modelValue || modelValue.length === 0"
style="color: rgb(var(--v-theme-primaryText));"
>
{{ tm('pluginSetSelector.notSelected') }}
</span>
<span v-else-if="isAllPlugins" style="color: rgb(var(--v-theme-primaryText));">
<span
v-else-if="isAllPlugins"
style="color: rgb(var(--v-theme-primaryText));"
>
{{ tm('pluginSetSelector.allPlugins') }}
</span>
<span v-else style="color: rgb(var(--v-theme-primaryText));">
<span
v-else
style="color: rgb(var(--v-theme-primaryText));"
>
{{ tm('pluginSetSelector.selectedCount', { count: modelValue.length }) }}
</span>
</div>
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
<v-btn
size="small"
color="primary"
variant="tonal"
@click="openDialog"
>
{{ buttonText || tm('pluginSetSelector.buttonText') }}
</v-btn>
</div>
</div>
<!-- Plugin Set Selection Dialog -->
<v-dialog v-model="dialog" max-width="700px">
<v-dialog
v-model="dialog"
max-width="700px"
>
<v-card>
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
<v-card-title
class="text-h3 py-4"
style="font-weight: normal;"
>
{{ tm('pluginSetSelector.dialogTitle') }}
</v-card-title>
<v-card-text class="pa-4">
<v-progress-linear v-if="loading" indeterminate color="primary"></v-progress-linear>
<v-progress-linear
v-if="loading"
indeterminate
color="primary"
/>
<div v-if="!loading">
<!-- 预设选项 -->
<v-radio-group v-model="selectionMode" class="mb-4" hide-details>
<v-radio-group
v-model="selectionMode"
class="mb-4"
hide-details
>
<v-radio
value="all"
:label="tm('pluginSetSelector.enableAll')"
color="primary"
></v-radio>
/>
<v-radio
value="none"
:label="tm('pluginSetSelector.enableNone')"
color="primary"
></v-radio>
/>
<v-radio
value="custom"
:label="tm('pluginSetSelector.customSelect')"
color="primary"
></v-radio>
/>
</v-radio-group>
<!-- 自定义选择时显示插件列表 -->
<div v-if="selectionMode === 'custom'" style="max-height: 300px; overflow-y: auto;">
<v-list v-if="pluginList.length > 0" density="compact">
<div
v-if="selectionMode === 'custom'"
style="max-height: 300px; overflow-y: auto;"
>
<v-list
v-if="pluginList.length > 0"
density="compact"
>
<v-list-item
v-for="plugin in pluginList"
:key="plugin.name"
rounded="md"
class="ma-1">
<template v-slot:prepend>
class="ma-1"
>
<template #prepend>
<v-checkbox
v-model="selectedPlugins"
:value="plugin.name"
color="primary"
hide-details
></v-checkbox>
/>
</template>
<v-list-item-title>{{ plugin.name }}</v-list-item-title>
<v-list-item-subtitle>
{{ plugin.desc || tm('pluginSetSelector.noDescription') }}
<v-chip v-if="!plugin.activated" size="x-small" color="grey" class="ml-1">
<v-chip
v-if="!plugin.activated"
size="x-small"
color="grey"
class="ml-1"
>
{{ tm('pluginSetSelector.notActivated') }}
</v-chip>
</v-list-item-subtitle>
@@ -80,20 +120,36 @@
</div>
</v-list>
<div v-else class="text-center py-8">
<v-icon size="64" color="grey-lighten-1">mdi-puzzle-outline</v-icon>
<p class="text-grey mt-4">{{ tm('pluginSetSelector.noPlugins') }}</p>
<div
v-else
class="text-center py-8"
>
<v-icon
size="64"
color="grey-lighten-1"
>
mdi-puzzle-outline
</v-icon>
<p class="text-grey mt-4">
{{ tm('pluginSetSelector.noPlugins') }}
</p>
</div>
</div>
</div>
</v-card-text>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="cancelSelection">{{ tm('pluginSetSelector.cancelSelection') }}</v-btn>
<v-spacer />
<v-btn
variant="text"
@click="cancelSelection"
>
{{ tm('pluginSetSelector.cancelSelection') }}
</v-btn>
<v-btn
color="primary"
@click="confirmSelection">
@click="confirmSelection"
>
{{ tm('pluginSetSelector.confirmSelection') }}
</v-btn>
</v-card-actions>

View File

@@ -1,9 +1,15 @@
<template>
<div class="d-flex align-center justify-space-between">
<span v-if="!hasSelection" style="color: rgb(var(--v-theme-primaryText));">
<span
v-if="!hasSelection"
style="color: rgb(var(--v-theme-primaryText));"
>
{{ tm('providerSelector.notSelected') }}
</span>
<span v-else class="provider-name-text">
<span
v-else
class="provider-name-text"
>
<template v-if="multiple">
{{ tm('providerSelector.selectedCount', { count: selectedProviders.length }) }}
</template>
@@ -11,12 +17,20 @@
{{ modelValue }}
</template>
</span>
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
<v-btn
size="small"
color="primary"
variant="tonal"
@click="openDialog"
>
{{ buttonText || tm('providerSelector.buttonText') }}
</v-btn>
</div>
<div v-if="multiple && selectedProviders.length > 0" class="selected-preview mt-2">
<div
v-if="multiple && selectedProviders.length > 0"
class="selected-preview mt-2"
>
<v-chip
v-for="providerId in selectedProviders"
:key="`preview-${providerId}`"
@@ -31,7 +45,10 @@
</div>
<!-- Provider Selection Dialog -->
<v-dialog v-model="dialog" max-width="600px">
<v-dialog
v-model="dialog"
max-width="600px"
>
<v-card>
<v-card-title
class="text-h3 py-4 d-flex align-center justify-space-between gap-4 flex-wrap"
@@ -49,14 +66,27 @@
</v-btn>
</v-card-title>
<v-card-text class="pa-0" style="max-height: 400px; overflow-y: auto;">
<v-progress-linear v-if="loading" indeterminate color="primary"></v-progress-linear>
<v-card-text
class="pa-0"
style="max-height: 400px; overflow-y: auto;"
>
<v-progress-linear
v-if="loading"
indeterminate
color="primary"
/>
<div v-if="multiple && selectedProviders.length > 0" class="pa-3">
<div
v-if="multiple && selectedProviders.length > 0"
class="pa-3"
>
<div class="text-caption text-medium-emphasis mb-2">
{{ tm('providerSelector.selectedCount', { count: selectedProviders.length }) }}
</div>
<v-list density="compact" class="selected-order-list">
<v-list
density="compact"
class="selected-order-list"
>
<v-list-item
v-for="(providerId, index) in selectedProviders"
:key="`selected-${providerId}-${index}`"
@@ -90,63 +120,94 @@
</template>
</v-list-item>
</v-list>
<v-divider class="ma-1"></v-divider>
<v-divider class="ma-1" />
</div>
<v-list v-if="!loading && providerList.length > 0" density="compact">
<v-list
v-if="!loading && providerList.length > 0"
density="compact"
>
<!-- 不选择选项 -->
<v-list-item
v-if="!multiple"
key="none"
value=""
@click="selectProvider({ id: '' })"
:active="selectedProvider === ''"
rounded="md"
class="ma-1">
class="ma-1"
@click="selectProvider({ id: '' })"
>
<v-list-item-title>{{ tm('providerSelector.clearSelection') }}</v-list-item-title>
<v-list-item-subtitle>{{ tm('providerSelector.clearSelectionSubtitle') }}</v-list-item-subtitle>
<template v-slot:append>
<v-icon v-if="selectedProvider === ''" color="primary">mdi-check-circle</v-icon>
<template #append>
<v-icon
v-if="selectedProvider === ''"
color="primary"
>
mdi-check-circle
</v-icon>
</template>
</v-list-item>
<v-divider class="ma-1"></v-divider>
<v-divider class="ma-1" />
<v-list-item
v-for="provider in providerList"
:key="provider.id"
:value="provider.id"
@click="selectProvider(provider)"
:active="isProviderSelected(provider.id)"
rounded="md"
class="ma-1">
class="ma-1"
@click="selectProvider(provider)"
>
<v-list-item-title>{{ provider.id }}</v-list-item-title>
<v-list-item-subtitle>
{{ provider.type || provider.provider_type || tm('providerSelector.unknownType') }}
<span v-if="provider.model">- {{ provider.model }}</span>
</v-list-item-subtitle>
<template v-slot:append>
<v-icon v-if="isProviderSelected(provider.id)" color="primary">mdi-check-circle</v-icon>
<template #append>
<v-icon
v-if="isProviderSelected(provider.id)"
color="primary"
>
mdi-check-circle
</v-icon>
</template>
</v-list-item>
</v-list>
<div v-else-if="!loading && providerList.length === 0" class="text-center py-8">
<v-icon size="64" color="grey-lighten-1">mdi-api-off</v-icon>
<p class="text-grey mt-4">{{ tm('providerSelector.noProviders') }}</p>
<div
v-else-if="!loading && providerList.length === 0"
class="text-center py-8"
>
<v-icon
size="64"
color="grey-lighten-1"
>
mdi-api-off
</v-icon>
<p class="text-grey mt-4">
{{ tm('providerSelector.noProviders') }}
</p>
</div>
</v-card-text>
<v-divider></v-divider>
<v-divider />
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="cancelSelection">{{ tm('providerSelector.cancelSelection') }}</v-btn>
<v-spacer />
<v-btn
variant="text"
@click="cancelSelection"
>
{{ tm('providerSelector.cancelSelection') }}
</v-btn>
<v-btn
color="primary"
@click="confirmSelection">
@click="confirmSelection"
>
{{ tm('providerSelector.confirmSelection') }}
</v-btn>
</v-card-actions>
@@ -161,9 +222,16 @@
:scrim="true"
@click:outside="closeProviderDrawer"
>
<v-card class="provider-drawer-card" elevation="12">
<v-card
class="provider-drawer-card"
elevation="12"
>
<div class="provider-drawer-header">
<v-btn icon variant="text" @click="closeProviderDrawer">
<v-btn
icon
variant="text"
@click="closeProviderDrawer"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</div>

View File

@@ -1,51 +1,90 @@
<template>
<h5>{{ tm('network.proxySelector.title') }}</h5>
<v-radio-group class="mt-2" v-model="radioValue" hide-details="true">
<v-radio :label="tm('network.proxySelector.noProxy')" value="0"></v-radio>
<v-radio value="1">
<template v-slot:label>
<span>{{ tm('network.proxySelector.useProxy') }}</span>
<v-btn v-if="radioValue === '1'" class="ml-2" @click="testAllProxies" size="x-small"
variant="tonal" :loading="loadingTestingConnection">
{{ tm('network.proxySelector.testConnection') }}
</v-btn>
</template>
<h5>{{ tm('network.proxySelector.title') }}</h5>
<v-radio-group
v-model="radioValue"
class="mt-2"
hide-details="true"
>
<v-radio
:label="tm('network.proxySelector.noProxy')"
value="0"
/>
<v-radio value="1">
<template #label>
<span>{{ tm('network.proxySelector.useProxy') }}</span>
<v-btn
v-if="radioValue === '1'"
class="ml-2"
size="x-small"
variant="tonal"
:loading="loadingTestingConnection"
@click="testAllProxies"
>
{{ tm('network.proxySelector.testConnection') }}
</v-btn>
</template>
</v-radio>
</v-radio-group>
<v-expand-transition>
<div
v-if="radioValue === '1'"
style="margin-left: 16px;"
>
<v-radio-group
v-model="githubProxyRadioControl"
class="mt-2"
hide-details="true"
>
<v-radio
v-for="(proxy, idx) in githubProxies"
:key="proxy"
color="success"
:value="String(idx)"
>
<template #label>
<div class="d-flex align-center">
<span class="mr-2">{{ proxy }}</span>
<div v-if="proxyStatus[idx]">
<v-chip
:color="proxyStatus[idx].available ? 'success' : 'error'"
size="x-small"
class="mr-1"
>
{{ proxyStatus[idx].available ? tm('network.proxySelector.available') : tm('network.proxySelector.unavailable') }}
</v-chip>
<v-chip
v-if="proxyStatus[idx].available"
color="info"
size="x-small"
>
{{ proxyStatus[idx].latency }}ms
</v-chip>
</div>
</div>
</template>
</v-radio>
</v-radio-group>
<v-expand-transition>
<div v-if="radioValue === '1'" style="margin-left: 16px;">
<v-radio-group v-model="githubProxyRadioControl" class="mt-2" hide-details="true">
<v-radio color="success" v-for="(proxy, idx) in githubProxies" :key="proxy" :value="String(idx)">
<template v-slot:label>
<div class="d-flex align-center">
<span class="mr-2">{{ proxy }}</span>
<div v-if="proxyStatus[idx]">
<v-chip
:color="proxyStatus[idx].available ? 'success' : 'error'"
size="x-small"
class="mr-1">
{{ proxyStatus[idx].available ? tm('network.proxySelector.available') : tm('network.proxySelector.unavailable') }}
</v-chip>
<v-chip
v-if="proxyStatus[idx].available"
color="info"
size="x-small">
{{ proxyStatus[idx].latency }}ms
</v-chip>
</div>
</div>
</template>
</v-radio>
<v-radio color="primary" value="-1" :label="tm('network.proxySelector.custom')">
<template v-slot:label v-if="String(githubProxyRadioControl) === '-1'">
<v-text-field density="compact" v-model="selectedGitHubProxy" variant="outlined"
style="width: 100vw;" :placeholder="tm('network.proxySelector.custom')" hide-details="true">
</v-text-field>
</template>
</v-radio>
</v-radio-group>
</div>
</v-expand-transition>
<v-radio
color="primary"
value="-1"
:label="tm('network.proxySelector.custom')"
>
<template
v-if="String(githubProxyRadioControl) === '-1'"
#label
>
<v-text-field
v-model="selectedGitHubProxy"
density="compact"
variant="outlined"
style="width: 100vw;"
:placeholder="tm('network.proxySelector.custom')"
hide-details="true"
/>
</template>
</v-radio>
</v-radio-group>
</div>
</v-expand-transition>
</template>
@@ -75,6 +114,64 @@ export default {
initializing: true,
}
},
watch: {
selectedGitHubProxy: function (newVal, oldVal) {
if (this.initializing) {
return;
}
if (!newVal) {
newVal = ""
}
localStorage.setItem('selectedGitHubProxy', newVal);
},
radioValue: function (newVal) {
if (this.initializing) {
return;
}
localStorage.setItem('githubProxyRadioValue', newVal);
if (String(newVal) === "0") {
this.selectedGitHubProxy = "";
} else if (String(this.githubProxyRadioControl) !== "-1") {
this.selectedGitHubProxy = this.getProxyByControl(this.githubProxyRadioControl);
}
},
githubProxyRadioControl: function (newVal) {
if (this.initializing) {
return;
}
const normalizedVal = String(newVal);
localStorage.setItem('githubProxyRadioControl', normalizedVal);
if (String(this.radioValue) !== "1") {
this.selectedGitHubProxy = "";
return;
}
if (normalizedVal !== "-1") {
this.selectedGitHubProxy = this.getProxyByControl(normalizedVal);
}
}
},
mounted() {
this.initializing = true;
const savedProxy = localStorage.getItem('selectedGitHubProxy') || "";
const savedRadio = localStorage.getItem('githubProxyRadioValue') || "0";
const savedControl = String(localStorage.getItem('githubProxyRadioControl') || "0");
this.radioValue = savedRadio;
this.githubProxyRadioControl = savedControl;
if (savedRadio === "1") {
if (savedControl !== "-1") {
this.selectedGitHubProxy = this.getProxyByControl(savedControl);
} else {
this.selectedGitHubProxy = savedProxy;
}
} else {
this.selectedGitHubProxy = "";
}
this.initializing = false;
},
methods: {
getProxyByControl(control) {
const normalizedControl = String(control);
@@ -128,64 +225,6 @@ export default {
await Promise.all(promises);
this.loadingTestingConnection = false;
},
},
mounted() {
this.initializing = true;
const savedProxy = localStorage.getItem('selectedGitHubProxy') || "";
const savedRadio = localStorage.getItem('githubProxyRadioValue') || "0";
const savedControl = String(localStorage.getItem('githubProxyRadioControl') || "0");
this.radioValue = savedRadio;
this.githubProxyRadioControl = savedControl;
if (savedRadio === "1") {
if (savedControl !== "-1") {
this.selectedGitHubProxy = this.getProxyByControl(savedControl);
} else {
this.selectedGitHubProxy = savedProxy;
}
} else {
this.selectedGitHubProxy = "";
}
this.initializing = false;
},
watch: {
selectedGitHubProxy: function (newVal, oldVal) {
if (this.initializing) {
return;
}
if (!newVal) {
newVal = ""
}
localStorage.setItem('selectedGitHubProxy', newVal);
},
radioValue: function (newVal) {
if (this.initializing) {
return;
}
localStorage.setItem('githubProxyRadioValue', newVal);
if (String(newVal) === "0") {
this.selectedGitHubProxy = "";
} else if (String(this.githubProxyRadioControl) !== "-1") {
this.selectedGitHubProxy = this.getProxyByControl(this.githubProxyRadioControl);
}
},
githubProxyRadioControl: function (newVal) {
if (this.initializing) {
return;
}
const normalizedVal = String(newVal);
localStorage.setItem('githubProxyRadioControl', normalizedVal);
if (String(this.radioValue) !== "1") {
this.selectedGitHubProxy = "";
return;
}
if (normalizedVal !== "-1") {
this.selectedGitHubProxy = this.getProxyByControl(normalizedVal);
}
}
}
}
</script>

View File

@@ -71,31 +71,30 @@ onUnmounted(() => {
if (copyFeedbackTimer.value) clearTimeout(copyFeedbackTimer.value);
});
// 渲染后的 HTML
const renderedHtml = computed(() => {
// 强制依赖 locale确保语言切换时重新渲染
const _ = locale?.value;
if (!content.value) return "";
md.renderer.rules.fence = (tokens, idx) => {
const token = tokens[idx];
const lang = token.info.trim() || "";
const code = token.content;
// 设置 fence 规则,直接使用当前作用域的 t 函数
md.renderer.rules.fence = (tokens, idx) => {
const token = tokens[idx];
const lang = token.info.trim() || "";
const code = token.content;
const highlighted =
lang && hljs.getLanguage(lang)
? hljs.highlight(code, { language: lang }).value
: md.utils.escapeHtml(code);
const highlighted =
lang && hljs.getLanguage(lang)
? hljs.highlight(code, { language: lang }).value
: md.utils.escapeHtml(code);
return `<div class="code-block-wrapper">
return `<div class="code-block-wrapper">
${lang ? `<span class="code-lang-label">${lang}</span>` : ""}
<button class="copy-code-btn" title="${t("core.common.copy")}">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
</button>
<pre class="hljs"><code class="language-${lang}">${highlighted}</code></pre>
</div>`;
};
};
// 渲染后的 HTML
const renderedHtml = computed(() => {
// 强制依赖 locale确保语言切换时重新渲染
const _ = locale?.value;
if (!content.value) return "";
const rawHtml = md.render(content.value);
@@ -365,16 +364,29 @@ const showActionArea = computed(() => {
</script>
<template>
<v-dialog v-model="_show" width="800">
<v-dialog
v-model="_show"
width="800"
>
<v-card>
<v-card-title class="d-flex justify-space-between align-center">
<span class="text-h2 pa-2">{{ modeConfig.title }}</span>
<v-btn icon @click="_show = false" variant="text">
<v-btn
icon
variant="text"
@click="_show = false"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text ref="scrollContainer" style="overflow-y: auto">
<div v-if="showActionArea" class="d-flex justify-space-between mb-4">
<v-card-text
ref="scrollContainer"
style="overflow-y: auto"
>
<div
v-if="showActionArea"
class="d-flex justify-space-between mb-4"
>
<v-btn
v-if="modeConfig.showGithubButton && repoUrl"
color="primary"
@@ -403,25 +415,31 @@ const showActionArea = computed(() => {
color="primary"
size="64"
class="mb-4"
></v-progress-circular>
<p class="text-body-1 text-center">{{ modeConfig.loading }}</p>
/>
<p class="text-body-1 text-center">
{{ modeConfig.loading }}
</p>
</div>
<div
v-else-if="renderedHtml"
class="markdown-body"
v-html="renderedHtml"
@click="handleContainerClick"
></div>
v-html="renderedHtml"
/>
<div
v-else-if="error"
class="d-flex flex-column align-center justify-center"
style="height: 100%"
>
<v-icon size="64" color="error" class="mb-4"
>mdi-alert-circle-outline</v-icon
<v-icon
size="64"
color="error"
class="mb-4"
>
mdi-alert-circle-outline
</v-icon>
<p class="text-body-1 text-center mb-2">
{{ t("core.common.error") }}
</p>
@@ -435,9 +453,13 @@ const showActionArea = computed(() => {
class="d-flex flex-column align-center justify-center"
style="height: 100%"
>
<v-icon size="64" color="warning" class="mb-4"
>mdi-file-question-outline</v-icon
<v-icon
size="64"
color="warning"
class="mb-4"
>
mdi-file-question-outline
</v-icon>
<p class="text-body-1 text-center mb-2">
{{ modeConfig.emptyTitle }}
</p>
@@ -447,8 +469,12 @@ const showActionArea = computed(() => {
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" variant="tonal" @click="_show = false">
<v-spacer />
<v-btn
color="primary"
variant="tonal"
@click="_show = false"
>
{{ t("core.common.close") }}
</v-btn>
</v-card-actions>

View File

@@ -4,13 +4,16 @@
color="primary"
variant="outlined"
size="small"
@click="openDialog"
style="margin-bottom: 8px;"
@click="openDialog"
>
{{ t('features.settings.sidebar.customize.title') }}
</v-btn>
<v-dialog v-model="dialog" max-width="700px">
<v-dialog
v-model="dialog"
max-width="700px"
>
<v-card>
<v-card-title class="d-flex justify-space-between align-center">
<span>{{ t('features.settings.sidebar.customize.title') }}</span>
@@ -18,15 +21,22 @@
icon="mdi-close"
variant="text"
@click="dialog = false"
></v-btn>
/>
</v-card-title>
<v-card-text>
<p class="text-body-2 mb-4">{{ t('features.settings.sidebar.customize.subtitle') }}</p>
<p class="text-body-2 mb-4">
{{ t('features.settings.sidebar.customize.subtitle') }}
</p>
<v-row>
<v-col cols="12" md="6">
<div class="mb-2 font-weight-medium">{{ t('features.settings.sidebar.customize.mainItems') }}</div>
<v-col
cols="12"
md="6"
>
<div class="mb-2 font-weight-medium">
{{ t('features.settings.sidebar.customize.mainItems') }}
</div>
<v-list
density="compact"
class="custom-list"
@@ -42,24 +52,33 @@
@dragover.prevent
@drop.stop="handleDrop($event, 'main', index)"
>
<template v-slot:prepend>
<v-icon :icon="item.icon" size="small" class="mr-2"></v-icon>
<template #prepend>
<v-icon
:icon="item.icon"
size="small"
class="mr-2"
/>
</template>
<v-list-item-title>{{ t(item.title) }}</v-list-item-title>
<template v-slot:append>
<template #append>
<v-btn
icon="mdi-arrow-right"
variant="text"
size="x-small"
@click="moveToMore(index)"
></v-btn>
/>
</template>
</v-list-item>
</v-list>
</v-col>
<v-col cols="12" md="6">
<div class="mb-2 font-weight-medium">{{ t('features.settings.sidebar.customize.moreItems') }}</div>
<v-col
cols="12"
md="6"
>
<div class="mb-2 font-weight-medium">
{{ t('features.settings.sidebar.customize.moreItems') }}
</div>
<v-list
density="compact"
class="custom-list"
@@ -75,17 +94,21 @@
@dragover.prevent
@drop.stop="handleDrop($event, 'more', index)"
>
<template v-slot:prepend>
<v-icon :icon="item.icon" size="small" class="mr-2"></v-icon>
<template #prepend>
<v-icon
:icon="item.icon"
size="small"
class="mr-2"
/>
</template>
<v-list-item-title>{{ t(item.title) }}</v-list-item-title>
<template v-slot:append>
<template #append>
<v-btn
icon="mdi-arrow-left"
variant="text"
size="x-small"
@click="moveToMain(index)"
></v-btn>
/>
</template>
</v-list-item>
</v-list>
@@ -101,7 +124,7 @@
>
{{ t('features.settings.sidebar.customize.reset') }}
</v-btn>
<v-spacer></v-spacer>
<v-spacer />
<v-btn
color="primary"
@click="saveCustomization"

View File

@@ -1,12 +1,25 @@
<template>
<v-menu v-bind="$attrs" :close-on-content-click="closeOnContentClick">
<template v-slot:activator="{ props: activatorProps }">
<slot name="activator" :props="activatorProps"></slot>
<v-menu
v-bind="$attrs"
:close-on-content-click="closeOnContentClick"
>
<template #activator="{ props: activatorProps }">
<slot
name="activator"
:props="activatorProps"
/>
</template>
<v-card class="styled-menu-card" elevation="8" rounded="lg">
<v-list density="compact" class="styled-menu-list pa-1">
<slot></slot>
<v-card
class="styled-menu-card"
elevation="8"
rounded="lg"
>
<v-list
density="compact"
class="styled-menu-list pa-1"
>
<slot />
</v-list>
</v-card>
</v-menu>

View File

@@ -1,6 +1,11 @@
<template>
<v-dialog v-model="dialog" max-width="1400px" persistent scrollable>
<template v-slot:activator="{ props }">
<v-dialog
v-model="dialog"
max-width="1400px"
persistent
scrollable
>
<template #activator="{ props }">
<v-btn
v-bind="props"
variant="outlined"
@@ -15,8 +20,11 @@
<v-card>
<v-card-title class="d-flex align-center justify-space-between">
<span>{{ tm('t2iTemplateEditor.dialogTitle') }}</span>
<v-spacer></v-spacer>
<div class="d-flex align-center gap-2" style="width: 60%">
<v-spacer />
<div
class="d-flex align-center gap-2"
style="width: 60%"
>
<v-text-field
v-if="isCreatingNew"
v-model="editingName"
@@ -27,7 +35,7 @@
class="flex-grow-1"
autofocus
:rules="[v => !!v || tm('t2iTemplateEditor.nameRequired')]"
></v-text-field>
/>
<v-select
v-else
v-model="selectedTemplate"
@@ -41,9 +49,12 @@
class="flex-grow-1"
:loading="loading"
>
<template v-slot:item="{ props, item }">
<v-list-item v-bind="props" :title="item.raw.name">
<template v-slot:append>
<template #item="{ props, item }">
<v-list-item
v-bind="props"
:title="item.raw.name"
>
<template #append>
<v-chip
v-if="item.raw.name === activeTemplate"
color="success"
@@ -59,8 +70,8 @@
color="primary"
size="small"
class="ml-2"
@click.stop="setActiveTemplate(item.raw.name)"
:loading="applyLoading"
@click.stop="setActiveTemplate(item.raw.name)"
>
{{ tm('t2iTemplateEditor.apply') }}
</v-btn>
@@ -79,55 +90,80 @@
</v-card-title>
<v-card-text class="pa-0">
<v-row no-gutters style="height: 70vh;">
<v-row
no-gutters
style="height: 70vh;"
>
<!-- 左侧编辑器 -->
<v-col cols="6" class="d-flex flex-column">
<v-toolbar density="compact" color="surface-variant">
<v-toolbar-title class="text-subtitle-2">{{ tm('t2iTemplateEditor.templateEditor') }}</v-toolbar-title>
<v-spacer></v-spacer>
<div class="d-flex align-center pa-1" style="border: 1px solid rgba(0,0,0,0.1); border-radius: 8px;">
<v-col
cols="6"
class="d-flex flex-column"
>
<v-toolbar
density="compact"
color="surface-variant"
>
<v-toolbar-title class="text-subtitle-2">
{{ tm('t2iTemplateEditor.templateEditor') }}
</v-toolbar-title>
<v-spacer />
<div
class="d-flex align-center pa-1"
style="border: 1px solid rgba(0,0,0,0.1); border-radius: 8px;"
>
<v-btn
variant="text"
size="small"
@click="newTemplate"
color="success"
@click="newTemplate"
>
<v-icon left>mdi-plus</v-icon>
<v-icon left>
mdi-plus
</v-icon>
{{ tm('t2iTemplateEditor.new') }}
</v-btn>
<v-divider vertical class="mx-1"></v-divider>
<v-divider
vertical
class="mx-1"
/>
<v-btn
variant="text"
size="small"
@click="resetToDefault"
:loading="resetLoading"
color="warning"
@click="resetToDefault"
>
{{ tm('t2iTemplateEditor.resetBase') }}
</v-btn>
<v-btn
variant="text"
size="small"
@click="promptDelete"
color="error"
:disabled="isCreatingNew || selectedTemplate === 'base' || !selectedTemplate"
@click="promptDelete"
>
{{ tm('t2iTemplateEditor.delete') }}
</v-btn>
<v-divider vertical class="mx-1"></v-divider>
<v-divider
vertical
class="mx-1"
/>
<v-btn
variant="text"
size="small"
@click="saveTemplate"
:loading="saveLoading"
color="primary"
:disabled="(isCreatingNew && !editingName) || (!isCreatingNew && !selectedTemplate)"
@click="saveTemplate"
>
{{ tm('t2iTemplateEditor.save') }}
</v-btn>
</div>
</v-toolbar>
<div class="flex-grow-1" style="border-right: 1px solid rgba(0,0,0,0.1);">
<div
class="flex-grow-1"
style="border-right: 1px solid rgba(0,0,0,0.1);"
>
<VueMonacoEditor
v-model:value="templateContent"
:theme="editorTheme"
@@ -139,15 +175,23 @@
</v-col>
<!-- 右侧预览 -->
<v-col cols="6" class="d-flex flex-column">
<v-toolbar density="compact" color="surface-variant">
<v-toolbar-title class="text-subtitle-2">{{ tm('t2iTemplateEditor.livePreview') }}</v-toolbar-title>
<v-spacer></v-spacer>
<v-col
cols="6"
class="d-flex flex-column"
>
<v-toolbar
density="compact"
color="surface-variant"
>
<v-toolbar-title class="text-subtitle-2">
{{ tm('t2iTemplateEditor.livePreview') }}
</v-toolbar-title>
<v-spacer />
<v-btn
variant="text"
size="small"
@click="refreshPreview"
:loading="previewLoading"
@click="refreshPreview"
>
{{ tm('t2iTemplateEditor.refreshPreview') }}
</v-btn>
@@ -164,10 +208,18 @@
</v-card-text>
<v-card-actions class="px-6 py-4">
<v-row no-gutters class="align-center">
<v-row
no-gutters
class="align-center"
>
<v-col>
<div class="text-caption text-grey">
<v-icon size="16" class="mr-1">mdi-information</v-icon>
<v-icon
size="16"
class="mr-1"
>
mdi-information
</v-icon>
{{ tm('t2iTemplateEditor.syntaxHint') }}
</div>
</v-col>
@@ -180,9 +232,9 @@
</v-btn>
<v-btn
color="primary"
@click="promptApplyAndClose"
:loading="saveLoading"
:disabled="isCreatingNew || !selectedTemplate"
@click="promptApplyAndClose"
>
{{ tm('t2iTemplateEditor.saveAndApply') }}
</v-btn>
@@ -192,50 +244,91 @@
</v-card>
<!-- 确认重置对话框 -->
<v-dialog v-model="resetDialog" max-width="400px">
<v-dialog
v-model="resetDialog"
max-width="400px"
>
<v-card>
<v-card-title>{{ tm('t2iTemplateEditor.confirmReset') }}</v-card-title>
<v-card-text>
{{ tm('t2iTemplateEditor.confirmResetMessage') }}
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click="resetDialog = false">{{ t('core.common.cancel') }}</v-btn>
<v-btn color="warning" @click="confirmReset" :loading="resetLoading">{{ tm('t2iTemplateEditor.confirmResetButton') }}</v-btn>
<v-spacer />
<v-btn
text
@click="resetDialog = false"
>
{{ t('core.common.cancel') }}
</v-btn>
<v-btn
color="warning"
:loading="resetLoading"
@click="confirmReset"
>
{{ tm('t2iTemplateEditor.confirmResetButton') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 删除确认对话框 -->
<v-dialog v-model="deleteDialog" max-width="400px">
<v-dialog
v-model="deleteDialog"
max-width="400px"
>
<v-card>
<v-card-title>{{ tm('t2iTemplateEditor.confirmDelete') }}</v-card-title>
<v-card-text>
{{ tm('t2iTemplateEditor.confirmDeleteMessage', { name: selectedTemplate }) }}
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click="deleteDialog = false">{{ t('core.common.cancel') }}</v-btn>
<v-btn color="error" @click="confirmDelete" :loading="saveLoading">{{ tm('t2iTemplateEditor.confirmDeleteButton') }}</v-btn>
<v-spacer />
<v-btn
text
@click="deleteDialog = false"
>
{{ t('core.common.cancel') }}
</v-btn>
<v-btn
color="error"
:loading="saveLoading"
@click="confirmDelete"
>
{{ tm('t2iTemplateEditor.confirmDeleteButton') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 保存并应用确认对话框 -->
<v-dialog v-model="applyAndCloseDialog" max-width="500px">
<v-dialog
v-model="applyAndCloseDialog"
max-width="500px"
>
<v-card>
<v-card-title>{{ tm('t2iTemplateEditor.confirmAction') }}</v-card-title>
<v-card-text>
{{ tm('t2iTemplateEditor.confirmApplyMessage', { name: selectedTemplate }) }}
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click="applyAndCloseDialog = false">{{ t('core.common.cancel') }}</v-btn>
<v-btn color="primary" @click="confirmApplyAndClose" :loading="saveLoading">{{ t('core.common.confirm') }}</v-btn>
<v-spacer />
<v-btn
text
@click="applyAndCloseDialog = false"
>
{{ t('core.common.cancel') }}
</v-btn>
<v-btn
color="primary"
:loading="saveLoading"
@click="confirmApplyAndClose"
>
{{ t('core.common.confirm') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-dialog>
</template>

View File

@@ -20,7 +20,9 @@
@click="addEntry(option.value)"
>
<v-list-item-title>{{ translateIfKey(option.label) }}</v-list-item-title>
<v-list-item-subtitle v-if="option.hint">{{ translateIfKey(option.hint) }}</v-list-item-subtitle>
<v-list-item-subtitle v-if="option.hint">
{{ translateIfKey(option.hint) }}
</v-list-item-subtitle>
</v-list-item>
</v-list>
</v-menu>
@@ -56,25 +58,54 @@
<v-icon>{{ expandedEntries[entryIndex] ? 'mdi-chevron-down' : 'mdi-chevron-right' }}</v-icon>
</v-btn>
<div class="d-flex flex-column">
<v-list-item-title class="property-name">{{ templateLabel(entry.__template_key) }}</v-list-item-title>
<v-list-item-subtitle class="property-hint" v-if="getTemplate(entry)?.hint || getTemplate(entry)?.description">
<v-list-item-title class="property-name">
{{ templateLabel(entry.__template_key) }}
</v-list-item-title>
<v-list-item-subtitle
v-if="getTemplate(entry)?.hint || getTemplate(entry)?.description"
class="property-hint"
>
{{ translateIfKey(getTemplate(entry)?.hint || getTemplate(entry)?.description) }}
</v-list-item-subtitle>
</div>
</div>
<div class="d-flex align-center ga-1">
<v-btn icon size="small" variant="text" color="error" @click.stop="removeEntry(entryIndex)">
<v-btn
icon
size="small"
variant="text"
color="error"
@click.stop="removeEntry(entryIndex)"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</div>
</v-card-title>
<v-expand-transition>
<v-card-text v-show="expandedEntries[entryIndex]" class="px-0 py-1">
<div v-if="!getTemplate(entry)" class="px-4 py-2">
<v-alert type="error" variant="tonal" density="compact">{{ t('core.common.templateList.missingTemplate') || '找不到对应模板,请删除后重新添加。' }}</v-alert>
<v-card-text
v-show="expandedEntries[entryIndex]"
class="px-0 py-1"
>
<div
v-if="!getTemplate(entry)"
class="px-4 py-2"
>
<v-alert
type="error"
variant="tonal"
density="compact"
>
{{ t('core.common.templateList.missingTemplate') || '找不到对应模板,请删除后重新添加。' }}
</v-alert>
</div>
<div v-else class="template-entry-body">
<template v-for="(itemMeta, itemKey, metaIndex) in getTemplate(entry).items" :key="itemKey">
<div
v-else
class="template-entry-body"
>
<template
v-for="(itemMeta, itemKey, metaIndex) in getTemplate(entry).items"
:key="itemKey"
>
<!-- Nested Object -->
<div
v-if="itemMeta?.type === 'object' && !itemMeta?.invisible && shouldShowItem(itemMeta, entry)"
@@ -84,14 +115,24 @@
<v-list-item-title class="config-title">
{{ translateIfKey(itemMeta?.description) || itemKey }}
</v-list-item-title>
<v-list-item-subtitle class="config-hint" v-if="itemMeta?.hint">
<v-list-item-subtitle
v-if="itemMeta?.hint"
class="config-hint"
>
{{ translateIfKey(itemMeta.hint) }}
</v-list-item-subtitle>
</div>
<div v-for="(childMeta, childKey, childIndex) in itemMeta.items" :key="childKey">
<div
v-for="(childMeta, childKey, childIndex) in itemMeta.items"
:key="childKey"
>
<template v-if="!childMeta?.invisible && shouldShowItem(childMeta, entry)">
<v-row class="config-row">
<v-col cols="12" sm="6" class="property-info">
<v-col
cols="12"
sm="6"
class="property-info"
>
<v-list-item density="compact">
<v-list-item-title class="property-name">
{{ translateIfKey(childMeta?.description) || childKey }}
@@ -101,7 +142,11 @@
</v-list-item-subtitle>
</v-list-item>
</v-col>
<v-col cols="12" sm="6" class="config-input">
<v-col
cols="12"
sm="6"
class="config-input"
>
<ConfigItemRenderer
v-model="entry[itemKey][childKey]"
:item-meta="childMeta"
@@ -111,7 +156,7 @@
<v-divider
v-if="hasVisibleItemsAfter(Object.entries(itemMeta.items), childIndex, entry)"
class="config-divider"
></v-divider>
/>
</template>
</div>
</div>
@@ -119,7 +164,11 @@
<!-- Regular Property -->
<template v-else-if="!itemMeta?.invisible && shouldShowItem(itemMeta, entry)">
<v-row class="config-row">
<v-col cols="12" sm="6" class="property-info">
<v-col
cols="12"
sm="6"
class="property-info"
>
<v-list-item density="compact">
<v-list-item-title class="property-name">
<span v-if="itemMeta?.description">{{ translateIfKey(itemMeta?.description) }} <span class="property-key">({{ itemKey }})</span></span>
@@ -130,7 +179,11 @@
</v-list-item-subtitle>
</v-list-item>
</v-col>
<v-col cols="12" sm="6" class="config-input">
<v-col
cols="12"
sm="6"
class="config-input"
>
<ConfigItemRenderer
v-model="entry[itemKey]"
:item-meta="itemMeta"
@@ -140,7 +193,7 @@
<v-divider
v-if="hasVisibleItemsAfter(Object.entries(getTemplate(entry).items), metaIndex, entry)"
class="config-divider"
></v-divider>
/>
</template>
</template>
</div>

View File

@@ -5,27 +5,52 @@ import { EventSourcePolyfill } from 'event-source-polyfill';
<template>
<div class="trace-wrapper">
<div class="trace-table" ref="scrollEl" :style="{ height: tableHeight }">
<div
ref="scrollEl"
class="trace-table"
:style="{ height: tableHeight }"
>
<div class="trace-row trace-header">
<div class="trace-cell time">Time</div>
<div class="trace-cell span">Event ID</div>
<div class="trace-cell umo">UMO</div>
<div class="trace-cell time">
Time
</div>
<div class="trace-cell span">
Event ID
</div>
<div class="trace-cell umo">
UMO
</div>
<!-- <div class="trace-cell count">Records</div> -->
<!-- <div class="trace-cell last">Last</div> -->
<div class="trace-cell sender">Sender</div>
<div class="trace-cell outline">Outline</div>
<div class="trace-cell fields"></div>
<div class="trace-cell sender">
Sender
</div>
<div class="trace-cell outline">
Outline
</div>
<div class="trace-cell fields" />
</div>
<div class="trace-group" :class="{ highlight: highlightMap[event.span_id] }" v-for="event in events"
:key="event.span_id">
<div
v-for="event in events"
:key="event.span_id"
class="trace-group"
:class="{ highlight: highlightMap[event.span_id] }"
>
<div class="trace-row trace-event">
<div class="trace-cell time">{{ formatTime(event.first_time) }}</div>
<div class="trace-cell span" :title="event.span_id">
<div class="trace-cell time">
{{ formatTime(event.first_time) }}
</div>
<div
class="trace-cell span"
:title="event.span_id"
>
<div class="event-title">
{{ shortSpan(event.span_id) }}
</div>
</div>
<div class="trace-cell umo">{{ event.umo }}</div>
<div class="trace-cell umo">
{{ event.umo }}
</div>
<!-- <div class="trace-cell count">
<div class="event-meta">{{ event.records.length }}</div>
</div> -->
@@ -33,33 +58,72 @@ import { EventSourcePolyfill } from 'event-source-polyfill';
<div class="event-meta">{{ formatTime(event.last_time) }}</div>
</div> -->
<div class="trace-cell sender">
<div class="event-sub" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">{{
event.sender_name || '-' }}</div>
<div
class="event-sub"
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"
>
{{
event.sender_name || '-' }}
</div>
</div>
<div class="trace-cell outline">
<div class="event-sub outline">{{ event.message_outline || '-' }}</div>
<div class="event-sub outline">
{{ event.message_outline || '-' }}
</div>
</div>
<div class="trace-cell fields event-controls">
<v-btn size="x-small" variant="text" color="primary" @click="toggleEvent(event.span_id)">
<v-btn
size="x-small"
variant="text"
color="primary"
@click="toggleEvent(event.span_id)"
>
{{ event.collapsed ? 'Expand' : 'Collapse' }}
<span v-if="event.hasAgentPrepare" class="agent-dot" />
<span
v-if="event.hasAgentPrepare"
class="agent-dot"
/>
</v-btn>
</div>
</div>
<div class="trace-records" v-if="!event.collapsed">
<div class="trace-record" v-for="record in getVisibleRecords(event)" :key="record.key">
<div class="trace-record-time">{{ record.timeLabel }}</div>
<div class="trace-record-action">{{ record.action }}</div>
<div
v-if="!event.collapsed"
class="trace-records"
>
<div
v-for="record in getVisibleRecords(event)"
:key="record.key"
class="trace-record"
>
<div class="trace-record-time">
{{ record.timeLabel }}
</div>
<div class="trace-record-action">
{{ record.action }}
</div>
<pre class="trace-record-fields">{{ record.fieldsText }}</pre>
</div>
<div class="event-more" v-if="event.visibleCount < event.records.length">
<v-btn size="x-small" variant="tonal" color="primary" @click="showMore(event.span_id)">
<div
v-if="event.visibleCount < event.records.length"
class="event-more"
>
<v-btn
size="x-small"
variant="tonal"
color="primary"
@click="showMore(event.span_id)"
>
Show more
</v-btn>
</div>
</div>
</div>
<div v-if="events.length === 0" class="trace-empty">No trace data yet.</div>
<div
v-if="events.length === 0"
class="trace-empty"
>
No trace data yet.
</div>
</div>
</div>
</template>

View File

@@ -15,9 +15,11 @@
{{ tm('dialogs.uninstall.message') }}
</div>
<v-divider class="my-4"></v-divider>
<v-divider class="my-4" />
<div class="text-subtitle-2 mb-3">{{ t('core.common.actions') }}:</div>
<div class="text-subtitle-2 mb-3">
{{ t('core.common.actions') }}:
</div>
<v-checkbox
v-model="deleteConfig"
@@ -26,10 +28,16 @@
hide-details
class="mb-2"
>
<template v-slot:append>
<template #append>
<v-tooltip location="top">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" size="small" color="grey">mdi-information-outline</v-icon>
<template #activator="{ props }">
<v-icon
v-bind="props"
size="small"
color="grey"
>
mdi-information-outline
</v-icon>
</template>
<span>{{ tm('dialogs.uninstall.configHint') }}</span>
</v-tooltip>
@@ -42,10 +50,16 @@
color="error"
hide-details
>
<template v-slot:append>
<template #append>
<v-tooltip location="top">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" size="small" color="grey">mdi-information-outline</v-icon>
<template #activator="{ props }">
<v-icon
v-bind="props"
size="small"
color="grey"
>
mdi-information-outline
</v-icon>
</template>
<span>{{ tm('dialogs.uninstall.dataHint') }}</span>
</v-tooltip>
@@ -59,7 +73,7 @@
density="compact"
class="mt-4"
>
<template v-slot:prepend>
<template #prepend>
<v-icon>mdi-alert</v-icon>
</template>
{{ t('messages.validation.operation_cannot_be_undone') }}
@@ -67,7 +81,7 @@
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-spacer />
<v-btn
color="grey"
variant="text"

View File

@@ -1,12 +1,19 @@
<template>
<v-dialog v-model="visible" persistent max-width="400">
<v-card>
<v-card-title>{{ t('core.common.restart.waiting') }}</v-card-title>
<v-card-text>
<v-progress-linear indeterminate color="primary"></v-progress-linear>
</v-card-text>
</v-card>
</v-dialog>
<v-dialog
v-model="visible"
persistent
max-width="400"
>
<v-card>
<v-card-title>{{ t('core.common.restart.waiting') }}</v-card-title>
<v-card-text>
<v-progress-linear
indeterminate
color="primary"
/>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script>

View File

@@ -8,7 +8,7 @@ export interface Conversation {
updated_at: number;
}
export function useConversations(chatboxMode: boolean = false) {
export function useConversations(chatboxMode = false) {
const router = useRouter();
const conversations = ref<Conversation[]>([]);
const selectedConversations = ref<string[]>([]);

View File

@@ -628,7 +628,7 @@ export function useMessages(
// 如果 message 是字符串 (旧格式),转换为数组格式
if (typeof message === 'string') {
const parts: MessagePart[] = [];
let text = message;
const text = message;
// 处理旧格式的特殊标记
if (text.startsWith('[IMAGE]')) {
@@ -695,7 +695,7 @@ export function useMessages(
const response = await axios.get('/api/chat/get_session?session_id=' + sessionId);
isConvRunning.value = response.data.data.is_running || false;
let history = response.data.data.history;
const history = response.data.data.history;
// 保存项目信息(如果存在)
currentSessionProject.value = response.data.data.project || null;
@@ -714,7 +714,7 @@ export function useMessages(
// 处理历史消息
for (let i = 0; i < history.length; i++) {
let content = history[i].content;
const content = history[i].content;
await parseMessageContent(content);
}
@@ -750,7 +750,7 @@ export function useMessages(
const partType = f.type === 'image' ? 'image' :
f.type === 'record' ? 'record' : 'file';
parts.push({
type: partType as 'image' | 'record' | 'file',
type: partType ,
attachment_id: f.attachment_id
});
}
@@ -793,7 +793,7 @@ export function useMessages(
isStreaming.value = true;
while (true) {
for (;;) {
try {
const { done, value } = await reader.read();
if (done) {
@@ -807,7 +807,7 @@ export function useMessages(
const lines = chunk.split('\n\n');
for (let i = 0; i < lines.length; i++) {
let line = lines[i].trim();
const line = lines[i].trim();
if (!line) continue;
let chunkJson: StreamChunk;
@@ -917,7 +917,7 @@ export function useMessages(
const embeddedUrl = await getAttachment(f.attachment_id);
userMessageParts.push({
type: partType as 'image' | 'record' | 'file',
type: partType ,
attachment_id: f.attachment_id,
filename: f.original_name,
embedded_url: partType !== 'file' ? embeddedUrl : undefined,

View File

@@ -13,7 +13,7 @@ export interface Session {
created_at: string;
}
export function useSessions(chatboxMode: boolean = false) {
export function useSessions(chatboxMode = false) {
const router = useRouter();
const sessions = ref<Session[]>([]);
const selectedSessions = ref<string[]>([]);

View File

@@ -10,7 +10,7 @@ export function generateMissingKeys(
): string[] {
const missing: string[] = [];
function traverse(source: any, target: any, path: string = '') {
function traverse(source: any, target: any, path = '') {
for (const key in source) {
const currentPath = path ? `${path}.${key}` : key;

View File

@@ -6,7 +6,7 @@
import type { ValidationResult, ValidationError, UsageReport, TranslationStats } from './types';
export class I18nValidator {
private baseLocale: string = 'zh-CN';
private baseLocale = 'zh-CN';
private supportedLocales: string[] = ['zh-CN', 'en-US'];
/**
@@ -232,7 +232,7 @@ export class I18nValidator {
/**
* 获取对象的所有键路径
*/
private getAllKeys(obj: any, prefix: string = ''): string[] {
private getAllKeys(obj: any, prefix = ''): string[] {
const keys: string[] = [];
for (const [key, value] of Object.entries(obj)) {

View File

@@ -96,7 +96,8 @@ onMounted(() => {
<template>
<v-locale-provider>
<v-app :theme="useCustomizerStore().uiTheme"
<v-app
:theme="useCustomizerStore().uiTheme"
:class="[customizer.fontTheme, customizer.mini_sidebar ? 'mini-sidebar' : '', customizer.inputBg ? 'inputWithbg' : '']"
>
<v-progress-linear
@@ -110,10 +111,12 @@ onMounted(() => {
/>
<VerticalHeaderVue />
<VerticalSidebarVue v-if="showSidebar" />
<v-main :style="{
height: showChatPage ? 'calc(100vh - 55px)' : undefined,
overflow: showChatPage ? 'hidden' : undefined
}">
<v-main
:style="{
height: showChatPage ? 'calc(100vh - 55px)' : undefined,
overflow: showChatPage ? 'hidden' : undefined
}"
>
<v-container
fluid
class="page-wrapper"
@@ -122,9 +125,13 @@ onMounted(() => {
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;">
<div
v-if="showChatPage"
style="height: 100%; width: 100%; overflow: hidden;"
>
<Chat />
</div>
<RouterView v-else />

View File

@@ -76,7 +76,7 @@ const desktopUpdateCurrentVersion = ref("-");
const desktopUpdateLatestVersion = ref("-");
const desktopUpdateStatus = ref("");
const getAppUpdaterBridge = (): AstrBotAppUpdaterBridge | null => {
const getAppUpdaterBridge = (): NonNullable<Window["astrbotAppUpdater"]> | null => {
if (typeof window === "undefined") {
return null;
}
@@ -533,7 +533,13 @@ const isChristmas = computed(() => {
</script>
<template>
<v-app-bar elevation="0" :priority="0" height="70" class="px-0" app>
<v-app-bar
elevation="0"
:priority="0"
height="70"
class="px-0"
app
>
<div class="fill-height d-flex align-center w-100 px-4">
<!-- 桌面端标题栏拖拽区域 -->
<div
@@ -547,7 +553,7 @@ const isChristmas = computed(() => {
height: 30px;
z-index: 9999;
"
></div>
/>
<div class="d-flex align-center">
<!-- 桌面端 menu 按钮 - 仅在 bot 模式下显示 -->
@@ -582,22 +588,18 @@ const isChristmas = computed(() => {
}"
@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 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
v-if="customizer.viewMode === 'chat'"
class="logo-text logo-text-light Outfit"
style="color: grey"
v-if="customizer.viewMode === 'chat'"
>ChatUI</span
>
>ChatUI</span>
<span class="version-text hidden-xs">{{ botCurrVersion }}</span>
</div>
@@ -612,10 +614,18 @@ const isChristmas = computed(() => {
group
density="compact"
>
<v-btn value="chat" prepend-icon="mdi-chat-processing-outline">
<v-btn
value="chat"
prepend-icon="mdi-chat-processing-outline"
>
Chat
</v-btn>
<v-btn value="bot" prepend-icon="mdi-robot-outline"> Bot </v-btn>
<v-btn
value="bot"
prepend-icon="mdi-robot-outline"
>
Bot
</v-btn>
</v-btn-toggle>
</div>
@@ -633,8 +643,11 @@ const isChristmas = computed(() => {
</div>
<!-- 功能菜单 -->
<StyledMenu offset="12" location="bottom end">
<template v-slot:activator="{ props: activatorProps }">
<StyledMenu
offset="12"
location="bottom end"
>
<template #activator="{ props: activatorProps }">
<v-btn
v-bind="activatorProps"
size="small"
@@ -659,12 +672,22 @@ const isChristmas = computed(() => {
color="primary"
class="mobile-mode-toggle"
>
<v-btn value="bot" size="small">
<v-icon start>mdi-robot</v-icon>
<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>
<v-btn
value="chat"
size="small"
>
<v-icon start>
mdi-chat
</v-icon>
Chat
</v-btn>
</v-btn-toggle>
@@ -681,25 +704,30 @@ const isChristmas = computed(() => {
:location="$vuetify.display.xs ? 'bottom' : 'start center'"
offset="8"
>
<template v-slot:activator="{ props: languageMenuProps }">
<template #activator="{ props: languageMenuProps }">
<v-list-item
v-bind="languageMenuProps"
class="styled-menu-item language-group-trigger"
rounded="md"
>
<template v-slot:prepend>
<template #prepend>
<v-icon>mdi-translate</v-icon>
</template>
<v-list-item-title>{{
t("core.common.language")
}}</v-list-item-title>
<template v-slot:append>
<v-list-item-title>
{{
t("core.common.language")
}}
</v-list-item-title>
<template #append>
<span class="language-group-current">{{
currentLanguage?.flag
}}</span>
<v-icon size="18" class="language-group-arrow"
>mdi-chevron-right</v-icon
<v-icon
size="18"
class="language-group-arrow"
>
mdi-chevron-right
</v-icon>
</template>
</v-list-item>
</template>
@@ -710,19 +738,22 @@ const isChristmas = computed(() => {
elevation="8"
rounded="lg"
>
<v-list density="compact" class="styled-menu-list pa-1">
<v-list
density="compact"
class="styled-menu-list pa-1"
>
<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"
@click="changeLanguage(lang.code)"
>
<template v-slot:prepend>
<template #prepend>
<span class="language-flag">{{ lang.flag }}</span>
</template>
<v-list-item-title>{{ lang.name }}</v-list-item-title>
@@ -733,11 +764,11 @@ const isChristmas = computed(() => {
<!-- 主题切换 -->
<v-list-item
@click="toggleTheme()"
class="styled-menu-item"
rounded="md"
@click="toggleTheme()"
>
<template v-slot:prepend>
<template #prepend>
<v-icon>
{{
useCustomizerStore().isDarkTheme
@@ -757,54 +788,65 @@ const isChristmas = computed(() => {
<!-- 更新按钮 -->
<v-list-item
@click="handleUpdateClick"
class="styled-menu-item"
rounded="md"
@click="handleUpdateClick"
>
<template v-slot:prepend>
<template #prepend>
<v-icon>mdi-arrow-up-circle</v-icon>
</template>
<v-list-item-title>{{
t("core.header.updateDialog.title")
}}</v-list-item-title>
<v-list-item-title>
{{
t("core.header.updateDialog.title")
}}
</v-list-item-title>
<template
v-slot:append
v-if="
hasNewVersion || (dashboardHasNewVersion && !isDesktopReleaseMode)
"
#append
>
<v-chip size="x-small" color="primary" variant="tonal" class="ml-2"
>!</v-chip
<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"
@click="dialog = true"
>
<template v-slot:prepend>
<template #prepend>
<v-icon>mdi-account</v-icon>
</template>
<v-list-item-title>{{
t("core.header.accountDialog.title")
}}</v-list-item-title>
<v-list-item-title>
{{
t("core.header.accountDialog.title")
}}
</v-list-item-title>
</v-list-item>
<!-- 退出登录 -->
<v-list-item
@click="handleLogout"
class="styled-menu-item"
rounded="md"
@click="handleLogout"
>
<template v-slot:prepend>
<template #prepend>
<v-icon>mdi-logout</v-icon>
</template>
<v-list-item-title>{{
t("core.header.buttons.logout")
}}</v-list-item-title>
<v-list-item-title>
{{
t("core.header.buttons.logout")
}}
</v-list-item-title>
</v-list-item>
</StyledMenu>
</div>
@@ -834,10 +876,12 @@ const isChristmas = computed(() => {
class="mb-4"
indeterminate
color="primary"
></v-progress-linear>
/>
<div>
<h1 style="display: inline-block">{{ botCurrVersion }}</h1>
<h1 style="display: inline-block">
{{ botCurrVersion }}
</h1>
<small style="margin-left: 4px">{{ updateStatus }}</small>
</div>
@@ -860,22 +904,18 @@ const isChristmas = computed(() => {
</div>
<div class="mb-4 mt-4">
<small
>{{ t("core.header.updateDialog.tip") }}
{{ t("core.header.updateDialog.tipContinue") }}</small
>
<small>{{ t("core.header.updateDialog.tip") }}
{{ t("core.header.updateDialog.tipContinue") }}</small>
</div>
<!-- 发行版 -->
<div>
<div class="mb-4">
<small
>{{ t("core.header.updateDialog.dockerTip") }}
<small>{{ t("core.header.updateDialog.dockerTip") }}
<a href="https://containrrr.dev/watchtower/usage-overview/">{{
t("core.header.updateDialog.dockerTipLink")
}}</a>
{{ t("core.header.updateDialog.dockerTipContinue") }}</small
>
{{ t("core.header.updateDialog.dockerTipContinue") }}</small>
</div>
<v-alert
@@ -886,14 +926,14 @@ const isChristmas = computed(() => {
variant="tonal"
border="start"
>
<template v-slot:prepend>
<template #prepend>
<v-icon>mdi-alert-circle-outline</v-icon>
</template>
<div class="text-body-2">
<strong>{{
t("core.header.updateDialog.preReleaseWarning.title")
}}</strong>
<br />
<br>
{{
t("core.header.updateDialog.preReleaseWarning.description")
}}
@@ -915,7 +955,7 @@ const isChristmas = computed(() => {
item-key="name"
:items-per-page="8"
>
<template v-slot:item.tag_name="{ item }: { item: any }">
<template #item.tag_name="{ item }: { item: any }">
<div class="d-flex align-center">
<span>{{ item.tag_name }}</span>
<v-chip
@@ -930,29 +970,30 @@ const isChristmas = computed(() => {
</div>
</template>
<template
v-slot:item.body="{
#item.body="{
item,
}: {
item: { body: string; tag_name: string };
}"
>
<v-btn
@click="openReleaseNotesDialog(item.body, item.tag_name)"
rounded="xl"
variant="tonal"
color="primary"
size="x-small"
>{{ t("core.header.updateDialog.table.view") }}</v-btn
@click="openReleaseNotesDialog(item.body, item.tag_name)"
>
{{ t("core.header.updateDialog.table.view") }}
</v-btn>
</template>
<template
v-slot:item.switch="{ item }: { item: { tag_name: string } }"
#item.switch="{ item }: { item: { tag_name: string } }"
>
<v-btn
@click="switchVersion(item.tag_name)"
rounded="xl"
variant="plain"
color="primary"
@click="switchVersion(item.tag_name)"
>
{{ t("core.header.updateDialog.table.switch") }}
</v-btn>
@@ -960,19 +1001,17 @@ const isChristmas = computed(() => {
</v-data-table>
</div>
<v-divider class="mt-4 mb-4"></v-divider>
<v-divider class="mt-4 mb-4" />
<div style="margin-top: 16px">
<h3 class="mb-4">
{{ t("core.header.updateDialog.dashboardUpdate.title") }}
</h3>
<div class="mb-4">
<small
>{{
t("core.header.updateDialog.dashboardUpdate.currentVersion")
}}
{{ dashboardCurrentVersion }}</small
>
<br />
<small>{{
t("core.header.updateDialog.dashboardUpdate.currentVersion")
}}
{{ dashboardCurrentVersion }}</small>
<br>
</div>
<div class="mb-4">
@@ -981,7 +1020,7 @@ const isChristmas = computed(() => {
t("core.header.updateDialog.dashboardUpdate.hasNewVersion")
}}
</p>
<p v-else="dashboardHasNewVersion">
<p v-else>
{{ t("core.header.updateDialog.dashboardUpdate.isLatest") }}
</p>
</div>
@@ -989,9 +1028,9 @@ const isChristmas = computed(() => {
<v-btn
color="primary"
style="border-radius: 10px"
@click="updateDashboard()"
:disabled="!dashboardHasNewVersion"
:loading="updatingDashboardLoading"
@click="updateDashboard()"
>
{{
t("core.header.updateDialog.dashboardUpdate.downloadAndUpdate")
@@ -1001,7 +1040,7 @@ const isChristmas = computed(() => {
</v-container>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-spacer />
<v-btn
color="blue-darken-1"
variant="text"
@@ -1014,7 +1053,10 @@ const isChristmas = computed(() => {
</v-dialog>
<!-- Release Notes Modal -->
<v-dialog v-model="releaseNotesDialog" max-width="800">
<v-dialog
v-model="releaseNotesDialog"
max-width="800"
>
<v-card>
<v-card-title class="text-h5">
{{ t("core.header.updateDialog.releaseNotes.title") }}:
@@ -1028,7 +1070,7 @@ const isChristmas = computed(() => {
/>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-spacer />
<v-btn
color="blue-darken-1"
variant="text"
@@ -1040,7 +1082,10 @@ const isChristmas = computed(() => {
</v-card>
</v-dialog>
<v-dialog v-model="desktopUpdateDialog" max-width="460">
<v-dialog
v-model="desktopUpdateDialog"
max-width="460"
>
<v-card>
<v-card-title class="text-h3 pa-4 pl-6 pb-0">
{{ t("core.header.updateDialog.desktopApp.title") }}
@@ -1049,7 +1094,11 @@ const isChristmas = computed(() => {
<div class="mb-3">
{{ t("core.header.updateDialog.desktopApp.message") }}
</div>
<v-alert type="info" variant="tonal" density="compact">
<v-alert
type="info"
variant="tonal"
density="compact"
>
<div>
{{ t("core.header.updateDialog.desktopApp.currentVersion") }}
<strong>{{ desktopUpdateCurrentVersion }}</strong>
@@ -1073,25 +1122,25 @@ const isChristmas = computed(() => {
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-spacer />
<v-btn
color="grey"
variant="text"
@click="cancelDesktopUpdate"
:disabled="desktopUpdateInstalling"
@click="cancelDesktopUpdate"
>
{{ t("core.common.dialog.cancelButton") }}
</v-btn>
<v-btn
color="primary"
variant="flat"
@click="confirmDesktopUpdate"
:loading="desktopUpdateInstalling"
:disabled="
desktopUpdateChecking ||
desktopUpdateInstalling ||
!desktopUpdateHasNewVersion
desktopUpdateInstalling ||
!desktopUpdateHasNewVersion
"
@click="confirmDesktopUpdate"
>
{{ t("core.common.dialog.confirmButton") }}
</v-btn>
@@ -1111,7 +1160,7 @@ const isChristmas = computed(() => {
<Logo
:title="t('core.header.logoTitle')"
:subtitle="t('core.header.accountDialog.title')"
></Logo>
/>
</div>
<v-alert
v-if="accountWarning"
@@ -1143,7 +1192,10 @@ const isChristmas = computed(() => {
{{ accountEditStatus.message }}
</v-alert>
<v-form v-model="formValid" @submit.prevent="accountEdit">
<v-form
v-model="formValid"
@submit.prevent="accountEdit"
>
<v-text-field
v-model="password"
:append-inner-icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
@@ -1152,11 +1204,11 @@ const isChristmas = computed(() => {
variant="outlined"
required
clearable
@click:append-inner="showPassword = !showPassword"
prepend-inner-icon="mdi-lock-outline"
hide-details="auto"
class="mb-4"
></v-text-field>
@click:append-inner="showPassword = !showPassword"
/>
<v-text-field
v-model="newPassword"
@@ -1166,12 +1218,12 @@ const isChristmas = computed(() => {
:label="t('core.header.accountDialog.form.newPassword')"
variant="outlined"
clearable
@click:append-inner="showNewPassword = !showNewPassword"
prepend-inner-icon="mdi-lock-plus-outline"
:hint="t('core.header.accountDialog.form.passwordHint')"
persistent-hint
class="mb-4"
></v-text-field>
@click:append-inner="showNewPassword = !showNewPassword"
/>
<v-text-field
v-model="confirmPassword"
@@ -1181,12 +1233,12 @@ const isChristmas = computed(() => {
:label="t('core.header.accountDialog.form.confirmPassword')"
variant="outlined"
clearable
@click:append-inner="showConfirmPassword = !showConfirmPassword"
prepend-inner-icon="mdi-lock-check-outline"
:hint="t('core.header.accountDialog.form.confirmPasswordHint')"
persistent-hint
class="mb-4"
></v-text-field>
@click:append-inner="showConfirmPassword = !showConfirmPassword"
/>
<v-text-field
v-model="newUsername"
@@ -1198,7 +1250,7 @@ const isChristmas = computed(() => {
:hint="t('core.header.accountDialog.form.usernameHint')"
persistent-hint
class="mb-3"
></v-text-field>
/>
</v-form>
<div class="text-caption text-medium-emphasis mt-2">
@@ -1206,25 +1258,25 @@ const isChristmas = computed(() => {
</div>
</v-card-text>
<v-divider></v-divider>
<v-divider />
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-spacer />
<v-btn
v-if="!accountWarning"
variant="tonal"
color="secondary"
@click="dialog = false"
:disabled="accountEditStatus.loading"
@click="dialog = false"
>
{{ t("core.header.accountDialog.actions.cancel") }}
</v-btn>
<v-btn
color="primary"
@click="accountEdit"
:loading="accountEditStatus.loading"
:disabled="!formValid"
prepend-icon="mdi-content-save"
@click="accountEdit"
>
{{ t("core.header.accountDialog.actions.save") }}
</v-btn>
@@ -1233,7 +1285,10 @@ const isChristmas = computed(() => {
</v-dialog>
<!-- About 对话框 - 仅在 chat mode 下使用 -->
<v-dialog v-model="aboutDialog" width="600">
<v-dialog
v-model="aboutDialog"
width="600"
>
<v-card>
<v-card-text style="overflow-y: auto">
<AboutPage />

View File

@@ -27,10 +27,20 @@ const isItemActive = computed(() => {
</script>
<template>
<v-list-group v-if="item.children" :value="item.title" :class="{ 'group-bordered': customizer.mini_sidebar }">
<template v-slot:activator="{ props }">
<v-list-item v-bind="props" rounded class="mb-1" color="secondary" :prepend-icon="item.icon"
:style="{ '--indent-padding': '0px' }">
<v-list-group
v-if="item.children"
:value="item.title"
:class="{ 'group-bordered': customizer.mini_sidebar }"
>
<template #activator="{ props }">
<v-list-item
v-bind="props"
rounded
class="mb-1"
color="secondary"
:prepend-icon="item.icon"
:style="{ '--indent-padding': '0px' }"
>
<v-list-item-title style="font-size: 14px; font-weight: 500; line-height: 1.2; word-break: break-word;">
{{ t(item.title) }}
</v-list-item-title>
@@ -38,24 +48,57 @@ const isItemActive = computed(() => {
</template>
<!-- children -->
<template v-for="(child, index) in item.children" :key="child.title || child.to || `child-${index}`">
<NavItem :item="child" :level="(level || 0) + 1" />
<template
v-for="(child, index) in item.children"
:key="child.title || child.to || `child-${index}`"
>
<NavItem
:item="child"
:level="(level || 0) + 1"
/>
</template>
</v-list-group>
<v-list-item v-else :to="item.type === 'external' ? '' : item.to" :href="item.type === 'external' ? item.to : ''"
:active="isItemActive" rounded class="mb-1" color="secondary" :disabled="item.disabled"
:target="item.type === 'external' ? '_blank' : ''" :style="itemStyle">
<template v-slot:prepend>
<v-icon v-if="item.icon" :size="item.iconSize" class="hide-menu" :icon="item.icon"></v-icon>
<v-list-item
v-else
:to="item.type === 'external' ? '' : item.to"
:href="item.type === 'external' ? item.to : ''"
:active="isItemActive"
rounded
class="mb-1"
color="secondary"
:disabled="item.disabled"
:target="item.type === 'external' ? '_blank' : ''"
:style="itemStyle"
>
<template #prepend>
<v-icon
v-if="item.icon"
:size="item.iconSize"
class="hide-menu"
:icon="item.icon"
/>
</template>
<v-list-item-title style="font-size: 14px;">{{ t(item.title) }}</v-list-item-title>
<v-list-item-subtitle v-if="item.subCaption" class="text-caption mt-n1 hide-menu">
<v-list-item-title style="font-size: 14px;">
{{ t(item.title) }}
</v-list-item-title>
<v-list-item-subtitle
v-if="item.subCaption"
class="text-caption mt-n1 hide-menu"
>
{{ item.subCaption }}
</v-list-item-subtitle>
<template v-slot:append v-if="item.chip">
<v-chip :color="item.chipColor" class="sidebarchip hide-menu" :size="item.chipIcon ? 'small' : 'default'"
:variant="item.chipVariant" :prepend-icon="item.chipIcon">
<template
v-if="item.chip"
#append
>
<v-chip
:color="item.chipColor"
class="sidebarchip hide-menu"
:size="item.chipIcon ? 'small' : 'default'"
:variant="item.chipVariant"
:prepend-icon="item.chipIcon"
>
{{ item.chip }}
</v-chip>
</template>

View File

@@ -278,8 +278,8 @@ function openChangelogDialog() {
<template>
<v-navigation-drawer
left
v-model="customizer.Sidebar_drawer"
left
elevation="0"
rail-width="80"
app
@@ -288,37 +288,79 @@ function openChangelogDialog() {
:rail="customizer.mini_sidebar"
>
<div class="sidebar-container">
<v-list :class="['pa-4', 'listitem', 'flex-grow-1', { 'hidden-scrollbar': customizer.mini_sidebar }]" v-model:opened="openedItems" :open-strategy="'multiple'">
<template v-for="(item, i) in sidebarMenu" :key="item.title || item.to || `sidebar-item-${i}`">
<NavItem :item="item" class="leftPadding" />
<v-list
v-model:opened="openedItems"
:class="['pa-4', 'listitem', 'flex-grow-1', { 'hidden-scrollbar': customizer.mini_sidebar }]"
:open-strategy="'multiple'"
>
<template
v-for="(item, i) in sidebarMenu"
:key="item.title || item.to || `sidebar-item-${i}`"
>
<NavItem
:item="item"
class="leftPadding"
/>
</template>
</v-list>
<div class="sidebar-footer" v-if="!customizer.mini_sidebar">
<v-btn class="sidebar-footer-btn" size="small" variant="tonal" color="primary" to="/settings" prepend-icon="mdi-cog">
<div
v-if="!customizer.mini_sidebar"
class="sidebar-footer"
>
<v-btn
class="sidebar-footer-btn"
size="small"
variant="tonal"
color="primary"
to="/settings"
prepend-icon="mdi-cog"
>
{{ t('core.navigation.settings') }}
</v-btn>
<v-btn class="sidebar-footer-btn" size="small" variant="text" prepend-icon="mdi-note-text-outline"
@click="openChangelogDialog">
<v-btn
class="sidebar-footer-btn"
size="small"
variant="text"
prepend-icon="mdi-note-text-outline"
@click="openChangelogDialog"
>
{{ t('core.navigation.changelog') }}
</v-btn>
<v-btn class="sidebar-footer-btn" size="small" variant="text" prepend-icon="mdi-book-open-variant"
@click="toggleIframe">
<v-btn
class="sidebar-footer-btn"
size="small"
variant="text"
prepend-icon="mdi-book-open-variant"
@click="toggleIframe"
>
{{ t('core.navigation.documentation') }}
</v-btn>
<v-btn class="sidebar-footer-btn" size="small" variant="text" prepend-icon="mdi-frequently-asked-questions"
@click="openFaqLink">
<v-btn
class="sidebar-footer-btn"
size="small"
variant="text"
prepend-icon="mdi-frequently-asked-questions"
@click="openFaqLink"
>
{{ t('core.navigation.faq') }}
</v-btn>
<v-btn class="sidebar-footer-btn" size="small" variant="text" prepend-icon="mdi-github"
@click="openIframeLink('https://github.com/AstrBotDevs/AstrBot')">
<v-btn
class="sidebar-footer-btn"
size="small"
variant="text"
prepend-icon="mdi-github"
@click="openIframeLink('https://github.com/AstrBotDevs/AstrBot')"
>
{{ t('core.navigation.github') }}
<v-chip
<v-chip
v-if="starCount"
size="x-small"
variant="outlined"
class="ml-2"
style="font-weight: normal;"
>{{ formatNumber(starCount) }}</v-chip>
>
{{ formatNumber(starCount) }}
</v-chip>
</v-btn>
</div>
</div>
@@ -326,10 +368,9 @@ function openChangelogDialog() {
<div
v-if="!customizer.mini_sidebar && customizer.Sidebar_drawer"
class="sidebar-resize-handle"
@mousedown="startSidebarResize"
:class="{ 'resizing': isResizing }"
>
</div>
@mousedown="startSidebarResize"
/>
</v-navigation-drawer>
<div
@@ -337,8 +378,11 @@ function openChangelogDialog() {
id="draggable-iframe"
:style="iframeStyle"
>
<div :style="dragHeaderStyle" @mousedown="onMouseDown" @touchstart="onTouchStart">
<div
:style="dragHeaderStyle"
@mousedown="onMouseDown"
@touchstart="onTouchStart"
>
<div style="display: flex; align-items: center;">
<v-icon icon="mdi-cursor-move" />
<span style="margin-left: 8px;">{{ t('core.navigation.drag') }}</span>
@@ -346,17 +390,17 @@ function openChangelogDialog() {
<div style="display: flex; gap: 8px;">
<v-btn
icon
style="border-radius: 8px; border: 1px solid #ccc;"
@click.stop="openIframeLink('https://astrbot.app')"
@mousedown.stop
style="border-radius: 8px; border: 1px solid #ccc;"
>
<v-icon icon="mdi-open-in-new" />
</v-btn>
<v-btn
icon
style="border-radius: 8px; border: 1px solid #ccc;"
@click.stop="toggleIframe"
@mousedown.stop
style="border-radius: 8px; border: 1px solid #ccc;"
>
<v-icon icon="mdi-close" />
</v-btn>
@@ -365,7 +409,7 @@ function openChangelogDialog() {
<iframe
src="https://astrbot.app"
style="width: 100%; height: calc(100% - 66px); border: none; border-bottom-left-radius: 12px; border-bottom-right-radius: 12px;"
></iframe>
/>
</div>
<!-- 更新日志对话框 -->

View File

@@ -118,7 +118,7 @@ async function initApp() {
const headers = new Headers(
init?.headers ||
(typeof input !== "string" && "headers" in input
? (input as Request).headers
? (input ).headers
: undefined),
);
if (token && !headers.has("Authorization")) {

View File

@@ -5,7 +5,6 @@ import axios from 'axios';
export const useAuthStore = defineStore({
id: 'auth',
state: () => ({
// @ts-ignore
username: '',
returnUrl: null
}),

View File

@@ -4,7 +4,6 @@ import axios from 'axios';
export const useCommonStore = defineStore({
id: 'common',
state: () => ({
// @ts-ignore
eventSource: null,
log_cache: [],
sse_connected: false,
@@ -141,7 +140,7 @@ export const useCommonStore = defineStore({
if (this.startTime !== -1) {
return this.startTime
}
this.fetchStartTime().catch(() => {});
this.fetchStartTime().catch(() => undefined);
return this.startTime
},
async getPluginCollections(force = false, customSource = null) {

View File

@@ -5,7 +5,11 @@ const HAN_IDEOGRAPH_RE = /\p{Unified_Ideograph}/u;
export const normalizeStr = (s) => (s ?? "").toString().toLowerCase().trim();
const normalizeLooseFromNormalized = (normalized) =>
normalized.replace(/[\s_-]+/g, "").replace(/[()()【】\[\]{}·•]+/g, "");
normalized
.replace(/[\s_-]+/g, "")
.replace(/[()()【】{}·•]/g, "")
.replace(/\[/g, "")
.replace(/\]/g, "");
export const normalizeLoose = (s) =>
normalizeLooseFromNormalized(normalizeStr(s));

View File

@@ -1,23 +1,40 @@
<template>
<div style="display: flex; flex-direction: column; height: 100%;">
<div style="flex-grow: 1; display: flex; align-items: center; justify-content: center; flex-direction: column;">
<div style="text-align: center; max-width: 600px;">
<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" 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" size="small"
variant="tonal" prepend-icon="mdi-comment-question">
{{ tm('hero.issueButton') }}
</v-btn>
</div>
</div>
<div style="display: flex; flex-direction: column; height: 100%;">
<div style="flex-grow: 1; display: flex; align-items: center; justify-content: center; flex-direction: column;">
<div style="text-align: center; max-width: 600px;">
<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
color="primary"
variant="tonal"
size="small"
prepend-icon="mdi-star"
@click="open('https://github.com/AstrBotDevs/AstrBot')"
>
{{ tm('hero.starButton') }}
</v-btn>
<v-btn
class="ml-4"
color="secondary"
size="small"
variant="tonal"
prepend-icon="mdi-comment-question"
@click="open('https://github.com/AstrBotDevs/AstrBot/issues')"
>
{{ tm('hero.issueButton') }}
</v-btn>
</div>
</div>
</div>
</div>
</template>
<script>

View File

@@ -1,35 +1,66 @@
<template>
<v-card style="height: 100%; width: 100%;">
<v-card-text class="pa-4" style="height: 100%;">
<v-container fluid class="d-flex flex-column" style="height: 100%;">
<v-card-text
class="pa-4"
style="height: 100%;"
>
<v-container
fluid
class="d-flex flex-column"
style="height: 100%;"
>
<div style="margin-bottom: 32px;">
<h1 class="gradient-text">{{ tm('page.title') }}</h1>
<h1 class="gradient-text">
{{ tm('page.title') }}
</h1>
<small style="color: #a3a3a3;">{{ tm('page.subtitle') }}</small>
</div>
<div style="display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap;">
<v-btn size="large" :variant="isActive('knowledge-base') ? 'flat' : 'tonal'"
:color="isActive('knowledge-base') ? '#9b72cb' : ''" rounded="lg"
@click="navigateTo('knowledge-base')">
<v-icon start>mdi-text-box-search</v-icon>
<v-btn
size="large"
:variant="isActive('knowledge-base') ? 'flat' : 'tonal'"
:color="isActive('knowledge-base') ? '#9b72cb' : ''"
rounded="lg"
@click="navigateTo('knowledge-base')"
>
<v-icon start>
mdi-text-box-search
</v-icon>
{{ tm('page.navigation.knowledgeBase') }}
</v-btn>
<v-btn size="large" :variant="isActive('long-term-memory') ? 'flat' : 'tonal'"
:color="isActive('long-term-memory') ? '#9b72cb' : ''" rounded="lg"
@click="navigateTo('long-term-memory')">
<v-icon start>mdi-dots-hexagon</v-icon>
<v-btn
size="large"
:variant="isActive('long-term-memory') ? 'flat' : 'tonal'"
:color="isActive('long-term-memory') ? '#9b72cb' : ''"
rounded="lg"
@click="navigateTo('long-term-memory')"
>
<v-icon start>
mdi-dots-hexagon
</v-icon>
{{ tm('page.navigation.longTermMemory') }}
</v-btn>
<v-btn size="large" :variant="isActive('other') ? 'flat' : 'tonal'"
:color="isActive('other') ? '#9b72cb' : ''" rounded="lg"
@click="navigateTo('other')">
<v-icon start>mdi-tools</v-icon>
<v-btn
size="large"
:variant="isActive('other') ? 'flat' : 'tonal'"
:color="isActive('other') ? '#9b72cb' : ''"
rounded="lg"
@click="navigateTo('other')"
>
<v-icon start>
mdi-tools
</v-icon>
{{ tm('page.navigation.other') }}
</v-btn>
</div>
<div id="sub-view" class="flex-grow-1" style="max-height: 100%;">
<router-view></router-view>
<div
id="sub-view"
class="flex-grow-1"
style="max-height: 100%;"
>
<router-view />
</div>
</v-container>
</v-card-text>
@@ -49,6 +80,12 @@ export default {
data() {
return {}
},
mounted() {
// 如果在根路径 /alkaid默认跳转到知识库页面
if (this.$route.path === '/alkaid') {
this.navigateTo('knowledge-base');
}
},
methods: {
navigateTo(tab) {
try {
@@ -67,12 +104,6 @@ export default {
return false;
}
}
},
mounted() {
// 如果在根路径 /alkaid默认跳转到知识库页面
if (this.$route.path === '/alkaid') {
this.navigateTo('knowledge-base');
}
}
}
</script>

View File

@@ -5,14 +5,18 @@ const customizer = useCustomizerStore();
</script>
<template>
<v-app :theme="customizer.uiTheme" style="height: 100%; width: 100%;">
<div
style="height: 100%; width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center;">
<div id="container">
<Chat :chatbox-mode="true"></Chat>
</div>
</div>
</v-app>
<v-app
:theme="customizer.uiTheme"
style="height: 100%; width: 100%;"
>
<div
style="height: 100%; width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center;"
>
<div id="container">
<Chat :chatbox-mode="true" />
</div>
</div>
</v-app>
</template>
<style scoped>

View File

@@ -3,9 +3,9 @@ import Chat from '@/components/chat/Chat.vue'
</script>
<template>
<div class="chat-container">
<Chat />
</div>
<div class="chat-container">
<Chat />
</div>
</template>
<style scoped>

Some files were not shown because too many files have changed in this diff Show More