agent-Specialization/static/src/App.vue

333 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<AppShell :download-file="downloadFile" :download-folder="downloadFolder">
<div v-if="!isConnected && messages.length === 0" class="app-loading-state">
<div class="loading-animation" aria-hidden="true">
<!-- From Uiverse.io by Nawsome -->
<div class="boxes">
<div class="box">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<div class="box">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<div class="box">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<div class="box">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
</div>
<div class="loading-copy">
<h2>加载中...</h2>
<p>正在连接服务器,请稍候</p>
</div>
</div>
<template v-else>
<div class="main-container">
<ConversationSidebar
v-if="!isMobileViewport"
:icon-style="iconStyle"
:format-time="formatTime"
@toggle="toggleSidebar"
@create="createNewConversation"
@search="handleSidebarSearch"
@select="loadConversation"
@load-more="loadMoreConversations"
@personal="openPersonalPage"
@delete="deleteConversation"
@duplicate="duplicateConversation"
@toggle-workspace="handleWorkspaceToggle"
/>
<div v-if="!isMobileViewport" class="workspace-region">
<LeftPanel
ref="leftPanel"
class="workspace-panel"
:class="{ 'workspace-panel--collapsed': workspaceCollapsed }"
:width="leftWidth"
:collapsed="workspaceCollapsed"
:icon-style="iconStyle"
:agent-version="agentVersion"
:thinking-mode="thinkingMode"
:run-mode="resolvedRunMode"
:is-connected="isConnected"
:panel-menu-open="panelMenuOpen"
:panel-mode="panelMode"
@toggle-panel-menu="togglePanelMenu"
@select-panel="selectPanelMode"
@open-file-manager="openGuiFileManager"
@toggle-thinking-mode="handleQuickModeToggle"
/>
<div
v-if="!workspaceCollapsed"
class="resize-handle"
@mousedown="startResize('left', $event)"
></div>
</div>
<main
class="chat-container"
:class="{
'chat-container--immersive': workspaceCollapsed,
'chat-container--mobile': isMobileViewport
}"
>
<TokenDrawer
:visible="Boolean(currentConversationId)"
:collapsed="tokenPanelCollapsed"
:current-conversation-tokens="currentConversationTokens"
:current-context-tokens="currentContextTokens"
:container-status="containerStatus"
:container-net-rate="containerNetRate"
:project-storage="projectStorage"
:usage-quota="usageQuota"
:format-token-count="formatTokenCount"
:format-percentage="formatPercentage"
:format-bytes="formatBytes"
:format-quota-value="formatQuotaValue"
:format-reset-time="formatResetTime"
:format-rate="formatRate"
/>
<ChatArea
ref="messagesArea"
:messages="messages"
:icon-style="iconStyle"
:expanded-blocks="expandedBlocks"
:render-markdown="renderMarkdown"
:toggle-block="toggleBlock"
:handle-thinking-scroll="handleThinkingScroll"
:streaming-message="streamingMessage"
:get-tool-icon="getToolIcon"
:get-tool-status-text="getToolStatusText"
:get-tool-animation-class="getToolAnimationClass"
:get-tool-description="getToolDescription"
:format-search-topic="formatSearchTopic"
:format-search-time="formatSearchTime"
/>
<div class="scroll-lock-toggle" :class="{ locked: autoScrollEnabled && !userScrolling }">
<button @click="toggleScrollLock" class="scroll-lock-btn">
<svg
v-if="autoScrollEnabled && !userScrolling"
viewBox="0 0 24 24"
aria-hidden="true"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M8 11h8a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1Z" />
<path d="M9 11V8a3 3 0 0 1 6 0v3" />
</svg>
<svg v-else viewBox="0 0 24 24" aria-hidden="true" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 5v12" />
<path d="M7 13l5 5 5-5" />
</svg>
</button>
</div>
<InputComposer
ref="inputComposer"
:input-message="inputMessage"
:input-is-multiline="inputIsMultiline"
:input-is-focused="inputIsFocused"
:is-connected="isConnected"
:streaming-message="streamingMessage"
:uploading="uploading"
:thinking-mode="thinkingMode"
:run-mode="resolvedRunMode"
:quick-menu-open="quickMenuOpen"
:tool-menu-open="toolMenuOpen"
:mode-menu-open="modeMenuOpen"
:tool-settings="toolSettings"
:tool-settings-loading="toolSettingsLoading"
:settings-open="settingsOpen"
:compressing="compressing"
:current-conversation-id="currentConversationId"
:icon-style="iconStyle"
:tool-category-icon="toolCategoryIcon"
@update:input-message="inputSetMessage"
@input-change="handleInputChange"
@input-focus="handleInputFocus"
@input-blur="handleInputBlur"
@toggle-quick-menu="toggleQuickMenu"
@send-message="sendMessage"
@send-or-stop="handleSendOrStop"
@quick-upload="handleQuickUpload"
@toggle-tool-menu="toggleToolMenu"
@toggle-mode-menu="toggleModeMenu"
@select-run-mode="handleModeSelect"
@toggle-settings="toggleSettings"
@update-tool-category="updateToolCategory"
@realtime-terminal="handleRealtimeTerminalClick"
@toggle-focus-panel="handleFocusPanelToggleClick"
@toggle-token-panel="handleTokenPanelToggleClick"
@compress-conversation="handleCompressConversationClick"
@file-selected="handleFileSelected"
/>
</main>
<div v-if="!isMobileViewport" class="resize-handle" @mousedown="startResize('right', $event)"></div>
<FocusPanel
v-if="!isMobileViewport"
:collapsed="rightCollapsed"
:width="rightWidth"
:icon-style="iconStyle"
:get-language-class="getLanguageClass"
/>
</div>
<PersonalizationDrawer />
<div
v-if="isMobileViewport"
class="mobile-panel-trigger"
:class="{ 'is-hidden': Boolean(activeMobileOverlay) }"
ref="mobilePanelTrigger"
>
<div class="mobile-panel-topbar">
<button type="button" class="mobile-panel-fab" aria-label="切换工作区" @click="toggleMobileOverlayMenu">
<img :src="mobilePanelIcon" alt="" aria-hidden="true" />
</button>
<div class="mobile-topbar-title" :title="currentConversationTitle || '未命名对话'">
{{ currentConversationTitle || '未命名对话' }}
</div>
</div>
<transition name="mobile-panel-menu">
<div v-if="mobileOverlayMenuOpen" class="mobile-panel-menu">
<button
type="button"
class="mobile-menu-btn mobile-menu-btn--conversation"
aria-label="对话记录"
@click="openMobileOverlay('conversation')"
>
<svg class="mobile-menu-svg" viewBox="0 0 28 28" fill="none" aria-hidden="true">
<path
d="M5 6.5c0-1.38 1.12-2.5 2.5-2.5h13c1.38 0 2.5 1.12 2.5 2.5v8.5c0 1.38-1.12 2.5-2.5 2.5h-5.6l-3.4 3.2.6-3.2H7.5c-1.38 0-2.5-1.12-2.5-2.5V6.5z"
fill="none"
stroke="currentColor"
stroke-width="1.9"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path d="M9 9.5h10" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" />
<path d="M9 13h6" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" />
</svg>
</button>
<button type="button" class="mobile-menu-btn" aria-label="工作台" @click="openMobileOverlay('workspace')">
<img :src="mobileMenuIcons.workspace" alt="" aria-hidden="true" />
</button>
<button type="button" class="mobile-menu-btn" aria-label="聚焦面板" @click="openMobileOverlay('focus')">
<img :src="mobileMenuIcons.focus" alt="" aria-hidden="true" />
</button>
<button type="button" class="mobile-menu-btn" aria-label="个人空间" @click="handleMobilePersonalClick">
<img :src="mobileMenuIcons.personal" alt="" aria-hidden="true" />
</button>
</div>
</transition>
</div>
<transition name="mobile-panel-overlay">
<div
v-if="isMobileViewport && activeMobileOverlay === 'conversation'"
class="mobile-panel-overlay mobile-panel-overlay--left"
@click.self="closeMobileOverlay"
>
<div class="mobile-panel-sheet mobile-panel-sheet--conversation">
<ConversationSidebar
class="mobile-overlay-content"
:icon-style="iconStyle"
:format-time="formatTime"
:show-collapse-button="true"
collapse-button-variant="close"
@toggle="closeMobileOverlay"
@create="createNewConversation"
@search="handleSidebarSearch"
@select="handleMobileOverlaySelect($event)"
@load-more="loadMoreConversations"
@personal="openPersonalPage"
@delete="deleteConversation"
@duplicate="duplicateConversation"
/>
</div>
</div>
</transition>
<transition name="mobile-panel-overlay">
<div
v-if="isMobileViewport && activeMobileOverlay === 'workspace'"
class="mobile-panel-overlay mobile-panel-overlay--left"
@click.self="closeMobileOverlay"
>
<div class="mobile-panel-sheet mobile-panel-sheet--workspace">
<button type="button" class="mobile-overlay-close" aria-label="关闭面板" @click="closeMobileOverlay">
×
</button>
<LeftPanel
class="mobile-overlay-content"
:width="Math.min(leftWidth, 420)"
:icon-style="iconStyle"
:agent-version="agentVersion"
:thinking-mode="thinkingMode"
:is-connected="isConnected"
:panel-menu-open="panelMenuOpen"
:panel-mode="panelMode"
@toggle-panel-menu="togglePanelMenu"
@select-panel="selectPanelMode"
@open-file-manager="openGuiFileManager"
/>
</div>
</div>
</transition>
<transition name="mobile-panel-overlay">
<div
v-if="isMobileViewport && activeMobileOverlay === 'focus'"
class="mobile-panel-overlay mobile-panel-overlay--right"
@click.self="closeMobileOverlay"
>
<div class="mobile-panel-sheet mobile-panel-sheet--focus">
<FocusPanel
class="mobile-overlay-content"
:collapsed="false"
:width="Math.min(rightWidth, 420)"
:icon-style="iconStyle"
:get-language-class="getLanguageClass"
:show-close-button="true"
@close="closeMobileOverlay"
/>
</div>
</div>
</transition>
</template>
</AppShell>
</template>
<script setup lang="ts">
import appOptions from './app';
const mobilePanelIcon = new URL('../icons/align-left.svg', import.meta.url).href;
const mobileMenuIcons = {
workspace: new URL('../icons/folder.svg', import.meta.url).href,
focus: new URL('../icons/eye.svg', import.meta.url).href,
personal: new URL('../icons/user.svg', import.meta.url).href
};
defineOptions(appOptions);
</script>