406 lines
16 KiB
Vue
406 lines
16 KiB
Vue
<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"
|
||
:display-mode="chatDisplayMode"
|
||
:display-mode-disabled="displayModeSwitchDisabled"
|
||
@toggle="toggleSidebar"
|
||
@create="createNewConversation"
|
||
@search="handleSidebarSearch"
|
||
@select="loadConversation"
|
||
@load-more="loadMoreConversations"
|
||
@personal="openPersonalPage"
|
||
@delete="deleteConversation"
|
||
@duplicate="duplicateConversation"
|
||
@toggle-workspace="handleWorkspaceToggle"
|
||
@toggle-display-mode="handleDisplayModeToggle"
|
||
/>
|
||
|
||
<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,
|
||
'chat-container--monitor': chatDisplayMode === 'monitor',
|
||
'has-title-ribbon': titleRibbonVisible
|
||
}"
|
||
>
|
||
<TokenDrawer
|
||
:visible="Boolean(currentConversationId)"
|
||
:collapsed="tokenPanelCollapsed"
|
||
@toggle="handleTokenPanelToggleClick"
|
||
: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"
|
||
/>
|
||
|
||
<div v-if="titleRibbonVisible" class="conversation-ribbon">
|
||
<span class="conversation-ribbon__text">{{ titleTypingText || currentConversationTitle }}</span>
|
||
</div>
|
||
|
||
<ChatArea
|
||
v-show="chatDisplayMode === 'chat'"
|
||
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"
|
||
/>
|
||
<VirtualMonitorSurface v-show="chatDisplayMode === 'monitor'" />
|
||
|
||
<div v-if="blankHeroActive" class="blank-hero-overlay">
|
||
<span class="icon icon-lg" :style="iconStyle('bot')" aria-hidden="true"></span>
|
||
<p class="blank-hero-text">{{ blankWelcomeText }}</p>
|
||
</div>
|
||
|
||
<div
|
||
v-if="chatDisplayMode === 'chat'"
|
||
class="scroll-lock-toggle"
|
||
:class="{ locked: autoScrollEnabled && !userScrolling && isOutputActive() }"
|
||
>
|
||
<button @click="toggleScrollLock" class="scroll-lock-btn">
|
||
<svg
|
||
v-if="autoScrollEnabled && !userScrolling && isOutputActive()"
|
||
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>
|
||
|
||
<div class="composer-container" :class="{ 'blank-hero-mode': composerHeroActive }">
|
||
<InputComposer
|
||
ref="inputComposer"
|
||
:input-message="inputMessage"
|
||
:input-is-multiline="inputIsMultiline"
|
||
:input-is-focused="inputIsFocused"
|
||
:is-connected="isConnected"
|
||
:streaming-message="composerBusy"
|
||
:input-locked="displayLockEngaged"
|
||
:uploading="uploading"
|
||
:thinking-mode="thinkingMode"
|
||
:run-mode="resolvedRunMode"
|
||
:model-menu-open="modelMenuOpen"
|
||
:model-options="modelOptions"
|
||
:current-model-key="currentModelKey"
|
||
: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"
|
||
:selected-images="selectedImages"
|
||
@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"
|
||
@toggle-model-menu="toggleModelMenu"
|
||
@select-run-mode="handleModeSelect"
|
||
@select-model="handleModelSelect"
|
||
@toggle-settings="toggleSettings"
|
||
@update-tool-category="updateToolCategory"
|
||
@realtime-terminal="handleRealtimeTerminalClick"
|
||
@toggle-focus-panel="handleFocusPanelToggleClick"
|
||
@toggle-token-panel="handleTokenPanelToggleClick"
|
||
@compress-conversation="handleCompressConversationClick"
|
||
@file-selected="handleFileSelected"
|
||
@pick-images="openImagePicker"
|
||
@remove-image="handleRemoveImage"
|
||
@open-review="openReviewDialog"
|
||
/>
|
||
</div>
|
||
</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 />
|
||
<LiquidGlassWidget />
|
||
<transition name="overlay-fade">
|
||
<ImagePicker
|
||
v-if="imagePickerOpen"
|
||
:open="imagePickerOpen"
|
||
:entries="imageEntries"
|
||
:initial-selected="selectedImages"
|
||
:loading="imageLoading"
|
||
@close="closeImagePicker"
|
||
@confirm="handleImagesConfirmed"
|
||
/>
|
||
</transition>
|
||
<transition name="overlay-fade">
|
||
<ConversationReviewDialog
|
||
v-if="reviewDialogOpen"
|
||
:open="reviewDialogOpen"
|
||
:conversations="conversations"
|
||
:current-conversation-id="currentConversationId"
|
||
:selected-id="reviewSelectedConversationId"
|
||
:loading="conversationsLoading"
|
||
:loading-more="loadingMoreConversations"
|
||
:has-more="hasMoreConversations"
|
||
:submitting="reviewSubmitting"
|
||
:preview="reviewPreviewLines"
|
||
:preview-loading="reviewPreviewLoading"
|
||
:preview-error="reviewPreviewError"
|
||
:preview-limit="reviewPreviewLimit"
|
||
:send-to-model="reviewSendToModel"
|
||
:generated-path="reviewGeneratedPath"
|
||
@close="reviewDialogOpen = false"
|
||
@select="handleReviewSelect"
|
||
@load-more="loadMoreConversations"
|
||
@toggle-send="reviewSendToModel = $event"
|
||
@confirm="handleConfirmReview"
|
||
/>
|
||
</transition>
|
||
|
||
<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"
|
||
:display-mode="chatDisplayMode"
|
||
:display-mode-disabled="displayModeSwitchDisabled"
|
||
: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"
|
||
@toggle-display-mode="handleDisplayModeToggle"
|
||
/>
|
||
</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';
|
||
import VirtualMonitorSurface from './components/chat/VirtualMonitorSurface.vue';
|
||
|
||
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>
|