agent-Specialization/static/src/components/sidebar/ConversationSidebar.vue

205 lines
7.6 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>
<aside class="conversation-sidebar" :class="{ collapsed }">
<div class="conversation-header" :class="{ 'collapsed-layout': collapsed }">
<template v-if="collapsed">
<div class="collapsed-header-buttons">
<button
type="button"
class="collapsed-control-btn conversation-menu-btn"
title="展开对话记录"
@click="$emit('toggle')"
>
<span class="sr-only">展开对话记录</span>
<span class="chat-icon" aria-hidden="true">
<slot name="collapsed-chat-icon">
<svg viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<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"
stroke="currentColor"
stroke-width="1.7"
stroke-linejoin="round"
/>
<path d="M9 9.5h10" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" />
<path d="M9 13h6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" />
</svg>
</slot>
</span>
</button>
<button
type="button"
class="collapsed-control-btn quick-plus-btn"
title="快捷新建对话"
@click="$emit('create')"
>
<span class="sr-only">新建对话</span>
<span class="pencil-icon" aria-hidden="true">
<slot name="collapsed-create-icon">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"
fill="none"
/>
<path d="m15 5 4 4" fill="none" />
</svg>
</slot>
</span>
</button>
</div>
</template>
<template v-else>
<button class="new-conversation-btn" @click="$emit('create')">
<span class="btn-icon">+</span>
<span class="btn-text">新建对话</span>
</button>
<button v-if="showCollapseButton" class="toggle-sidebar-btn" @click="$emit('toggle')">
<template v-if="collapseButtonVariant === 'toggle'">
<span v-if="collapsed" class="icon icon-md" :style="iconStyle('menu')" aria-hidden="true"></span>
<span v-else class="toggle-arrow" aria-hidden="true">←</span>
</template>
<template v-else>
<span class="toggle-close" aria-hidden="true">×</span>
</template>
</button>
</template>
</div>
<template v-if="!collapsed">
<div class="conversation-search">
<input
class="search-input"
:value="searchQuery"
placeholder="搜索对话..."
@input="$emit('search', ($event.target as HTMLInputElement).value)"
/>
</div>
<div class="conversation-list">
<div v-if="loading" class="loading-conversations">正在加载...</div>
<div v-else-if="!conversations.length" class="no-conversations">暂无对话记录</div>
<div v-else>
<div
v-for="conv in conversations"
:key="conv.id"
class="conversation-item"
:class="{ active: conv.id === currentConversationId }"
@click="$emit('select', conv.id)"
>
<div class="conversation-title">{{ conv.title }}</div>
<div class="conversation-meta">
<span class="conversation-time">{{ formatTime(conv.updated_at) }}</span>
<span class="conversation-counts">
{{ (conv.total_messages || 0) }}条消息
<span v-if="(conv.total_tools || 0) > 0"> · {{ conv.total_tools }}工具</span>
</span>
</div>
<div class="conversation-actions" @click.stop>
<button
type="button"
class="conversation-action-btn delete-btn"
title="删除对话"
@click="$emit('delete', conv.id)"
>
×
</button>
<button
type="button"
class="conversation-action-btn copy-btn"
title="复制对话"
@click="$emit('duplicate', conv.id)"
>
</button>
</div>
</div>
</div>
<div v-if="hasMore" class="load-more">
<button class="load-more-btn" type="button" :disabled="loadingMore" @click="$emit('load-more')">
{{ loadingMore ? '载入中...' : '加载更多' }}
</button>
</div>
</div>
</template>
<template v-else>
<div class="conversation-collapsed-spacer"></div>
</template>
<div class="conversation-personal-entry" :class="{ collapsed, active: personalPageVisible }">
<button
type="button"
class="personal-page-btn"
:class="{ 'icon-only': collapsed }"
title="个人页面"
@click="$emit('personal')"
>
<span class="sr-only">个人页面</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
></path>
</svg>
<span class="personal-label" v-if="!collapsed">个人空间</span>
</button>
</div>
</aside>
</template>
<script setup lang="ts">
defineOptions({ name: 'ConversationSidebar' });
import { computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useUiStore } from '@/stores/ui';
import { useConversationStore } from '@/stores/conversation';
import { usePersonalizationStore } from '@/stores/personalization';
const props = defineProps<{
formatTime?: (input: unknown) => string;
iconStyle?: (key: string) => Record<string, string>;
showCollapseButton?: boolean;
collapseButtonVariant?: 'toggle' | 'close';
}>();
defineEmits<{
(event: 'toggle'): void;
(event: 'create'): void;
(event: 'search', value: string): void;
(event: 'select', id: string): void;
(event: 'load-more'): void;
(event: 'personal'): void;
(event: 'delete', id: string): void;
(event: 'duplicate', id: string): void;
}>();
const uiStore = useUiStore();
const conversationStore = useConversationStore();
const personalizationStore = usePersonalizationStore();
const { sidebarCollapsed: collapsed } = storeToRefs(uiStore);
const {
searchQuery,
conversations,
conversationsLoading: loading,
hasMoreConversations: hasMore,
loadingMoreConversations: loadingMore,
currentConversationId
} = storeToRefs(conversationStore);
const { visible: personalPageVisible } = storeToRefs(personalizationStore);
const formatTime = (value: unknown) => (props.formatTime ? props.formatTime(value) : String(value));
const iconStyle = (key: string) => (props.iconStyle ? props.iconStyle(key) : {});
const showCollapseButton = computed(() => props.showCollapseButton !== false);
const collapseButtonVariant = computed(() => props.collapseButtonVariant || 'toggle');
</script>