186 lines
7.4 KiB
Vue
186 lines
7.4 KiB
Vue
<template>
|
||
<aside
|
||
class="sidebar left-sidebar"
|
||
:class="{ 'panel-hidden': hidden }"
|
||
:style="panelStyles"
|
||
:aria-hidden="hidden ? 'true' : 'false'"
|
||
>
|
||
<div class="sidebar-status">
|
||
<div class="compact-status-card">
|
||
<div class="status-line">
|
||
<div class="status-brand">
|
||
<span class="icon icon-lg status-logo" :style="iconStyle('bot')" aria-hidden="true"></span>
|
||
<div class="brand-text">
|
||
<span class="brand-name">AI Agent</span>
|
||
<span class="agent-version" v-if="agentVersion">{{ agentVersion }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="status-indicators">
|
||
<button
|
||
type="button"
|
||
class="mode-indicator"
|
||
:class="{ thinking: thinkingMode, fast: !thinkingMode }"
|
||
:title="thinkingMode ? '切换到快速模式' : '切换到思考模式'"
|
||
:aria-pressed="thinkingMode ? 'true' : 'false'"
|
||
@click="$emit('toggle-mode')"
|
||
>
|
||
<transition name="mode-icon" mode="out-in">
|
||
<span
|
||
class="icon icon-sm"
|
||
:style="iconStyle(thinkingMode ? 'brain' : 'zap')"
|
||
:key="thinkingMode ? 'brain' : 'zap'"
|
||
aria-hidden="true"
|
||
></span>
|
||
</transition>
|
||
</button>
|
||
<span class="connection-dot" :class="{ active: isConnected }" :title="isConnected ? '已连接' : '未连接'"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="sidebar-panel-card-wrapper">
|
||
<div class="sidebar-panel-card">
|
||
<div class="sidebar-header">
|
||
<div class="panel-menu-wrapper" ref="panelMenuWrapper">
|
||
<button class="sidebar-view-toggle" @click.stop="$emit('toggle-panel-menu')" title="切换侧边栏">
|
||
<span class="icon icon-md" :style="iconStyle('menu')" aria-hidden="true"></span>
|
||
</button>
|
||
<transition name="fade">
|
||
<div class="panel-menu" v-if="panelMenuOpen">
|
||
<button
|
||
type="button"
|
||
:class="{ active: panelMode === 'files' }"
|
||
@click.stop="$emit('select-panel', 'files')"
|
||
title="项目文件"
|
||
>
|
||
<span class="icon icon-md" :style="iconStyle('folder')" aria-hidden="true"></span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
:class="{ active: panelMode === 'todo' }"
|
||
@click.stop="$emit('select-panel', 'todo')"
|
||
title="待办列表"
|
||
>
|
||
<span class="icon icon-md" :style="iconStyle('stickyNote')" aria-hidden="true"></span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
:class="{ active: panelMode === 'subAgents' }"
|
||
@click.stop="$emit('select-panel', 'subAgents')"
|
||
title="子智能体"
|
||
>
|
||
<span class="icon icon-md" :style="iconStyle('bot')" aria-hidden="true"></span>
|
||
</button>
|
||
</div>
|
||
</transition>
|
||
</div>
|
||
<button class="sidebar-manage-btn" @click="$emit('open-file-manager')" title="打开桌面式文件管理器">管理</button>
|
||
<h3>
|
||
<span v-if="panelMode === 'files'" class="icon-label">
|
||
<span class="icon icon-sm" :style="iconStyle('folder')" aria-hidden="true"></span>
|
||
<span>项目文件</span>
|
||
</span>
|
||
<span v-else-if="panelMode === 'todo'" class="icon-label">
|
||
<span class="icon icon-sm" :style="iconStyle('stickyNote')" aria-hidden="true"></span>
|
||
<span>待办列表</span>
|
||
</span>
|
||
<span v-else class="icon-label">
|
||
<span class="icon icon-sm" :style="iconStyle('bot')" aria-hidden="true"></span>
|
||
<span>子智能体</span>
|
||
</span>
|
||
</h3>
|
||
</div>
|
||
<div class="sidebar-panel-content">
|
||
<div v-if="panelMode === 'todo'" class="todo-panel">
|
||
<div v-if="!todoList" class="todo-empty">暂无待办列表</div>
|
||
<div v-else>
|
||
<div class="todo-task" v-for="task in todoList.tasks || []" :key="task.index" :class="{ done: task.status === 'done' }">
|
||
<span class="todo-task-title">task{{ task.index }}:{{ task.title }}</span>
|
||
<span class="todo-task-status icon-label">
|
||
<span class="icon icon-sm" :style="iconStyle(task.status === 'done' ? 'check' : 'checkbox')" aria-hidden="true"></span>
|
||
<span>{{ task.status === 'done' ? '完成' : '未完成' }}</span>
|
||
</span>
|
||
</div>
|
||
<div class="todo-instruction">{{ todoList.instruction }}</div>
|
||
</div>
|
||
</div>
|
||
<div v-else-if="panelMode === 'subAgents'" class="sub-agent-panel">
|
||
<div v-if="!subAgents.length" class="sub-agent-empty">暂无运行中的子智能体</div>
|
||
<div v-else class="sub-agent-cards">
|
||
<div class="sub-agent-card" v-for="agent in subAgents" :key="agent.task_id" @click="openSubAgent(agent)">
|
||
<div class="sub-agent-header">
|
||
<span class="sub-agent-id">#{{ agent.agent_id }}</span>
|
||
<span class="sub-agent-status" :class="agent.status">{{ agent.status }}</span>
|
||
</div>
|
||
<div class="sub-agent-summary">{{ agent.summary }}</div>
|
||
<div class="sub-agent-tool" v-if="agent.last_tool">当前:{{ agent.last_tool }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-else class="file-tree" @contextmenu.prevent>
|
||
<FileNode
|
||
v-for="node in fileTree"
|
||
:key="node.path"
|
||
:node="node"
|
||
:level="0"
|
||
:expanded-folders="expandedFolders"
|
||
:icon-style="iconStyle"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, ref } from 'vue';
|
||
import { storeToRefs } from 'pinia';
|
||
import FileNode from '@/components/files/FileNode.vue';
|
||
import { useFileStore } from '@/stores/file';
|
||
import { useSubAgentStore } from '@/stores/subAgent';
|
||
|
||
defineOptions({ name: 'LeftPanel' });
|
||
|
||
const props = defineProps<{
|
||
width: number;
|
||
iconStyle: (key: string) => Record<string, string>;
|
||
agentVersion: string | null;
|
||
thinkingMode: boolean;
|
||
isConnected: boolean;
|
||
panelMenuOpen: boolean;
|
||
panelMode: 'files' | 'todo' | 'subAgents';
|
||
hidden?: boolean;
|
||
}>();
|
||
|
||
defineEmits<{
|
||
(event: 'toggle-panel-menu'): void;
|
||
(event: 'select-panel', mode: 'files' | 'todo' | 'subAgents'): void;
|
||
(event: 'open-file-manager'): void;
|
||
(event: 'toggle-mode'): void;
|
||
}>();
|
||
|
||
const panelMenuWrapper = ref<HTMLElement | null>(null);
|
||
const fileStore = useFileStore();
|
||
const subAgentStore = useSubAgentStore();
|
||
const { fileTree, expandedFolders, todoList } = storeToRefs(fileStore);
|
||
const { subAgents } = storeToRefs(subAgentStore);
|
||
|
||
const openSubAgent = (agent: any) => {
|
||
subAgentStore.openSubAgent(agent);
|
||
};
|
||
|
||
const panelStyles = computed(() => {
|
||
const basis = props.hidden ? 0 : props.width;
|
||
return {
|
||
width: `${basis}px`,
|
||
minWidth: `${basis}px`,
|
||
flexBasis: `${basis}px`
|
||
};
|
||
});
|
||
|
||
defineExpose({
|
||
panelMenuWrapper
|
||
});
|
||
</script>
|