mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-14 19:45:08 +08:00
refactor(ux): simplify interactions and humanize UI with Jobs-inspired review
This commit is contained in:
parent
4cd373dcdd
commit
1c2acfd2e9
@ -261,6 +261,19 @@ public class ConversationService {
|
|||||||
messageMapper.updateById(message);
|
messageMapper.updateById(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重命名会话
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void renameConversation(String conversationId, String title) {
|
||||||
|
ConversationEntity conv = conversationMapper.selectOne(new LambdaQueryWrapper<ConversationEntity>()
|
||||||
|
.eq(ConversationEntity::getConversationId, conversationId));
|
||||||
|
if (conv != null) {
|
||||||
|
conv.setTitle(title);
|
||||||
|
conversationMapper.updateById(conv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新会话的流状态(running / idle)
|
* 更新会话的流状态(running / idle)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -68,6 +68,24 @@ public class ConversationController {
|
|||||||
return R.ok();
|
return R.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重命名会话
|
||||||
|
*/
|
||||||
|
@Operation(summary = "重命名会话")
|
||||||
|
@PutMapping("/{conversationId}/title")
|
||||||
|
public R<Void> rename(@PathVariable String conversationId, @RequestBody Map<String, String> body, Authentication auth) {
|
||||||
|
String username = auth != null ? auth.getName() : "anonymous";
|
||||||
|
if (!conversationService.isConversationOwner(conversationId, username)) {
|
||||||
|
return R.fail("无权操作该会话");
|
||||||
|
}
|
||||||
|
String title = body.getOrDefault("title", "").trim();
|
||||||
|
if (title.isEmpty() || title.length() > 100) {
|
||||||
|
return R.fail("标题不合法");
|
||||||
|
}
|
||||||
|
conversationService.renameConversation(conversationId, title);
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清空会话消息(保留会话记录)
|
* 清空会话消息(保留会话记录)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -136,6 +136,8 @@ export const conversationApi = {
|
|||||||
http.delete(`/conversations/${conversationId}`),
|
http.delete(`/conversations/${conversationId}`),
|
||||||
clearMessages: (conversationId: string) =>
|
clearMessages: (conversationId: string) =>
|
||||||
http.delete(`/conversations/${conversationId}/messages`),
|
http.delete(`/conversations/${conversationId}/messages`),
|
||||||
|
rename: (conversationId: string, title: string) =>
|
||||||
|
http.put(`/conversations/${conversationId}/title`, { title }),
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Skill ====================
|
// ==================== Skill ====================
|
||||||
|
|||||||
@ -1,65 +1,67 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="workspace-switcher" :class="{ collapsed }">
|
<div class="workspace-switcher" :class="{ collapsed }">
|
||||||
<el-dropdown
|
<button
|
||||||
v-if="workspaces.length > 0"
|
class="ws-trigger"
|
||||||
trigger="click"
|
:title="collapsed ? currentLabel : ''"
|
||||||
:teleported="true"
|
@click="open = !open"
|
||||||
@command="onSwitch"
|
|
||||||
>
|
>
|
||||||
<button class="ws-trigger" :title="collapsed ? currentLabel : ''">
|
<span class="ws-trigger__icon">
|
||||||
<span class="ws-icon">
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<rect x="2" y="7" width="20" height="14" rx="2" ry="2"/>
|
||||||
|
<path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<template v-if="!collapsed">
|
||||||
|
<span class="ws-trigger__name">{{ currentLabel }}</span>
|
||||||
|
<svg class="ws-trigger__arrow" :class="{ open }" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
||||||
|
</template>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Transition name="fade">
|
||||||
|
<div v-if="open" class="ws-backdrop" @click="open = false"></div>
|
||||||
|
</Transition>
|
||||||
|
<Transition name="ws-dropdown">
|
||||||
|
<div v-if="open" class="ws-dropdown">
|
||||||
|
<div
|
||||||
|
v-for="ws in workspaces"
|
||||||
|
:key="ws.id"
|
||||||
|
class="ws-item"
|
||||||
|
:class="{ active: ws.id === currentWorkspaceId }"
|
||||||
|
@click="onSelect(ws.id)"
|
||||||
|
>
|
||||||
|
<svg class="ws-item__icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<rect x="2" y="7" width="20" height="14" rx="2" ry="2"/>
|
<rect x="2" y="7" width="20" height="14" rx="2" ry="2"/>
|
||||||
<path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>
|
<path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
<span class="ws-item__name">{{ ws.name }}</span>
|
||||||
<span v-if="!collapsed" class="ws-name">{{ currentLabel }}</span>
|
<svg v-if="ws.id === currentWorkspaceId" class="ws-item__check" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
|
||||||
<svg v-if="!collapsed" class="ws-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
</div>
|
||||||
<polyline points="6 9 12 15 18 9"/>
|
<div class="ws-divider"></div>
|
||||||
</svg>
|
<div class="ws-item ws-item--manage" @click="onManage">
|
||||||
</button>
|
<svg class="ws-item__icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||||
<template #dropdown>
|
</svg>
|
||||||
<el-dropdown-menu>
|
<span class="ws-item__name">{{ t('common.manageWorkspaces') }}</span>
|
||||||
<el-dropdown-item
|
</div>
|
||||||
v-for="ws in workspaces"
|
</div>
|
||||||
:key="ws.id"
|
</Transition>
|
||||||
:command="ws.id"
|
|
||||||
:class="{ 'is-active': ws.id === currentWorkspaceId }"
|
|
||||||
>
|
|
||||||
<span class="ws-menu-icon">
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<rect x="2" y="7" width="20" height="14" rx="2" ry="2"/>
|
|
||||||
<path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
{{ ws.name }}
|
|
||||||
</el-dropdown-item>
|
|
||||||
<el-dropdown-item divided command="__manage__">
|
|
||||||
<span class="ws-menu-icon">
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
Manage Workspaces
|
|
||||||
</el-dropdown-item>
|
|
||||||
</el-dropdown-menu>
|
|
||||||
</template>
|
|
||||||
</el-dropdown>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
|
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
collapsed?: boolean
|
collapsed?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
const store = useWorkspaceStore()
|
const store = useWorkspaceStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const open = ref(false)
|
||||||
const workspaces = computed(() => store.workspaces)
|
const workspaces = computed(() => store.workspaces)
|
||||||
const currentWorkspaceId = computed(() => store.currentWorkspaceId)
|
const currentWorkspaceId = computed(() => store.currentWorkspaceId)
|
||||||
const currentLabel = computed(() => store.currentWorkspace?.name || 'Workspace')
|
const currentLabel = computed(() => store.currentWorkspace?.name || 'Workspace')
|
||||||
@ -68,45 +70,51 @@ onMounted(() => {
|
|||||||
store.fetchWorkspaces()
|
store.fetchWorkspaces()
|
||||||
})
|
})
|
||||||
|
|
||||||
function onSwitch(id: number | string) {
|
function onSelect(id: number) {
|
||||||
if (id === '__manage__') {
|
open.value = false
|
||||||
router.push('/settings/workspaces')
|
if (id !== currentWorkspaceId.value) {
|
||||||
return
|
store.switchWorkspace(id)
|
||||||
}
|
}
|
||||||
store.switchWorkspace(id as number)
|
}
|
||||||
|
|
||||||
|
function onManage() {
|
||||||
|
open.value = false
|
||||||
|
router.push('/settings/workspaces')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.workspace-switcher {
|
.workspace-switcher {
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-bottom: 1px solid var(--el-border-color-lighter, #ebeef5);
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-switcher.collapsed {
|
.workspace-switcher.collapsed {
|
||||||
padding: 8px 4px;
|
padding: 8px 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Trigger */
|
||||||
.ws-trigger {
|
.ws-trigger {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 6px 8px;
|
padding: 7px 10px;
|
||||||
border: none;
|
border: 1px solid var(--mc-sidebar-border, var(--mc-border-light));
|
||||||
border-radius: 6px;
|
border-radius: 12px;
|
||||||
background: var(--el-fill-color-light, #f5f7fa);
|
background: var(--mc-sidebar-hover, rgba(0,0,0,0.04));
|
||||||
color: var(--el-text-color-primary, #303133);
|
color: var(--mc-sidebar-text, var(--mc-text-primary));
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
transition: background 0.2s;
|
transition: all 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ws-trigger:hover {
|
.ws-trigger:hover {
|
||||||
background: var(--el-fill-color, #f0f2f5);
|
background: var(--mc-sidebar-active, rgba(0,0,0,0.06));
|
||||||
|
border-color: var(--mc-primary, #d96d46);
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapsed .ws-trigger {
|
.collapsed .ws-trigger {
|
||||||
@ -114,36 +122,129 @@ function onSwitch(id: number | string) {
|
|||||||
height: 36px;
|
height: 36px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ws-icon {
|
.ws-trigger__icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
opacity: 0.7;
|
color: var(--mc-accent, var(--mc-primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
.ws-name {
|
.ws-trigger__name {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ws-chevron {
|
.ws-trigger__arrow {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--mc-sidebar-text, var(--mc-text-tertiary));
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-trigger__arrow.open {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Backdrop */
|
||||||
|
.ws-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown */
|
||||||
|
.ws-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 4px);
|
||||||
|
left: 12px;
|
||||||
|
right: 12px;
|
||||||
|
z-index: 100;
|
||||||
|
background: var(--mc-bg-elevated);
|
||||||
|
border: 1px solid var(--mc-border);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 6px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||||
|
max-height: 280px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsed .ws-dropdown {
|
||||||
|
left: 6px;
|
||||||
|
right: auto;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.12s;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--mc-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-item:hover {
|
||||||
|
background: var(--mc-bg-sunken);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-item.active {
|
||||||
|
background: var(--mc-primary-bg);
|
||||||
|
color: var(--mc-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-item__icon {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ws-menu-icon {
|
.ws-item.active .ws-item__icon {
|
||||||
display: inline-flex;
|
opacity: 1;
|
||||||
margin-right: 6px;
|
color: var(--mc-primary);
|
||||||
opacity: 0.6;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.is-active) {
|
.ws-item__name {
|
||||||
color: var(--el-color-primary);
|
flex: 1;
|
||||||
font-weight: 600;
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ws-item__check {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--mc-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: var(--mc-border-light);
|
||||||
|
margin: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-item--manage {
|
||||||
|
color: var(--mc-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-item--manage:hover {
|
||||||
|
color: var(--mc-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Transitions */
|
||||||
|
.fade-enter-active, .fade-leave-active { transition: opacity 0.15s; }
|
||||||
|
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||||
|
|
||||||
|
.ws-dropdown-enter-active { transition: all 0.15s ease-out; }
|
||||||
|
.ws-dropdown-leave-active { transition: all 0.1s ease-in; }
|
||||||
|
.ws-dropdown-enter-from { opacity: 0; transform: translateY(-6px) scale(0.97); }
|
||||||
|
.ws-dropdown-leave-to { opacity: 0; transform: translateY(-4px) scale(0.98); }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -22,6 +22,7 @@ export default {
|
|||||||
search: 'Search',
|
search: 'Search',
|
||||||
expandSidebar: 'Expand sidebar',
|
expandSidebar: 'Expand sidebar',
|
||||||
collapseSidebar: 'Collapse sidebar',
|
collapseSidebar: 'Collapse sidebar',
|
||||||
|
manageWorkspaces: 'Manage Workspaces',
|
||||||
show: 'Show',
|
show: 'Show',
|
||||||
hide: 'Hide',
|
hide: 'Hide',
|
||||||
on: 'On',
|
on: 'On',
|
||||||
@ -115,6 +116,7 @@ export default {
|
|||||||
loadConversationsFailed: 'Failed to load conversations',
|
loadConversationsFailed: 'Failed to load conversations',
|
||||||
loadMessagesFailed: 'Failed to load messages',
|
loadMessagesFailed: 'Failed to load messages',
|
||||||
deleteConversationFailed: 'Failed to delete conversation',
|
deleteConversationFailed: 'Failed to delete conversation',
|
||||||
|
deleteConfirm: 'Delete this conversation? This cannot be undone.',
|
||||||
switchModelFailed: 'Failed to switch model',
|
switchModelFailed: 'Failed to switch model',
|
||||||
uploadFailed: 'File upload failed',
|
uploadFailed: 'File upload failed',
|
||||||
dropToUpload: 'Drop files or folders here',
|
dropToUpload: 'Drop files or folders here',
|
||||||
@ -597,6 +599,10 @@ export default {
|
|||||||
planExecute: 'Plan-Execute',
|
planExecute: 'Plan-Execute',
|
||||||
enabled: 'Enabled',
|
enabled: 'Enabled',
|
||||||
disabled: 'Disabled',
|
disabled: 'Disabled',
|
||||||
|
basic: 'Basic',
|
||||||
|
skills: 'Skills',
|
||||||
|
tools: 'Tools',
|
||||||
|
context: 'Context',
|
||||||
},
|
},
|
||||||
columns: {
|
columns: {
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
@ -656,6 +662,14 @@ export default {
|
|||||||
toggleFailed: 'Failed to toggle agent status',
|
toggleFailed: 'Failed to toggle agent status',
|
||||||
toggleSuccess: 'Status updated',
|
toggleSuccess: 'Status updated',
|
||||||
},
|
},
|
||||||
|
binding: {
|
||||||
|
skillsHint: 'Select skills this agent can use. Leave empty to use all enabled skills.',
|
||||||
|
toolsHint: 'Select tools this agent can use. Leave empty to use all enabled tools.',
|
||||||
|
noSkills: 'No skills available',
|
||||||
|
noTools: 'No tools available',
|
||||||
|
contextHint: 'Manage context files (e.g. AGENT.md) that define this agent\'s behavior, knowledge, and instructions.',
|
||||||
|
goToContext: 'Edit Context Files',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
security: {
|
security: {
|
||||||
title: 'Security',
|
title: 'Security',
|
||||||
@ -727,6 +741,7 @@ export default {
|
|||||||
workspaces: {
|
workspaces: {
|
||||||
title: 'Workspaces',
|
title: 'Workspaces',
|
||||||
desc: 'Manage workspaces for your organization.',
|
desc: 'Manage workspaces for your organization.',
|
||||||
|
manage: 'Manage Workspaces',
|
||||||
newWorkspace: 'New Workspace',
|
newWorkspace: 'New Workspace',
|
||||||
loading: 'Loading workspaces...',
|
loading: 'Loading workspaces...',
|
||||||
noWorkspaces: 'No workspaces found.',
|
noWorkspaces: 'No workspaces found.',
|
||||||
@ -1527,8 +1542,8 @@ export default {
|
|||||||
periodDesc: 'A sharper view of how your system behaves across short, medium, and monthly horizons.',
|
periodDesc: 'A sharper view of how your system behaves across short, medium, and monthly horizons.',
|
||||||
runsDesc: 'Execution history with timing, cost, and outcome at a glance.',
|
runsDesc: 'Execution history with timing, cost, and outcome at a glance.',
|
||||||
trend: {
|
trend: {
|
||||||
title: '7-Day Trend',
|
title: 'Your Week',
|
||||||
subtitle: 'Messages and token consumption over the past week.',
|
subtitle: 'How often you and your AI have been working together.',
|
||||||
},
|
},
|
||||||
periodComparison: 'Period Comparison',
|
periodComparison: 'Period Comparison',
|
||||||
periods: {
|
periods: {
|
||||||
|
|||||||
@ -22,6 +22,7 @@ export default {
|
|||||||
search: '搜索',
|
search: '搜索',
|
||||||
expandSidebar: '展开侧边栏',
|
expandSidebar: '展开侧边栏',
|
||||||
collapseSidebar: '折叠侧边栏',
|
collapseSidebar: '折叠侧边栏',
|
||||||
|
manageWorkspaces: '管理工作区',
|
||||||
show: '显示',
|
show: '显示',
|
||||||
hide: '隐藏',
|
hide: '隐藏',
|
||||||
on: '开启',
|
on: '开启',
|
||||||
@ -115,6 +116,7 @@ export default {
|
|||||||
loadConversationsFailed: '加载会话列表失败',
|
loadConversationsFailed: '加载会话列表失败',
|
||||||
loadMessagesFailed: '加载消息记录失败',
|
loadMessagesFailed: '加载消息记录失败',
|
||||||
deleteConversationFailed: '删除会话失败',
|
deleteConversationFailed: '删除会话失败',
|
||||||
|
deleteConfirm: '确定删除这个会话?此操作不可恢复。',
|
||||||
switchModelFailed: '切换模型失败',
|
switchModelFailed: '切换模型失败',
|
||||||
uploadFailed: '文件上传失败',
|
uploadFailed: '文件上传失败',
|
||||||
dropToUpload: '拖放文件或文件夹到此处',
|
dropToUpload: '拖放文件或文件夹到此处',
|
||||||
@ -597,6 +599,10 @@ export default {
|
|||||||
planExecute: 'Plan-Execute',
|
planExecute: 'Plan-Execute',
|
||||||
enabled: '已启用',
|
enabled: '已启用',
|
||||||
disabled: '已停用',
|
disabled: '已停用',
|
||||||
|
basic: '基本信息',
|
||||||
|
skills: '技能',
|
||||||
|
tools: '工具',
|
||||||
|
context: '上下文',
|
||||||
},
|
},
|
||||||
columns: {
|
columns: {
|
||||||
name: '名称',
|
name: '名称',
|
||||||
@ -656,6 +662,14 @@ export default {
|
|||||||
toggleFailed: '切换状态失败',
|
toggleFailed: '切换状态失败',
|
||||||
toggleSuccess: '状态已更新',
|
toggleSuccess: '状态已更新',
|
||||||
},
|
},
|
||||||
|
binding: {
|
||||||
|
skillsHint: '选择此智能体可使用的技能。留空则使用所有已启用的技能。',
|
||||||
|
toolsHint: '选择此智能体可使用的工具。留空则使用所有已启用的工具。',
|
||||||
|
noSkills: '暂无可用技能',
|
||||||
|
noTools: '暂无可用工具',
|
||||||
|
contextHint: '管理此智能体的上下文文件(如 AGENT.md),定义智能体的行为、知识和指令。',
|
||||||
|
goToContext: '前往编辑上下文',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
security: {
|
security: {
|
||||||
title: '安全管理',
|
title: '安全管理',
|
||||||
@ -727,6 +741,7 @@ export default {
|
|||||||
workspaces: {
|
workspaces: {
|
||||||
title: '工作区管理',
|
title: '工作区管理',
|
||||||
desc: '管理组织的工作区。',
|
desc: '管理组织的工作区。',
|
||||||
|
manage: '管理工作区',
|
||||||
newWorkspace: '新建工作区',
|
newWorkspace: '新建工作区',
|
||||||
loading: '加载中...',
|
loading: '加载中...',
|
||||||
noWorkspaces: '暂无工作区。',
|
noWorkspaces: '暂无工作区。',
|
||||||
@ -1537,8 +1552,8 @@ export default {
|
|||||||
periodDesc: '从日、周、月三个维度观察系统运行状况。',
|
periodDesc: '从日、周、月三个维度观察系统运行状况。',
|
||||||
runsDesc: '定时任务执行记录,包含耗时、消耗和结果。',
|
runsDesc: '定时任务执行记录,包含耗时、消耗和结果。',
|
||||||
trend: {
|
trend: {
|
||||||
title: '7 天趋势',
|
title: '本周活跃度',
|
||||||
subtitle: '过去一周的消息量和 Token 消耗趋势。',
|
subtitle: '你和 AI 的互动频率。',
|
||||||
},
|
},
|
||||||
periodComparison: '周期对比',
|
periodComparison: '周期对比',
|
||||||
periods: {
|
periods: {
|
||||||
|
|||||||
@ -207,6 +207,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { agentApi, agentContextApi } from '@/api/index'
|
import { agentApi, agentContextApi } from '@/api/index'
|
||||||
@ -262,8 +263,15 @@ const sortedFiles = computed(() => {
|
|||||||
|
|
||||||
const renderedMarkdown = computed(() => renderMarkdown(fileContent.value || ''))
|
const renderedMarkdown = computed(() => renderMarkdown(fileContent.value || ''))
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadAgents()
|
await loadAgents()
|
||||||
|
// Hydrate from query param (e.g. from Agents page "Context" tab link)
|
||||||
|
const qAgentId = route.query.agentId
|
||||||
|
if (qAgentId && agents.value.some(a => String(a.id) === String(qAgentId))) {
|
||||||
|
selectedAgentId.value = qAgentId as string
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(selectedAgentId, () => {
|
watch(selectedAgentId, () => {
|
||||||
|
|||||||
@ -33,50 +33,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Agent table -->
|
<!-- Agent card grid -->
|
||||||
<div class="table-wrap mc-surface-card" v-if="filteredAgents.length > 0">
|
<div class="agent-grid" v-if="filteredAgents.length > 0">
|
||||||
<table class="agent-table">
|
<div
|
||||||
<thead>
|
v-for="agent in filteredAgents"
|
||||||
<tr>
|
:key="agent.id"
|
||||||
<th class="col-name">{{ t('agents.columns.name') }}</th>
|
class="agent-card mc-surface-card"
|
||||||
<th class="col-type">{{ t('agents.columns.agentType') }}</th>
|
:class="{ 'agent-card--disabled': !agent.enabled }"
|
||||||
<th class="col-tags">{{ t('agents.columns.tags') }}</th>
|
>
|
||||||
<th class="col-status">{{ t('agents.columns.enabled') }}</th>
|
<div class="agent-card__header">
|
||||||
<th class="col-time">{{ t('agents.columns.updateTime') }}</th>
|
<span class="agent-card__icon">{{ agent.icon || '🤖' }}</span>
|
||||||
<th class="col-actions">{{ t('agents.columns.actions') }}</th>
|
<label class="toggle-switch toggle-switch--sm">
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="agent in filteredAgents" :key="agent.id" :class="{ 'row-disabled': !agent.enabled }">
|
|
||||||
<td class="col-name">
|
|
||||||
<div class="agent-name-cell">
|
|
||||||
<span class="agent-icon">{{ agent.icon || '🤖' }}</span>
|
|
||||||
<div class="agent-name-info">
|
|
||||||
<span class="agent-name">{{ agent.name }}</span>
|
|
||||||
<span class="agent-desc">{{ agent.description || t('agents.messages.noDescription') }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="col-type">
|
|
||||||
<span class="tag type-tag">{{ agent.agentType === 'react' ? 'ReAct' : 'Plan-Execute' }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="col-tags">
|
|
||||||
<div class="tags-cell" v-if="agent.tags">
|
|
||||||
<span v-for="tag in parseTags(agent.tags)" :key="tag" class="tag tag-item">{{ tag }}</span>
|
|
||||||
</div>
|
|
||||||
<span v-else class="text-muted">-</span>
|
|
||||||
</td>
|
|
||||||
<td class="col-status">
|
|
||||||
<label class="toggle-switch">
|
|
||||||
<input type="checkbox" :checked="agent.enabled" @change="toggleAgent(agent)" />
|
<input type="checkbox" :checked="agent.enabled" @change="toggleAgent(agent)" />
|
||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</td>
|
</div>
|
||||||
<td class="col-time">
|
<div class="agent-card__body">
|
||||||
|
<h3 class="agent-card__name">{{ agent.name }}</h3>
|
||||||
|
<p class="agent-card__desc">{{ agent.description || t('agents.messages.noDescription') }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="agent-card__meta">
|
||||||
|
<span class="tag type-tag">{{ agent.agentType === 'react' ? 'ReAct' : 'Plan-Execute' }}</span>
|
||||||
|
<div class="tags-cell" v-if="agent.tags">
|
||||||
|
<span v-for="tag in parseTags(agent.tags)" :key="tag" class="tag tag-item">{{ tag }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="agent-card__footer">
|
||||||
<span class="time-label">{{ formatTime(agent.updateTime) }}</span>
|
<span class="time-label">{{ formatTime(agent.updateTime) }}</span>
|
||||||
</td>
|
<div class="agent-card__actions">
|
||||||
<td class="col-actions">
|
<button class="action-btn" :title="t('agents.tabs.context')" @click="goToAgentContextFor(agent)">
|
||||||
<div class="action-btns">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button class="action-btn" :title="t('agents.actions.edit')" @click="openEditModal(agent)">
|
<button class="action-btn" :title="t('agents.actions.edit')" @click="openEditModal(agent)">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||||
@ -90,10 +79,8 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Empty state -->
|
<!-- Empty state -->
|
||||||
@ -221,8 +208,8 @@
|
|||||||
|
|
||||||
<!-- Skills Tab -->
|
<!-- Skills Tab -->
|
||||||
<div v-if="modalTab === 'skills'" class="binding-tab">
|
<div v-if="modalTab === 'skills'" class="binding-tab">
|
||||||
<p class="binding-hint">{{ t('agents.binding.skillsHint', 'Select skills this agent can use. Leave empty to use all enabled skills.') }}</p>
|
<p class="binding-hint">{{ t('agents.binding.skillsHint') }}</p>
|
||||||
<div v-if="availableSkills.length === 0" class="binding-empty">{{ t('agents.binding.noSkills', 'No skills available') }}</div>
|
<div v-if="availableSkills.length === 0" class="binding-empty">{{ t('agents.binding.noSkills') }}</div>
|
||||||
<div v-else class="binding-list">
|
<div v-else class="binding-list">
|
||||||
<label
|
<label
|
||||||
v-for="skill in availableSkills"
|
v-for="skill in availableSkills"
|
||||||
@ -243,8 +230,8 @@
|
|||||||
|
|
||||||
<!-- Tools Tab -->
|
<!-- Tools Tab -->
|
||||||
<div v-if="modalTab === 'tools'" class="binding-tab">
|
<div v-if="modalTab === 'tools'" class="binding-tab">
|
||||||
<p class="binding-hint">{{ t('agents.binding.toolsHint', 'Select tools this agent can use. Leave empty to use all enabled tools.') }}</p>
|
<p class="binding-hint">{{ t('agents.binding.toolsHint') }}</p>
|
||||||
<div v-if="availableTools.length === 0" class="binding-empty">{{ t('agents.binding.noTools', 'No tools available') }}</div>
|
<div v-if="availableTools.length === 0" class="binding-empty">{{ t('agents.binding.noTools') }}</div>
|
||||||
<div v-else class="binding-list">
|
<div v-else class="binding-list">
|
||||||
<label
|
<label
|
||||||
v-for="tool in availableTools"
|
v-for="tool in availableTools"
|
||||||
@ -276,11 +263,13 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { agentApi, agentBindingApi, skillApi, toolApi, templateApi } from '@/api/index'
|
import { agentApi, agentBindingApi, skillApi, toolApi, templateApi } from '@/api/index'
|
||||||
import type { Agent } from '@/types/index'
|
import type { Agent } from '@/types/index'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const agents = ref<Agent[]>([])
|
const agents = ref<Agent[]>([])
|
||||||
const searchText = ref('')
|
const searchText = ref('')
|
||||||
@ -485,6 +474,16 @@ async function deleteAgent(agent: Agent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goToAgentContext() {
|
||||||
|
const agentId = editingAgent.value?.id
|
||||||
|
closeModal()
|
||||||
|
router.push({ path: '/settings/agent-context', query: agentId ? { agentId: String(agentId) } : {} })
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToAgentContextFor(agent: Agent) {
|
||||||
|
router.push({ path: '/settings/agent-context', query: { agentId: String(agent.id) } })
|
||||||
|
}
|
||||||
|
|
||||||
async function toggleAgent(agent: Agent) {
|
async function toggleAgent(agent: Agent) {
|
||||||
try {
|
try {
|
||||||
await agentApi.update(agent.id, { ...agent, enabled: !agent.enabled })
|
await agentApi.update(agent.id, { ...agent, enabled: !agent.enabled })
|
||||||
@ -515,27 +514,122 @@ async function toggleAgent(agent: Agent) {
|
|||||||
.filter-tab:hover { background: var(--mc-bg-sunken); }
|
.filter-tab:hover { background: var(--mc-bg-sunken); }
|
||||||
.filter-tab.active { background: var(--mc-primary-bg); border-color: var(--mc-primary); color: var(--mc-primary); font-weight: 500; }
|
.filter-tab.active { background: var(--mc-primary-bg); border-color: var(--mc-primary); color: var(--mc-primary); font-weight: 500; }
|
||||||
|
|
||||||
/* Table */
|
/* Agent Card Grid */
|
||||||
.table-wrap { overflow-x: auto; }
|
.agent-grid {
|
||||||
.agent-table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
display: grid;
|
||||||
.agent-table th { padding: 14px 16px; text-align: left; font-weight: 700; font-size: 12px; color: var(--mc-text-secondary); background: var(--mc-bg-muted); border-bottom: 1px solid var(--mc-border); white-space: nowrap; text-transform: uppercase; letter-spacing: 0.08em; }
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
.agent-table td { padding: 12px 16px; border-bottom: 1px solid var(--mc-border-light); vertical-align: middle; }
|
gap: 16px;
|
||||||
.agent-table tbody tr:last-child td { border-bottom: none; }
|
}
|
||||||
.agent-table tbody tr:hover { background: var(--mc-bg-muted); }
|
|
||||||
.agent-table tbody tr.row-disabled { opacity: 0.55; }
|
|
||||||
|
|
||||||
.col-name { min-width: 200px; }
|
.agent-card {
|
||||||
.col-type { min-width: 110px; }
|
display: flex;
|
||||||
.col-tags { min-width: 120px; }
|
flex-direction: column;
|
||||||
.col-status { min-width: 80px; }
|
gap: 12px;
|
||||||
.col-time { min-width: 140px; }
|
padding: 20px;
|
||||||
.col-actions { min-width: 90px; }
|
transition: all 0.15s;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
.agent-name-cell { display: flex; align-items: center; gap: 10px; }
|
.agent-card:hover {
|
||||||
.agent-icon { font-size: 20px; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; background: var(--mc-primary-bg); border-radius: 8px; flex-shrink: 0; }
|
transform: translateY(-2px);
|
||||||
.agent-name-info { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
||||||
.agent-name { font-weight: 600; color: var(--mc-text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
}
|
||||||
.agent-desc { font-size: 12px; color: var(--mc-text-tertiary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 200px; }
|
|
||||||
|
.agent-card--disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-card__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-card__icon {
|
||||||
|
font-size: 36px;
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--mc-primary-bg);
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-card__body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-card__name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--mc-text-primary);
|
||||||
|
margin: 0 0 4px;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-card__desc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--mc-text-tertiary);
|
||||||
|
margin: 0;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-card__meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-card__footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid var(--mc-border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-card__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-card:hover .agent-card__actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch--sm { width: 32px; height: 18px; }
|
||||||
|
.toggle-switch--sm .toggle-slider::before { width: 12px; height: 12px; }
|
||||||
|
.toggle-switch--sm input:checked + .toggle-slider::before { transform: translateX(14px); }
|
||||||
|
|
||||||
|
/* Context link card */
|
||||||
|
.context-link-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid var(--mc-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
.context-link-card:hover {
|
||||||
|
border-color: var(--mc-primary);
|
||||||
|
background: var(--mc-primary-bg);
|
||||||
|
}
|
||||||
|
.context-link-card__icon { font-size: 28px; }
|
||||||
|
.context-link-card__info { flex: 1; display: flex; flex-direction: column; gap: 2px; }
|
||||||
|
.context-link-card__title { font-size: 14px; font-weight: 600; color: var(--mc-text-primary); }
|
||||||
|
.context-link-card__desc { font-size: 12px; color: var(--mc-text-tertiary); }
|
||||||
|
.context-link-card__arrow { color: var(--mc-text-tertiary); flex-shrink: 0; }
|
||||||
|
|
||||||
.tag { padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 500; }
|
.tag { padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 500; }
|
||||||
.type-tag { background: var(--mc-primary-bg); color: var(--mc-primary); }
|
.type-tag { background: var(--mc-primary-bg); color: var(--mc-primary); }
|
||||||
|
|||||||
@ -14,18 +14,41 @@
|
|||||||
<div class="panel-kicker">{{ $t('nav.chat') }}</div>
|
<div class="panel-kicker">{{ $t('nav.chat') }}</div>
|
||||||
<h2 class="panel-title">{{ $t('chat.conversations') }}</h2>
|
<h2 class="panel-title">{{ $t('chat.conversations') }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<button class="new-chat-btn" @click="newConversation" :title="$t('chat.newChat')">
|
<button class="new-chat-btn" @click="newConversation" :title="`${$t('chat.newChat')} (⌘N)`">
|
||||||
<el-icon><Plus /></el-icon>
|
<el-icon><Plus /></el-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="agent-selector">
|
<div class="agent-selector">
|
||||||
<select v-model="selectedAgentId" class="agent-select" @change="onAgentChange">
|
<button class="agent-select-trigger" @click="agentDropdownOpen = !agentDropdownOpen" :title="`${$t('chat.selectAgent')} (⌘K)`">
|
||||||
<option v-if="agents.length === 0" value="">{{ $t('chat.loadingAgents') }}</option>
|
<span class="agent-select-trigger__icon">{{ currentAgent?.icon || '🤖' }}</span>
|
||||||
<option v-for="agent in agents" :key="agent.id" :value="agent.id">
|
<span class="agent-select-trigger__name">{{ currentAgent?.name || $t('chat.selectAgent') }}</span>
|
||||||
{{ agent.icon || '🤖' }} {{ agent.name }}
|
<svg class="agent-select-trigger__arrow" :class="{ open: agentDropdownOpen }" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
||||||
</option>
|
</button>
|
||||||
</select>
|
<Transition name="fade">
|
||||||
|
<div v-if="agentDropdownOpen" class="agent-dropdown-backdrop" @click="agentDropdownOpen = false"></div>
|
||||||
|
</Transition>
|
||||||
|
<Transition name="agent-dropdown">
|
||||||
|
<div v-if="agentDropdownOpen" class="agent-dropdown">
|
||||||
|
<div
|
||||||
|
v-for="agent in agents"
|
||||||
|
:key="agent.id"
|
||||||
|
class="agent-dropdown-item"
|
||||||
|
:class="{ active: String(agent.id) === String(selectedAgentId) }"
|
||||||
|
@click="selectAgent(agent)"
|
||||||
|
>
|
||||||
|
<span class="agent-dropdown-item__icon">{{ agent.icon || '🤖' }}</span>
|
||||||
|
<div class="agent-dropdown-item__info">
|
||||||
|
<span class="agent-dropdown-item__name">{{ agent.name }}</span>
|
||||||
|
<span class="agent-dropdown-item__desc">{{ agent.description || agent.agentType }}</span>
|
||||||
|
</div>
|
||||||
|
<span v-if="String(agent.id) === String(selectedAgentId)" class="agent-dropdown-item__check">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="agents.length === 0" class="agent-dropdown-empty">{{ $t('chat.loadingAgents') }}</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="conversation-list">
|
<div class="conversation-list">
|
||||||
@ -42,14 +65,24 @@
|
|||||||
<img :src="channelIconUrl(conv.source)" width="14" height="14" alt="" />
|
<img :src="channelIconUrl(conv.source)" width="14" height="14" alt="" />
|
||||||
</div>
|
</div>
|
||||||
<div class="conv-info">
|
<div class="conv-info">
|
||||||
<div class="conv-title">{{ conv.title }}</div>
|
<input
|
||||||
|
v-if="renamingConvId === conv.conversationId"
|
||||||
|
v-model="renameText"
|
||||||
|
class="conv-title-input"
|
||||||
|
@keydown.enter="confirmRename(conv)"
|
||||||
|
@keydown.escape="cancelRename"
|
||||||
|
@blur="confirmRename(conv)"
|
||||||
|
@click.stop
|
||||||
|
ref="renameInputRef"
|
||||||
|
/>
|
||||||
|
<div v-else class="conv-title" @dblclick.stop="startRename(conv)">{{ conv.title }}</div>
|
||||||
<div class="conv-meta">
|
<div class="conv-meta">
|
||||||
<span>{{ $t('chat.messages', { count: conv.messageCount }) }}</span>
|
<span>{{ $t('chat.messages', { count: conv.messageCount }) }}</span>
|
||||||
<span class="conv-dot">·</span>
|
<span class="conv-dot">·</span>
|
||||||
<span>{{ formatConversationTime(conv.lastActiveTime) }}</span>
|
<span>{{ formatConversationTime(conv.lastActiveTime) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="conv-delete" @click.stop="deleteConversation(conv.conversationId)" :title="$t('common.delete')">
|
<button class="conv-delete" @click.stop="confirmDeleteConversation(conv.conversationId)" :title="$t('common.delete')">
|
||||||
<el-icon><Delete /></el-icon>
|
<el-icon><Delete /></el-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -97,23 +130,57 @@
|
|||||||
<div v-else class="no-agent-hint">{{ $t('chat.selectAgent') }}</div>
|
<div v-else class="no-agent-hint">{{ $t('chat.selectAgent') }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-header-right">
|
<div class="chat-header-right">
|
||||||
<select
|
<!-- Model selector -->
|
||||||
v-if="eligibleModels.length > 0"
|
<div v-if="eligibleModels.length > 0" class="model-selector-wrap">
|
||||||
:value="activeModelValue"
|
<button class="model-select-trigger" :disabled="modelSaving" @click="modelDropdownOpen = !modelDropdownOpen">
|
||||||
class="model-select"
|
<span class="model-select-trigger__name">{{ activeModelLabel || $t('chat.configModel') }}</span>
|
||||||
:disabled="modelSaving"
|
<svg class="model-select-trigger__arrow" :class="{ open: modelDropdownOpen }" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
||||||
@change="onModelChange"
|
</button>
|
||||||
>
|
<Transition name="fade">
|
||||||
<option v-for="item in eligibleModels" :key="item.value" :value="item.value">
|
<div v-if="modelDropdownOpen" class="model-dropdown-backdrop" @click="modelDropdownOpen = false"></div>
|
||||||
{{ item.label }}
|
</Transition>
|
||||||
</option>
|
<Transition name="agent-dropdown">
|
||||||
</select>
|
<div v-if="modelDropdownOpen" class="model-dropdown">
|
||||||
|
<div
|
||||||
|
v-for="item in eligibleModels"
|
||||||
|
:key="item.value"
|
||||||
|
class="model-dropdown-item"
|
||||||
|
:class="{ active: item.value === activeModelValue }"
|
||||||
|
@click="selectModel(item.value)"
|
||||||
|
>
|
||||||
|
<span class="model-dropdown-item__name">{{ item.label }}</span>
|
||||||
|
<span v-if="item.value === activeModelValue" class="model-dropdown-item__check">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
<button v-else class="header-btn" @click="goToModelSettings" :title="$t('chat.configModel')">
|
<button v-else class="header-btn" @click="goToModelSettings" :title="$t('chat.configModel')">
|
||||||
<el-icon><Setting /></el-icon>
|
<el-icon><Setting /></el-icon>
|
||||||
</button>
|
</button>
|
||||||
<button class="header-btn" @click="clearMessages" :title="$t('chat.clearMessages')">
|
<!-- Overflow menu -->
|
||||||
<el-icon><Delete /></el-icon>
|
<div class="header-overflow-wrap">
|
||||||
</button>
|
<button class="header-btn" @click="headerMenuOpen = !headerMenuOpen" :title="$t('common.more') || 'More'">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg>
|
||||||
|
</button>
|
||||||
|
<Transition name="fade">
|
||||||
|
<div v-if="headerMenuOpen" class="header-menu-backdrop" @click="headerMenuOpen = false"></div>
|
||||||
|
</Transition>
|
||||||
|
<Transition name="agent-dropdown">
|
||||||
|
<div v-if="headerMenuOpen" class="header-menu">
|
||||||
|
<button class="header-menu-item" @click="headerMenuOpen = false; goToModelSettings()">
|
||||||
|
<el-icon><Setting /></el-icon>
|
||||||
|
<span>{{ $t('chat.configModel') }}</span>
|
||||||
|
</button>
|
||||||
|
<div class="header-menu-divider"></div>
|
||||||
|
<button class="header-menu-item header-menu-item--danger" @click="handleClearMessages">
|
||||||
|
<el-icon><Delete /></el-icon>
|
||||||
|
<span>{{ $t('chat.clearMessages') }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -162,7 +229,7 @@
|
|||||||
:loading="isGenerating && !hasPendingApproval"
|
:loading="isGenerating && !hasPendingApproval"
|
||||||
:disabled="showModelPrompt || !currentAgent"
|
:disabled="showModelPrompt || !currentAgent"
|
||||||
:placeholder="$t('chat.messagePlaceholder')"
|
:placeholder="$t('chat.messagePlaceholder')"
|
||||||
:hint="`MateClaw · ${currentRuntimeModel}` + (currentConversationId ? ` · Session · ${currentConversationId.slice(0, 8)}...` : '')"
|
:hint="currentRuntimeModel"
|
||||||
:attachments="pendingAttachments"
|
:attachments="pendingAttachments"
|
||||||
:uploading="uploadingAttachment"
|
:uploading="uploadingAttachment"
|
||||||
:max-length="10240"
|
:max-length="10240"
|
||||||
@ -195,10 +262,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { ChatDotRound, Delete, Plus, Setting, UploadFilled } from '@element-plus/icons-vue'
|
import { ChatDotRound, Delete, Plus, Setting, UploadFilled } from '@element-plus/icons-vue'
|
||||||
import { conversationApi, agentApi, modelApi, chatApi } from '@/api/index'
|
import { conversationApi, agentApi, modelApi, chatApi } from '@/api/index'
|
||||||
import { channelIconUrl } from '@/utils/channelSource'
|
import { channelIconUrl } from '@/utils/channelSource'
|
||||||
@ -227,12 +294,30 @@ function handleMobileChange(e: MediaQueryListEvent | MediaQueryList) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============ 配置和常量 ============
|
// ============ 配置和常量 ============
|
||||||
const suggestions = computed(() => [
|
const suggestions = computed(() => {
|
||||||
t('chat.suggestionIntro'),
|
const agent = currentAgent.value
|
||||||
t('chat.suggestionPoem'),
|
// If agent has custom suggestions (stored as newline-separated string in description etc.)
|
||||||
t('chat.suggestionCode'),
|
const agentSuggestions = (agent as any)?.suggestions as string | undefined
|
||||||
t('chat.suggestionWeather'),
|
if (agentSuggestions) {
|
||||||
])
|
const parsed = agentSuggestions.split('\n').filter(Boolean).slice(0, 4)
|
||||||
|
if (parsed.length) return parsed
|
||||||
|
}
|
||||||
|
// Agent-type-aware defaults
|
||||||
|
if (agent?.agentType === 'plan_execute') {
|
||||||
|
return [
|
||||||
|
t('chat.suggestionPlan1', '帮我制定一个完整的项目计划'),
|
||||||
|
t('chat.suggestionPlan2', '分步骤帮我完成一个复杂任务'),
|
||||||
|
t('chat.suggestionIntro'),
|
||||||
|
t('chat.suggestionWeather'),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
t('chat.suggestionIntro'),
|
||||||
|
t('chat.suggestionPoem'),
|
||||||
|
t('chat.suggestionCode'),
|
||||||
|
t('chat.suggestionWeather'),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
// ============ 状态 ============
|
// ============ 状态 ============
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -252,6 +337,80 @@ const activeModels = ref<ActiveModelsInfo | null>(null)
|
|||||||
const pendingAttachments = ref<ChatAttachment[]>([])
|
const pendingAttachments = ref<ChatAttachment[]>([])
|
||||||
const uploadingAttachment = ref(false)
|
const uploadingAttachment = ref(false)
|
||||||
|
|
||||||
|
// Dropdowns & menus
|
||||||
|
const agentDropdownOpen = ref(false)
|
||||||
|
const modelDropdownOpen = ref(false)
|
||||||
|
const headerMenuOpen = ref(false)
|
||||||
|
|
||||||
|
function selectAgent(agent: Agent) {
|
||||||
|
agentDropdownOpen.value = false
|
||||||
|
if (String(agent.id) !== String(selectedAgentId.value)) {
|
||||||
|
selectedAgentId.value = agent.id
|
||||||
|
newConversation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectModel(value: string) {
|
||||||
|
modelDropdownOpen.value = false
|
||||||
|
const [providerId, model] = value.split('::')
|
||||||
|
if (!providerId || !model) return
|
||||||
|
modelSaving.value = true
|
||||||
|
try {
|
||||||
|
const res: any = await modelApi.setActive({ providerId, model })
|
||||||
|
activeModels.value = res.data || { activeLlm: { providerId, model } }
|
||||||
|
await loadModelState()
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(t('chat.switchModelFailed'))
|
||||||
|
} finally {
|
||||||
|
modelSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClearMessages() {
|
||||||
|
headerMenuOpen.value = false
|
||||||
|
clearMessages()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conversation rename
|
||||||
|
const renamingConvId = ref('')
|
||||||
|
const renameText = ref('')
|
||||||
|
const renameInputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
|
function startRename(conv: Conversation) {
|
||||||
|
renamingConvId.value = conv.conversationId
|
||||||
|
renameText.value = conv.title || ''
|
||||||
|
nextTick(() => {
|
||||||
|
renameInputRef.value?.focus()
|
||||||
|
renameInputRef.value?.select()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmRename(conv: Conversation) {
|
||||||
|
const newTitle = renameText.value.trim()
|
||||||
|
renamingConvId.value = ''
|
||||||
|
if (!newTitle || newTitle === conv.title) return
|
||||||
|
conv.title = newTitle
|
||||||
|
try {
|
||||||
|
await conversationApi.rename(conv.conversationId, newTitle)
|
||||||
|
} catch {
|
||||||
|
// revert on fail — reload
|
||||||
|
await loadConversations()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelRename() {
|
||||||
|
renamingConvId.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete with confirmation
|
||||||
|
function confirmDeleteConversation(conversationId: string) {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
t('chat.deleteConfirm') || 'Delete this conversation?',
|
||||||
|
t('common.confirm'),
|
||||||
|
{ type: 'warning', confirmButtonText: t('common.confirm'), cancelButtonText: t('common.cancel') }
|
||||||
|
).then(() => deleteConversation(conversationId)).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
// 拖拽上传
|
// 拖拽上传
|
||||||
const isDragging = ref(false)
|
const isDragging = ref(false)
|
||||||
let dragCounter = 0
|
let dragCounter = 0
|
||||||
@ -450,6 +609,12 @@ const activeModelValue = computed(() => {
|
|||||||
return providerId && model ? `${providerId}::${model}` : ''
|
return providerId && model ? `${providerId}::${model}` : ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const activeModelLabel = computed(() => {
|
||||||
|
if (!activeModelValue.value) return ''
|
||||||
|
const match = eligibleModels.value.find(m => m.value === activeModelValue.value)
|
||||||
|
return match?.label || ''
|
||||||
|
})
|
||||||
|
|
||||||
const activeProvider = computed(() => {
|
const activeProvider = computed(() => {
|
||||||
const providerId = activeModels.value?.activeLlm?.providerId
|
const providerId = activeModels.value?.activeLlm?.providerId
|
||||||
return providerId ? providers.value.find((provider) => provider.id === providerId) || null : null
|
return providerId ? providers.value.find((provider) => provider.id === providerId) || null : null
|
||||||
@ -488,7 +653,21 @@ const eligibleModels = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// ============ 生命周期 ============
|
// ============ 生命周期 ============
|
||||||
|
function handleKeyboardShortcuts(e: KeyboardEvent) {
|
||||||
|
const mod = e.metaKey || e.ctrlKey
|
||||||
|
if (mod && e.key === 'n') {
|
||||||
|
e.preventDefault()
|
||||||
|
newConversation()
|
||||||
|
chatInputRef.value?.focus?.()
|
||||||
|
}
|
||||||
|
if (mod && e.key === 'k') {
|
||||||
|
e.preventDefault()
|
||||||
|
agentDropdownOpen.value = !agentDropdownOpen.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
document.addEventListener('keydown', handleKeyboardShortcuts)
|
||||||
document.addEventListener('click', handleCodeCopy)
|
document.addEventListener('click', handleCodeCopy)
|
||||||
startECharts()
|
startECharts()
|
||||||
mobileQuery = window.matchMedia('(max-width: 768px)')
|
mobileQuery = window.matchMedia('(max-width: 768px)')
|
||||||
@ -499,6 +678,7 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener('keydown', handleKeyboardShortcuts)
|
||||||
document.removeEventListener('click', handleCodeCopy)
|
document.removeEventListener('click', handleCodeCopy)
|
||||||
disposeECharts()
|
disposeECharts()
|
||||||
mobileQuery?.removeEventListener('change', handleMobileChange)
|
mobileQuery?.removeEventListener('change', handleMobileChange)
|
||||||
@ -701,26 +881,9 @@ async function clearMessages() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAgentChange() {
|
// onAgentChange removed — replaced by selectAgent()
|
||||||
newConversation()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onModelChange(event: Event) {
|
// onModelChange removed — replaced by selectModel()
|
||||||
const value = (event.target as HTMLSelectElement).value
|
|
||||||
const [providerId, model] = value.split('::')
|
|
||||||
if (!providerId || !model) return
|
|
||||||
|
|
||||||
modelSaving.value = true
|
|
||||||
try {
|
|
||||||
const res: any = await modelApi.setActive({ providerId, model })
|
|
||||||
activeModels.value = res.data || { activeLlm: { providerId, model } }
|
|
||||||
await loadModelState()
|
|
||||||
} catch (e) {
|
|
||||||
ElMessage.error(t('chat.switchModelFailed'))
|
|
||||||
} finally {
|
|
||||||
modelSaving.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function goToModelSettings() {
|
function goToModelSettings() {
|
||||||
router.push('/settings/models')
|
router.push('/settings/models')
|
||||||
@ -1219,11 +1382,15 @@ function handleCodeCopy(e: MouseEvent) {
|
|||||||
.agent-selector {
|
.agent-selector {
|
||||||
padding: 10px 12px 12px;
|
padding: 10px 12px 12px;
|
||||||
border-bottom: 1px solid var(--mc-border-light);
|
border-bottom: 1px solid var(--mc-border-light);
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-select {
|
.agent-select-trigger {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 9px 12px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
border: 1px solid var(--mc-border);
|
border: 1px solid var(--mc-border);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@ -1231,11 +1398,132 @@ function handleCodeCopy(e: MouseEvent) {
|
|||||||
background: var(--mc-bg-sunken);
|
background: var(--mc-bg-sunken);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
transition: all 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-select:focus {
|
.agent-select-trigger:hover {
|
||||||
border-color: var(--mc-primary);
|
border-color: var(--mc-primary);
|
||||||
box-shadow: 0 0 0 2px rgba(217, 119, 87, 0.1);
|
background: var(--mc-bg-elevated);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-select-trigger__icon {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-select-trigger__name {
|
||||||
|
flex: 1;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-select-trigger__arrow {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--mc-text-tertiary);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-select-trigger__arrow.open {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-dropdown-backdrop,
|
||||||
|
.model-dropdown-backdrop,
|
||||||
|
.header-menu-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 4px);
|
||||||
|
left: 12px;
|
||||||
|
right: 12px;
|
||||||
|
z-index: 100;
|
||||||
|
background: var(--mc-bg-elevated);
|
||||||
|
border: 1px solid var(--mc-border);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 6px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||||
|
max-height: 320px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-dropdown-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-dropdown-item:hover {
|
||||||
|
background: var(--mc-bg-sunken);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-dropdown-item.active {
|
||||||
|
background: var(--mc-primary-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-dropdown-item__icon {
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-dropdown-item__info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-dropdown-item__name {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--mc-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-dropdown-item__desc {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--mc-text-tertiary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-dropdown-item__check {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--mc-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-dropdown-empty {
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--mc-text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-dropdown-enter-active {
|
||||||
|
transition: all 0.15s ease-out;
|
||||||
|
}
|
||||||
|
.agent-dropdown-leave-active {
|
||||||
|
transition: all 0.1s ease-in;
|
||||||
|
}
|
||||||
|
.agent-dropdown-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-6px) scale(0.97);
|
||||||
|
}
|
||||||
|
.agent-dropdown-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-4px) scale(0.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversation-list {
|
.conversation-list {
|
||||||
@ -1316,6 +1604,19 @@ function handleCodeCopy(e: MouseEvent) {
|
|||||||
color: var(--mc-text-tertiary);
|
color: var(--mc-text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.conv-title-input {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--mc-text-primary);
|
||||||
|
background: var(--mc-bg-elevated);
|
||||||
|
border: 1px solid var(--mc-primary);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px rgba(217, 119, 87, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
.conv-delete {
|
.conv-delete {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
width: 22px;
|
width: 22px;
|
||||||
@ -1478,15 +1779,142 @@ function handleCodeCopy(e: MouseEvent) {
|
|||||||
color: var(--mc-text-tertiary);
|
color: var(--mc-text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-select {
|
/* Model selector */
|
||||||
min-width: 260px;
|
.model-selector-wrap {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-select-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
height: 34px;
|
height: 34px;
|
||||||
|
padding: 0 12px;
|
||||||
border: 1px solid var(--mc-border);
|
border: 1px solid var(--mc-border);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: var(--mc-panel-raised);
|
background: var(--mc-panel-raised);
|
||||||
color: var(--mc-text-primary);
|
color: var(--mc-text-primary);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
padding: 0 12px;
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-select-trigger:hover {
|
||||||
|
border-color: var(--mc-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-select-trigger:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-select-trigger__name {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-select-trigger__arrow {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--mc-text-tertiary);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-select-trigger__arrow.open {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 4px);
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
min-width: 260px;
|
||||||
|
background: var(--mc-bg-elevated);
|
||||||
|
border: 1px solid var(--mc-border);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 6px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||||
|
max-height: 360px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-dropdown-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-dropdown-item:hover {
|
||||||
|
background: var(--mc-bg-sunken);
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-dropdown-item.active {
|
||||||
|
background: var(--mc-primary-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-dropdown-item__name {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--mc-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-dropdown-item__check {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--mc-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header overflow menu */
|
||||||
|
.header-overflow-wrap {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 4px);
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
min-width: 180px;
|
||||||
|
background: var(--mc-bg-elevated);
|
||||||
|
border: 1px solid var(--mc-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 4px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 9px 12px;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--mc-text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-menu-item:hover {
|
||||||
|
background: var(--mc-bg-sunken);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-menu-item--danger:hover {
|
||||||
|
background: var(--mc-danger-bg);
|
||||||
|
color: var(--mc-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-menu-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: var(--mc-border-light);
|
||||||
|
margin: 2px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-btn {
|
.header-btn {
|
||||||
@ -1628,10 +2056,12 @@ function handleCodeCopy(e: MouseEvent) {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-select {
|
.model-select-trigger {
|
||||||
min-width: 0;
|
|
||||||
max-width: 160px;
|
max-width: 160px;
|
||||||
flex: 1;
|
}
|
||||||
|
|
||||||
|
.model-dropdown {
|
||||||
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drop-overlay__content {
|
.drop-overlay__content {
|
||||||
|
|||||||
@ -10,14 +10,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="hero-note mc-surface-card">
|
<div class="hero-note mc-surface-card">
|
||||||
<div class="hero-note__label">{{ t('dashboard.periods.today') }}</div>
|
<div class="hero-note__label">{{ t('dashboard.periods.today') }}</div>
|
||||||
<div class="hero-note__value">{{ formatTokens(todayStats.totalTokens) }}</div>
|
<div class="hero-note__value">{{ todayStats.conversations }}</div>
|
||||||
<div class="hero-note__meta">{{ t('dashboard.tokens') }} · {{ todayStats.toolCalls }} {{ t('dashboard.toolCalls') }}</div>
|
<div class="hero-note__meta">{{ t('dashboard.conversations') }} · {{ todayStats.messages }} {{ t('dashboard.messages') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dashboard-body">
|
<div class="dashboard-body">
|
||||||
<div class="stats-grid">
|
<div class="stats-grid">
|
||||||
<div class="stat-card mc-surface-card">
|
<div class="stat-card mc-surface-card stat-card--primary">
|
||||||
<div class="stat-icon">
|
<div class="stat-icon">
|
||||||
<el-icon><ChatDotRound /></el-icon>
|
<el-icon><ChatDotRound /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
@ -26,7 +26,7 @@
|
|||||||
<div class="stat-label">{{ t('dashboard.conversations') }}</div>
|
<div class="stat-label">{{ t('dashboard.conversations') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card mc-surface-card">
|
<div class="stat-card mc-surface-card stat-card--primary">
|
||||||
<div class="stat-icon">
|
<div class="stat-icon">
|
||||||
<el-icon><Document /></el-icon>
|
<el-icon><Document /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
@ -35,16 +35,7 @@
|
|||||||
<div class="stat-label">{{ t('dashboard.messages') }}</div>
|
<div class="stat-label">{{ t('dashboard.messages') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card mc-surface-card">
|
<div class="stat-card mc-surface-card stat-card--secondary">
|
||||||
<div class="stat-icon">
|
|
||||||
<el-icon><DataLine /></el-icon>
|
|
||||||
</div>
|
|
||||||
<div class="stat-body">
|
|
||||||
<div class="stat-value">{{ formatTokens(todayStats.totalTokens) }}</div>
|
|
||||||
<div class="stat-label">{{ t('dashboard.tokens') }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card mc-surface-card">
|
|
||||||
<div class="stat-icon">
|
<div class="stat-icon">
|
||||||
<el-icon><Tools /></el-icon>
|
<el-icon><Tools /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
@ -53,6 +44,15 @@
|
|||||||
<div class="stat-label">{{ t('dashboard.toolCalls') }}</div>
|
<div class="stat-label">{{ t('dashboard.toolCalls') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="stat-card mc-surface-card stat-card--secondary">
|
||||||
|
<div class="stat-icon">
|
||||||
|
<el-icon><DataLine /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-body">
|
||||||
|
<div class="stat-value">{{ formatTokens(todayStats.totalTokens) }}</div>
|
||||||
|
<div class="stat-label">{{ t('dashboard.tokens') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="trendData.length" class="trend-section">
|
<div v-if="trendData.length" class="trend-section">
|
||||||
@ -372,6 +372,10 @@ function calcDuration(run: any): string {
|
|||||||
.stat-value { font-size: 30px; font-weight: 800; color: var(--mc-text-primary); line-height: 1; letter-spacing: -0.05em; }
|
.stat-value { font-size: 30px; font-weight: 800; color: var(--mc-text-primary); line-height: 1; letter-spacing: -0.05em; }
|
||||||
.stat-label { font-size: 12px; color: var(--mc-text-tertiary); margin-top: 6px; text-transform: uppercase; letter-spacing: 0.08em; }
|
.stat-label { font-size: 12px; color: var(--mc-text-tertiary); margin-top: 6px; text-transform: uppercase; letter-spacing: 0.08em; }
|
||||||
|
|
||||||
|
.stat-card--secondary { opacity: 0.75; }
|
||||||
|
.stat-card--secondary .stat-icon { background: linear-gradient(135deg, rgba(148, 163, 184, 0.12), rgba(148, 163, 184, 0.06)); color: var(--mc-text-secondary); }
|
||||||
|
.stat-card--secondary .stat-value { font-size: 24px; }
|
||||||
|
|
||||||
.trend-section { margin-bottom: 22px; }
|
.trend-section { margin-bottom: 22px; }
|
||||||
.trend-chart { padding: 18px; }
|
.trend-chart { padding: 18px; }
|
||||||
.chart-container { width: 100%; height: 240px; }
|
.chart-container { width: 100%; height: 240px; }
|
||||||
|
|||||||
@ -632,16 +632,7 @@ watch(() => workspaceStore.currentWorkspaceId, () => {
|
|||||||
box-shadow: inset 0 0 0 1px rgba(217, 109, 70, 0.08);
|
box-shadow: inset 0 0 0 1px rgba(217, 109, 70, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item.active::before {
|
/* Active indicator bar removed — active state uses bg color + font weight only */
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 8px;
|
|
||||||
bottom: 8px;
|
|
||||||
width: 3px;
|
|
||||||
background: var(--mc-primary);
|
|
||||||
border-radius: 0 3px 3px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-icon { display: flex; align-items: center; flex-shrink: 0; }
|
.nav-icon { display: flex; align-items: center; flex-shrink: 0; }
|
||||||
.nav-label { overflow: hidden; text-overflow: ellipsis; }
|
.nav-label { overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user