mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
refactor(ui): unify employee selector into shared AgentPickerDialog
This commit is contained in:
parent
033854a3ff
commit
09eb28fb63
@ -39,58 +39,13 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ t('channels.fields.bindAgent') }}</label>
|
||||
<!-- Custom dropdown so SkillIcon renders pi:* glyphs; native
|
||||
<select><option> can't host the component. Mirrors the
|
||||
ChannelOnboardingWizard step-3 pattern. -->
|
||||
<div class="agent-select" :class="{ open: agentDropdownOpen }">
|
||||
<button
|
||||
type="button"
|
||||
class="agent-select-trigger"
|
||||
@click="agentDropdownOpen = !agentDropdownOpen"
|
||||
>
|
||||
<span class="agent-select-trigger__icon" :style="{ color: agentIconColor(selectedAgent?.icon) }">
|
||||
<SkillIcon :value="selectedAgent?.icon" :size="20" :fallback="'🤖'" />
|
||||
</span>
|
||||
<span class="agent-select-trigger__name">{{
|
||||
selectedAgent?.name || t('channels.placeholders.selectAgent')
|
||||
}}</span>
|
||||
<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>
|
||||
</button>
|
||||
<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="a in agents"
|
||||
:key="a.id"
|
||||
class="agent-dropdown-item"
|
||||
:class="{ active: String(a.id) === String(form.agentId) }"
|
||||
@click="onSelectAgent(a)"
|
||||
>
|
||||
<span class="agent-dropdown-item__icon" :style="{ color: agentIconColor(a.icon) }">
|
||||
<SkillIcon :value="a.icon" :size="18" :fallback="'🤖'" />
|
||||
</span>
|
||||
<span class="agent-dropdown-item__name">{{ a.name }}</span>
|
||||
<span v-if="String(a.id) === String(form.agentId)" 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>
|
||||
<AgentPickerDialog
|
||||
block
|
||||
:model-value="form.agentId ?? null"
|
||||
:agents="props.agents"
|
||||
:placeholder="t('channels.placeholders.selectAgent')"
|
||||
@update:model-value="(v) => (form.agentId = v ?? undefined)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -487,8 +442,7 @@ import { useWeixinQrcodePoll } from '@/composables/channels/useWeixinQrcodePoll'
|
||||
import { useWecomBotAuth } from '@/composables/channels/useWecomBotAuth'
|
||||
import { useFeishuAppRegister } from '@/composables/channels/useFeishuAppRegister'
|
||||
import { useDingTalkAppRegister } from '@/composables/channels/useDingTalkAppRegister'
|
||||
import SkillIcon from '@/components/common/SkillIcon.vue'
|
||||
import { agentIconColor } from '@/utils/agentIconColor'
|
||||
import AgentPickerDialog from '@/components/common/AgentPickerDialog.vue'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
@ -538,20 +492,6 @@ const showAdvanced = ref(false)
|
||||
const accessControl = ref<AccessControlValue>(defaultAccessControl())
|
||||
const renderConfig = ref<RenderConfigValue>(defaultRenderConfig())
|
||||
|
||||
// ========== Custom agent dropdown ==========
|
||||
// Native <select><option> can't render <SkillIcon> for pi:* glyphs;
|
||||
// mirror the ChatConsole.vue / ChannelOnboardingWizard.vue pattern.
|
||||
const agentDropdownOpen = ref(false)
|
||||
const selectedAgent = computed<Agent | null>(() => {
|
||||
const id = form.value.agentId
|
||||
if (id == null) return null
|
||||
return props.agents.find(a => String(a.id) === String(id)) ?? null
|
||||
})
|
||||
function onSelectAgent(a: Agent) {
|
||||
form.value.agentId = a.id as any
|
||||
agentDropdownOpen.value = false
|
||||
}
|
||||
|
||||
// ========== Auth composables ==========
|
||||
|
||||
const weixin = useWeixinQrcodePoll(({ botToken, baseUrl }) => {
|
||||
@ -1019,69 +959,6 @@ function save() {
|
||||
.json-hint { font-size: 12px; color: var(--mc-text-tertiary); margin: 0 0 8px; }
|
||||
.json-editor { font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace; font-size: 13px; line-height: 1.5; tab-size: 2; }
|
||||
|
||||
/* ===== Agent custom dropdown (mirrors ChatConsole.vue / wizard pattern) =====
|
||||
Native <select><option> can't host <SkillIcon> for pi:* glyphs.
|
||||
z-index has to clear the modal-overlay (1000). */
|
||||
.agent-select { position: relative; }
|
||||
.agent-select-trigger {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--mc-border);
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
color: var(--mc-text-primary);
|
||||
background: var(--mc-bg-elevated);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
transition: border-color 0.15s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.agent-select-trigger:hover { border-color: var(--mc-primary); }
|
||||
.agent-select.open .agent-select-trigger { border-color: var(--mc-primary); box-shadow: 0 0 0 3px rgba(217,119,87,0.12); }
|
||||
.agent-select-trigger__icon { font-size: 18px; line-height: 1; flex-shrink: 0; display: inline-flex; }
|
||||
.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 { position: fixed; inset: 0; z-index: 1099; }
|
||||
.agent-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1100;
|
||||
background: var(--mc-bg-elevated);
|
||||
border: 1px solid var(--mc-border);
|
||||
border-radius: 12px;
|
||||
padding: 6px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.12);
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.agent-dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
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: 20px; line-height: 1; flex-shrink: 0; display: inline-flex; }
|
||||
.agent-dropdown-item__name { flex: 1; font-size: 14px; font-weight: 500; color: var(--mc-text-primary); 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); }
|
||||
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.15s; }
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||
</style>
|
||||
|
||||
@ -219,59 +219,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bind agent (custom dropdown so SkillIcon renders pi:* glyphs;
|
||||
native <select><option> cannot host components) -->
|
||||
<!-- Bind agent -->
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ t('channels.wizard.bindAgentLabel') }}</label>
|
||||
<div class="agent-select" :class="{ open: agentDropdownOpen }">
|
||||
<button
|
||||
type="button"
|
||||
class="agent-select-trigger"
|
||||
@click="agentDropdownOpen = !agentDropdownOpen"
|
||||
>
|
||||
<span class="agent-select-trigger__icon" :style="{ color: agentIconColor(selectedAgent?.icon) }">
|
||||
<SkillIcon :value="selectedAgent?.icon" :size="20" :fallback="'🤖'" />
|
||||
</span>
|
||||
<span class="agent-select-trigger__name">{{
|
||||
selectedAgent?.name || t('channels.placeholders.selectAgent')
|
||||
}}</span>
|
||||
<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>
|
||||
</button>
|
||||
<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="a in agents"
|
||||
:key="a.id"
|
||||
class="agent-dropdown-item"
|
||||
:class="{ active: String(a.id) === String(form.agentId) }"
|
||||
@click="onSelectAgent(a)"
|
||||
>
|
||||
<span class="agent-dropdown-item__icon" :style="{ color: agentIconColor(a.icon) }">
|
||||
<SkillIcon :value="a.icon" :size="18" :fallback="'🤖'" />
|
||||
</span>
|
||||
<span class="agent-dropdown-item__name">{{ a.name }}</span>
|
||||
<span v-if="String(a.id) === String(form.agentId)" 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>
|
||||
<AgentPickerDialog
|
||||
block
|
||||
:model-value="form.agentId ?? null"
|
||||
:agents="props.agents"
|
||||
:placeholder="t('channels.placeholders.selectAgent')"
|
||||
@update:model-value="(v) => (form.agentId = v ?? undefined)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="ready-hint">{{ t('channels.wizard.sendTestHint', { service: serviceName }) }}</p>
|
||||
@ -324,8 +281,7 @@ import { useWecomBotAuth } from '@/composables/channels/useWecomBotAuth'
|
||||
import { useWeixinQrcodePoll } from '@/composables/channels/useWeixinQrcodePoll'
|
||||
import { useDingTalkAppRegister } from '@/composables/channels/useDingTalkAppRegister'
|
||||
import { useFeishuAppRegister } from '@/composables/channels/useFeishuAppRegister'
|
||||
import SkillIcon from '@/components/common/SkillIcon.vue'
|
||||
import { agentIconColor } from '@/utils/agentIconColor'
|
||||
import AgentPickerDialog from '@/components/common/AgentPickerDialog.vue'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
@ -377,19 +333,6 @@ const channelConfig = ref<Record<string, any>>({})
|
||||
const visibleFields = ref<Record<string, boolean>>({})
|
||||
const showAdvanced = ref(false)
|
||||
|
||||
// Custom agent dropdown — native <select><option> can't render SkillIcon
|
||||
// component for pi:* glyphs; mirror ChatConsole.vue's pattern.
|
||||
const agentDropdownOpen = ref(false)
|
||||
const selectedAgent = computed<Agent | null>(() => {
|
||||
const id = form.value.agentId
|
||||
if (id == null) return null
|
||||
return props.agents.find(a => String(a.id) === String(id)) ?? null
|
||||
})
|
||||
function onSelectAgent(a: Agent) {
|
||||
form.value.agentId = a.id as any
|
||||
agentDropdownOpen.value = false
|
||||
}
|
||||
|
||||
const verifying = ref(false)
|
||||
const verifyResult = ref<VerifyResult | null>(null)
|
||||
const invalidField = ref<string | null>(null)
|
||||
@ -879,75 +822,6 @@ function formatIdentityKey(key: string): string {
|
||||
.btn-secondary { padding: 10px 18px; background: var(--mc-bg-elevated); color: var(--mc-text-primary); border: 1px solid var(--mc-border); border-radius: 12px; font-size: 14px; font-weight: 600; cursor: pointer; transition: background 0.15s; }
|
||||
.btn-secondary:hover { background: var(--mc-bg-sunken); }
|
||||
|
||||
/* ===== Agent custom dropdown (mirrors ChatConsole.vue pattern) =====
|
||||
Native <select><option> can't render the <SkillIcon> component
|
||||
needed for pi:* glyphs, so step-3 uses a custom dropdown.
|
||||
z-index has to clear the wizard modal-overlay (z-index: 1000). */
|
||||
.agent-select { position: relative; }
|
||||
.agent-select-trigger {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--mc-border);
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
color: var(--mc-text-primary);
|
||||
background: var(--mc-bg-elevated);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
transition: border-color 0.15s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.agent-select-trigger:hover { border-color: var(--mc-primary); }
|
||||
.agent-select.open .agent-select-trigger { border-color: var(--mc-primary); box-shadow: 0 0 0 3px rgba(217,119,87,0.12); }
|
||||
.agent-select-trigger__icon { font-size: 18px; line-height: 1; flex-shrink: 0; display: inline-flex; }
|
||||
.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 {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
/* must clear modal-overlay (1000) so clicks fall through to backdrop, not modal */
|
||||
z-index: 1099;
|
||||
}
|
||||
.agent-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1100;
|
||||
background: var(--mc-bg-elevated);
|
||||
border: 1px solid var(--mc-border);
|
||||
border-radius: 12px;
|
||||
padding: 6px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.12);
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.agent-dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
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: 20px; line-height: 1; flex-shrink: 0; display: inline-flex; }
|
||||
.agent-dropdown-item__name { flex: 1; font-size: 14px; font-weight: 500; color: var(--mc-text-primary); 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); }
|
||||
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.15s; }
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||
</style>
|
||||
|
||||
514
mateclaw-ui/src/components/common/AgentPickerDialog.vue
Normal file
514
mateclaw-ui/src/components/common/AgentPickerDialog.vue
Normal file
@ -0,0 +1,514 @@
|
||||
<template>
|
||||
<div
|
||||
class="mc-agent-picker"
|
||||
:class="{ 'is-block': block, 'is-disabled': disabled }"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="mc-agent-picker__trigger"
|
||||
:class="{
|
||||
'is-compact': compact,
|
||||
'is-empty': !selectedAgent && !isUnknown,
|
||||
'is-unknown': isUnknown,
|
||||
'is-disabled': disabled,
|
||||
}"
|
||||
:disabled="disabled"
|
||||
:title="triggerLabel"
|
||||
@click="openDialog"
|
||||
>
|
||||
<SkillIcon
|
||||
class="mc-agent-picker__trigger-icon"
|
||||
:value="selectedAgent?.icon"
|
||||
:size="compact ? 24 : 22"
|
||||
fallback="🤖"
|
||||
/>
|
||||
<template v-if="!compact">
|
||||
<span class="mc-agent-picker__trigger-name">{{ triggerLabel }}</span>
|
||||
<span
|
||||
v-if="clearable && (selectedAgent || isUnknown) && !disabled"
|
||||
class="mc-agent-picker__clear"
|
||||
role="button"
|
||||
:aria-label="t('common.clear')"
|
||||
@click.stop="clear"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</span>
|
||||
<svg
|
||||
class="mc-agent-picker__caret"
|
||||
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>
|
||||
|
||||
<Teleport to="body">
|
||||
<Transition name="mc-agent-picker-fade">
|
||||
<div v-if="open" class="mc-agent-picker__overlay" @click.self="close">
|
||||
<div class="mc-agent-picker__panel" role="dialog" aria-modal="true">
|
||||
<div class="mc-agent-picker__head">
|
||||
<h2 class="mc-agent-picker__title">{{ t('agentContext.selectAgent') }}</h2>
|
||||
<button class="mc-agent-picker__close" :aria-label="t('common.close')" @click="close">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="searchable" class="mc-agent-picker__search">
|
||||
<svg
|
||||
class="mc-agent-picker__search-icon"
|
||||
width="14" height="14" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="2"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||
</svg>
|
||||
<input
|
||||
ref="searchRef"
|
||||
v-model="query"
|
||||
class="mc-agent-picker__search-input"
|
||||
:placeholder="t('common.search')"
|
||||
@keydown.esc.stop="close"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mc-agent-picker__body">
|
||||
<button
|
||||
v-if="clearable && !query.trim()"
|
||||
type="button"
|
||||
class="mc-agent-picker__item mc-agent-picker__item--none"
|
||||
:class="{ active: !selectedAgent && !isUnknown }"
|
||||
@click="clear"
|
||||
>
|
||||
<span class="mc-agent-picker__none-icon">∅</span>
|
||||
<div class="mc-agent-picker__item-info">
|
||||
<span class="mc-agent-picker__item-name">{{ t('common.none') }}</span>
|
||||
</div>
|
||||
<svg
|
||||
v-if="!selectedAgent && !isUnknown"
|
||||
class="mc-agent-picker__check"
|
||||
width="16" height="16" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="2.5"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-for="agent in filteredAgents"
|
||||
:key="agent.id"
|
||||
type="button"
|
||||
class="mc-agent-picker__item"
|
||||
:class="{ active: isSelected(agent) }"
|
||||
@click="select(agent)"
|
||||
>
|
||||
<SkillIcon
|
||||
class="mc-agent-picker__item-icon"
|
||||
:value="agent.icon"
|
||||
:size="28"
|
||||
fallback="🤖"
|
||||
/>
|
||||
<div class="mc-agent-picker__item-info">
|
||||
<span class="mc-agent-picker__item-name">{{ agent.name }}</span>
|
||||
<span v-if="subtitle(agent)" class="mc-agent-picker__item-desc">{{ subtitle(agent) }}</span>
|
||||
</div>
|
||||
<svg
|
||||
v-if="isSelected(agent)"
|
||||
class="mc-agent-picker__check"
|
||||
width="16" height="16" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="2.5"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div v-if="filteredAgents.length === 0" class="mc-agent-picker__empty">
|
||||
{{ query.trim() ? t('common.noResults') : t('common.noOptions') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import SkillIcon from '@/components/common/SkillIcon.vue'
|
||||
|
||||
/** Minimal structural shape so this picker works with the full `Agent`
|
||||
* entity as well as the lighter agent option lists (e.g. workflow steps). */
|
||||
export interface PickableAgent {
|
||||
id: string | number
|
||||
name: string
|
||||
icon?: string | null
|
||||
description?: string
|
||||
agentType?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
/** v-model value: the selected agent id. Kept as a string to preserve Snowflake precision. */
|
||||
modelValue: string | number | null
|
||||
/** Agents to choose from. */
|
||||
agents: PickableAgent[]
|
||||
/** Trigger label shown when no agent is selected. Defaults to agentContext.selectAgent. */
|
||||
placeholder?: string
|
||||
/** Render trigger in disabled state (no dialog). */
|
||||
disabled?: boolean
|
||||
/** Icon-only trigger — no name, no caret. For collapsed sidebars. */
|
||||
compact?: boolean
|
||||
/** Full-width trigger, for form-field contexts. */
|
||||
block?: boolean
|
||||
/** Allow clearing the selection (shows a ✕ on the trigger and a "none" row). */
|
||||
clearable?: boolean
|
||||
/** Label shown when modelValue is set but the agent is not in `agents`
|
||||
* (e.g. a workflow step referencing a since-deleted employee). */
|
||||
unknownLabel?: string
|
||||
}>(), {
|
||||
disabled: false,
|
||||
compact: false,
|
||||
block: false,
|
||||
clearable: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string | number | null]
|
||||
change: [value: string | number | null, agent: PickableAgent | null]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const open = ref(false)
|
||||
const query = ref('')
|
||||
const searchRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const hasValue = computed(() =>
|
||||
props.modelValue !== '' && props.modelValue !== null && props.modelValue !== undefined,
|
||||
)
|
||||
|
||||
const selectedAgent = computed<PickableAgent | null>(() => {
|
||||
if (!hasValue.value) return null
|
||||
return props.agents.find(a => String(a.id) === String(props.modelValue)) || null
|
||||
})
|
||||
|
||||
/** modelValue is set but resolves to no known agent — the referenced
|
||||
* employee was likely renamed or removed. */
|
||||
const isUnknown = computed(() => hasValue.value && !selectedAgent.value)
|
||||
|
||||
const triggerLabel = computed(() => {
|
||||
if (selectedAgent.value) return selectedAgent.value.name
|
||||
if (isUnknown.value) return props.unknownLabel || String(props.modelValue)
|
||||
return props.placeholder || t('agentContext.selectAgent')
|
||||
})
|
||||
|
||||
/** Show the search box once the list is long enough to warrant scanning. */
|
||||
const searchable = computed(() => props.agents.length > 6)
|
||||
|
||||
const filteredAgents = computed<PickableAgent[]>(() => {
|
||||
const q = query.value.trim().toLowerCase()
|
||||
if (!q) return props.agents
|
||||
return props.agents.filter(a =>
|
||||
`${a.name} ${a.description || ''} ${a.title || ''} ${a.agentType || ''}`.toLowerCase().includes(q),
|
||||
)
|
||||
})
|
||||
|
||||
function subtitle(agent: PickableAgent): string {
|
||||
return agent.description || agent.title || agent.agentType || ''
|
||||
}
|
||||
|
||||
function isSelected(agent: PickableAgent): boolean {
|
||||
return String(agent.id) === String(props.modelValue)
|
||||
}
|
||||
|
||||
function openDialog() {
|
||||
if (props.disabled) return
|
||||
open.value = true
|
||||
}
|
||||
|
||||
function close() {
|
||||
open.value = false
|
||||
}
|
||||
|
||||
function select(agent: PickableAgent) {
|
||||
emit('update:modelValue', agent.id)
|
||||
emit('change', agent.id, agent)
|
||||
close()
|
||||
}
|
||||
|
||||
function clear() {
|
||||
emit('update:modelValue', null)
|
||||
emit('change', null, null)
|
||||
close()
|
||||
}
|
||||
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') close()
|
||||
}
|
||||
|
||||
watch(open, (val) => {
|
||||
if (val) {
|
||||
query.value = ''
|
||||
window.addEventListener('keydown', onKeydown)
|
||||
nextTick(() => searchRef.value?.focus())
|
||||
} else {
|
||||
window.removeEventListener('keydown', onKeydown)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', onKeydown)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mc-agent-picker {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
}
|
||||
.mc-agent-picker.is-block {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ===== Trigger ===== */
|
||||
.mc-agent-picker__trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 240px;
|
||||
padding: 9px 12px;
|
||||
border: 1px solid var(--mc-border);
|
||||
border-radius: 12px;
|
||||
background: var(--mc-bg-elevated);
|
||||
color: var(--mc-text-primary);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
.is-block .mc-agent-picker__trigger {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
.mc-agent-picker__trigger.is-compact {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
padding: 8px;
|
||||
}
|
||||
.mc-agent-picker__trigger:hover:not(.is-disabled) {
|
||||
border-color: color-mix(in srgb, var(--mc-primary) 50%, var(--mc-border));
|
||||
}
|
||||
.mc-agent-picker__trigger:focus-visible {
|
||||
outline: none;
|
||||
border-color: var(--mc-primary);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--mc-primary) 15%, transparent);
|
||||
}
|
||||
.mc-agent-picker__trigger.is-disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.mc-agent-picker__trigger-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mc-agent-picker__trigger-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mc-agent-picker__trigger.is-empty .mc-agent-picker__trigger-name {
|
||||
color: var(--mc-text-tertiary);
|
||||
}
|
||||
.mc-agent-picker__trigger.is-unknown .mc-agent-picker__trigger-name {
|
||||
color: var(--mc-warning, #f59e0b);
|
||||
}
|
||||
.mc-agent-picker__clear {
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
color: var(--mc-text-tertiary);
|
||||
transition: background 0.12s ease, color 0.12s ease;
|
||||
}
|
||||
.mc-agent-picker__clear:hover {
|
||||
background: var(--mc-bg-sunken);
|
||||
color: var(--mc-text-primary);
|
||||
}
|
||||
.mc-agent-picker__caret {
|
||||
flex-shrink: 0;
|
||||
color: var(--mc-text-tertiary);
|
||||
}
|
||||
|
||||
/* ===== Dialog ===== */
|
||||
.mc-agent-picker__overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2400;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.mc-agent-picker__panel {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
max-height: 72vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--mc-bg-elevated);
|
||||
border: 1px solid var(--mc-border);
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.22);
|
||||
animation: mc-agent-picker-pop 0.26s cubic-bezier(0.32, 0.72, 0, 1.2);
|
||||
}
|
||||
@keyframes mc-agent-picker-pop {
|
||||
from { opacity: 0; transform: scale(0.94) translateY(6px); }
|
||||
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||
}
|
||||
|
||||
.mc-agent-picker-fade-enter-active,
|
||||
.mc-agent-picker-fade-leave-active { transition: opacity 0.18s ease; }
|
||||
.mc-agent-picker-fade-enter-from,
|
||||
.mc-agent-picker-fade-leave-to { opacity: 0; }
|
||||
|
||||
.mc-agent-picker__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 16px 20px 12px;
|
||||
border-bottom: 1px solid var(--mc-border-light);
|
||||
}
|
||||
.mc-agent-picker__title {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--mc-text-primary);
|
||||
}
|
||||
.mc-agent-picker__close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: none;
|
||||
border-radius: 9px;
|
||||
background: transparent;
|
||||
color: var(--mc-text-tertiary);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: background 0.18s ease, color 0.18s ease;
|
||||
}
|
||||
.mc-agent-picker__close:hover { background: var(--mc-bg-sunken); color: var(--mc-text-primary); }
|
||||
|
||||
.mc-agent-picker__search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 12px 16px 0;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid var(--mc-border);
|
||||
border-radius: 9px;
|
||||
background: var(--mc-bg-sunken);
|
||||
}
|
||||
.mc-agent-picker__search-icon { color: var(--mc-text-tertiary); flex-shrink: 0; }
|
||||
.mc-agent-picker__search-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
font-size: 13px;
|
||||
color: var(--mc-text-primary);
|
||||
}
|
||||
.mc-agent-picker__search-input::placeholder { color: var(--mc-text-tertiary); }
|
||||
|
||||
.mc-agent-picker__body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 8px;
|
||||
}
|
||||
.mc-agent-picker__body::-webkit-scrollbar { width: 4px; }
|
||||
.mc-agent-picker__body::-webkit-scrollbar-thumb { background: var(--mc-border); border-radius: 2px; }
|
||||
|
||||
.mc-agent-picker__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: background 0.12s ease;
|
||||
}
|
||||
.mc-agent-picker__item:hover { background: var(--mc-bg-sunken); }
|
||||
.mc-agent-picker__item.active {
|
||||
background: var(--mc-primary-bg);
|
||||
}
|
||||
.mc-agent-picker__item-icon { flex-shrink: 0; }
|
||||
.mc-agent-picker__none-icon {
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
color: var(--mc-text-tertiary);
|
||||
}
|
||||
.mc-agent-picker__item-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.mc-agent-picker__item-name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--mc-text-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mc-agent-picker__item-desc {
|
||||
font-size: 11.5px;
|
||||
color: var(--mc-text-tertiary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mc-agent-picker__check { flex-shrink: 0; color: var(--mc-primary); }
|
||||
|
||||
.mc-agent-picker__empty {
|
||||
padding: 28px 12px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: var(--mc-text-tertiary);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.mc-agent-picker__panel { max-width: 100%; max-height: 86vh; }
|
||||
}
|
||||
</style>
|
||||
@ -39,23 +39,17 @@
|
||||
|
||||
<label class="panel-field" v-if="modeNeedsAgent">
|
||||
<span class="field-label">{{ t('workflows.canvas.nodeAgent') }}</span>
|
||||
<!-- Digital employee picker. Agent steps persist agentId so the
|
||||
workflow cannot drift when an employee is renamed. -->
|
||||
<div class="agent-picker">
|
||||
<select
|
||||
class="mc-input"
|
||||
:value="agentSelectValue"
|
||||
@change="onAgentSelect"
|
||||
>
|
||||
<option value="">{{ t('workflows.canvas.fields.agentPlaceholder') }}</option>
|
||||
<option v-for="a in availableAgents" :key="a.id" :value="String(a.id)">
|
||||
{{ a.name }}<template v-if="a.title"> — {{ a.title }}</template>
|
||||
</option>
|
||||
<option v-if="agentNotInList" :value="agentSelectValue" disabled>
|
||||
{{ t('workflows.canvas.fields.agentMissing', { name: step.agentName || step.agentId }) }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Agent steps persist agentId so the workflow cannot drift when an
|
||||
employee is renamed. -->
|
||||
<AgentPickerDialog
|
||||
block
|
||||
clearable
|
||||
:model-value="agentModelValue"
|
||||
:agents="availableAgents"
|
||||
:placeholder="t('workflows.canvas.fields.agentPlaceholder')"
|
||||
:unknown-label="t('workflows.canvas.fields.agentMissing', { name: step?.agentName || step?.agentId })"
|
||||
@change="onAgentPicked"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="panel-field" v-if="modeNeedsAgent">
|
||||
@ -280,6 +274,7 @@
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { RawStep } from '@/composables/useWorkflowGraph'
|
||||
import AgentPickerDialog, { type PickableAgent } from '@/components/common/AgentPickerDialog.vue'
|
||||
|
||||
/** Minimal agent shape the panel needs to render the picker — kept
|
||||
* inside the component so the panel doesn't have to import the
|
||||
@ -288,6 +283,8 @@ export interface AgentOption {
|
||||
id: number | string
|
||||
name: string
|
||||
title?: string
|
||||
icon?: string
|
||||
description?: string
|
||||
}
|
||||
export interface ChannelOption {
|
||||
id: number | string
|
||||
@ -387,32 +384,25 @@ function onDuplicate() {
|
||||
emit('duplicate', { index: props.index })
|
||||
}
|
||||
|
||||
const agentNotInList = computed(() => {
|
||||
if (props.step?.agentId != null) {
|
||||
return !props.availableAgents.some((a) => String(a.id) === String(props.step?.agentId))
|
||||
}
|
||||
const n = props.step?.agentName
|
||||
if (!n) return false
|
||||
return !props.availableAgents.some((a) => a.name === n)
|
||||
})
|
||||
const agentSelectValue = computed(() => {
|
||||
if (props.step?.agentId != null) return String(props.step.agentId)
|
||||
// Resolve the picker's selected id. Newer steps persist agentId; legacy
|
||||
// steps stored only agentName, so fall back to a name lookup.
|
||||
const agentModelValue = computed<string | number | null>(() => {
|
||||
if (props.step?.agentId != null) return props.step.agentId
|
||||
const byName = props.availableAgents.find((a) => a.name === props.step?.agentName)
|
||||
return byName ? String(byName.id) : (props.step?.agentName ?? '')
|
||||
if (byName) return byName.id
|
||||
return props.step?.agentName ?? null
|
||||
})
|
||||
function onAgentSelect(e: Event) {
|
||||
const v = (e.target as HTMLSelectElement).value
|
||||
if (!v) {
|
||||
function onAgentPicked(value: string | number | null, agent: PickableAgent | null) {
|
||||
if (value == null) {
|
||||
patch({ agentId: undefined, agentName: undefined })
|
||||
return
|
||||
}
|
||||
// Keep the id as the raw string from the option value — Snowflake IDs
|
||||
// exceed Number.MAX_SAFE_INTEGER, so Number(v) would silently truncate.
|
||||
const selected = props.availableAgents.find((a) => String(a.id) === v)
|
||||
// Keep the id as a string — Snowflake IDs exceed Number.MAX_SAFE_INTEGER,
|
||||
// so a numeric round-trip would silently truncate.
|
||||
patch({
|
||||
agentId: v,
|
||||
// Kept as a denormalized label for the canvas node; runtime resolves by agentId.
|
||||
agentName: selected?.name,
|
||||
agentId: String(value),
|
||||
// Denormalized label for the canvas node; runtime resolves by agentId.
|
||||
agentName: agent?.name,
|
||||
})
|
||||
}
|
||||
|
||||
@ -588,12 +578,6 @@ function onDispatchTargetInput(channelType: string, raw: string) {
|
||||
font-family: 'JetBrains Mono', Consolas, monospace;
|
||||
font-size: 11.5px;
|
||||
}
|
||||
.agent-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.agent-picker .mc-input { flex: 1; min-width: 0; }
|
||||
.channel-picker {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
|
||||
@ -26,6 +26,7 @@ export default {
|
||||
search: 'Search',
|
||||
noResults: 'No matches',
|
||||
noOptions: 'No options',
|
||||
none: 'None',
|
||||
clear: 'Clear',
|
||||
expandSidebar: 'Expand sidebar',
|
||||
collapseSidebar: 'Collapse sidebar',
|
||||
@ -994,7 +995,7 @@ export default {
|
||||
kicker: 'Context Studio',
|
||||
title: 'Agent Context',
|
||||
desc: 'Manage prompt files and memory for agents',
|
||||
selectAgent: 'Select Agent',
|
||||
selectAgent: 'Select Employee',
|
||||
noAgent: 'Please select an agent first',
|
||||
files: 'Files',
|
||||
coreFiles: 'Core Files',
|
||||
@ -3258,7 +3259,7 @@ export default {
|
||||
kicker: 'Memory',
|
||||
title: 'Memory',
|
||||
desc: 'What your agent thinks about, remembers, and lets go.',
|
||||
selectAgent: 'Select Agent',
|
||||
selectAgent: 'Select Employee',
|
||||
selectAgentHint: 'Select an agent to view its memory',
|
||||
tabTimeline: 'Timeline',
|
||||
tabMemory: 'Long-term',
|
||||
|
||||
@ -26,6 +26,7 @@ export default {
|
||||
search: '搜索',
|
||||
noResults: '没有匹配项',
|
||||
noOptions: '暂无可选项',
|
||||
none: '不指定',
|
||||
clear: '清除',
|
||||
expandSidebar: '展开侧边栏',
|
||||
collapseSidebar: '折叠侧边栏',
|
||||
@ -886,7 +887,7 @@ export default {
|
||||
kicker: '上下文工作面',
|
||||
title: '智能体上下文',
|
||||
desc: '管理智能体的提示文件和记忆',
|
||||
selectAgent: '选择智能体',
|
||||
selectAgent: '选择员工',
|
||||
noAgent: '请先选择一个智能体',
|
||||
files: '文件列表',
|
||||
coreFiles: '核心文件',
|
||||
@ -3350,7 +3351,7 @@ export default {
|
||||
kicker: '记忆',
|
||||
title: '记忆',
|
||||
desc: '你的智能体在想什么、记住了什么、遗忘了什么。',
|
||||
selectAgent: '选择智能体',
|
||||
selectAgent: '选择员工',
|
||||
selectAgentHint: '请先选择一个智能体查看其记忆',
|
||||
tabTimeline: '整理时间线',
|
||||
tabMemory: '长期记忆',
|
||||
|
||||
@ -4,7 +4,7 @@ import { http } from '@/api'
|
||||
|
||||
export interface DreamReportItem {
|
||||
id: string
|
||||
agentId: number
|
||||
agentId: string | number
|
||||
mode: string
|
||||
topic: string | null
|
||||
triggerSource: string
|
||||
@ -28,7 +28,7 @@ export const useMemoryStore = defineStore('memory', () => {
|
||||
const currentReport = ref<DreamReportItem | null>(null)
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
async function fetchReports(agentId: number, page = 1, size = 20) {
|
||||
async function fetchReports(agentId: string | number, page = 1, size = 20) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
@ -46,7 +46,7 @@ export const useMemoryStore = defineStore('memory', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchReport(agentId: number, reportId: string) {
|
||||
async function fetchReport(agentId: string | number, reportId: string) {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await http.get(`/memory/${agentId}/dream/reports/${reportId}`)
|
||||
@ -65,7 +65,7 @@ export const useMemoryStore = defineStore('memory', () => {
|
||||
* SSE via EventSource doesn't support Authorization headers,
|
||||
* causing 401 errors. Polling every 15s is sufficient for dream events.
|
||||
*/
|
||||
function subscribeEvents(agentId: number) {
|
||||
function subscribeEvents(agentId: string | number) {
|
||||
unsubscribeEvents()
|
||||
pollTimer = setInterval(() => {
|
||||
fetchReports(agentId, 1, 20)
|
||||
|
||||
@ -6,12 +6,7 @@
|
||||
<div class="workspace-header workspace-header--compact">
|
||||
<h1 class="page-title page-title--compact">{{ t('agentContext.title') }}</h1>
|
||||
<div class="header-actions mc-surface-card">
|
||||
<select v-model="selectedAgentId" class="agent-select" @change="onAgentChange">
|
||||
<option value="" disabled>{{ t('agentContext.selectAgent') }}</option>
|
||||
<option v-for="agent in agents" :key="agent.id" :value="agent.id">
|
||||
{{ plainTextIcon(agent.icon) }} {{ agent.name }}
|
||||
</option>
|
||||
</select>
|
||||
<AgentPickerDialog v-model="selectedAgentId" :agents="agents" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -334,7 +329,7 @@ import { copyToClipboard } from '@/utils/clipboard'
|
||||
import type { Agent, WorkspaceFile } from '@/types/index'
|
||||
import { useMarkdownRenderer } from '@/composables/useMarkdownRenderer'
|
||||
import { handleMermaidDownload } from '@/composables/useMermaidRenderer'
|
||||
import { plainTextIcon } from '@/composables/usePixelarticons'
|
||||
import AgentPickerDialog from '@/components/common/AgentPickerDialog.vue'
|
||||
|
||||
const { renderMarkdown } = useMarkdownRenderer()
|
||||
|
||||
@ -452,10 +447,6 @@ async function loadAgents() {
|
||||
}
|
||||
}
|
||||
|
||||
function onAgentChange() {
|
||||
// watch 已处理
|
||||
}
|
||||
|
||||
async function fetchFiles() {
|
||||
if (!selectedAgentId.value) return
|
||||
try {
|
||||
@ -737,8 +728,6 @@ function formatTime(time?: string): string {
|
||||
.page-title--compact { font-size: clamp(24px, 2.5vw, 32px); font-weight: 800; letter-spacing: -0.03em; margin: 0; }
|
||||
.page-desc { font-size: 15px; color: var(--mc-text-secondary); margin: 0; line-height: 1.7; max-width: 720px; }
|
||||
.header-actions { display: flex; gap: 8px; align-items: center; padding: 12px; border-radius: 18px; flex-shrink: 0; }
|
||||
.agent-select { padding: 10px 14px; border: 1px solid var(--mc-border); border-radius: 12px; background: var(--mc-bg-elevated); color: var(--mc-text-primary); font-size: 14px; outline: none; min-width: 240px; cursor: pointer; }
|
||||
.agent-select:focus { border-color: var(--mc-primary); }
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; text-align: center; }
|
||||
@ -1047,7 +1036,8 @@ function formatTime(time?: string): string {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.agent-select {
|
||||
.header-actions :deep(.mc-agent-picker),
|
||||
.header-actions :deep(.mc-agent-picker__trigger) {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@ -25,35 +25,14 @@
|
||||
</button>
|
||||
|
||||
<div class="agent-selector">
|
||||
<button class="agent-select-trigger" @click="agentDropdownOpen = !agentDropdownOpen" :title="$t('chat.selectAgent')">
|
||||
<span class="agent-select-trigger__icon" :style="{ color: agentIconColor(currentAgent?.icon) }"><SkillIcon :value="currentAgent?.icon" :size="24" :fallback="'🤖'" /></span>
|
||||
<span v-if="!convPanelCollapsed || isMobile" class="agent-select-trigger__name">{{ currentAgent?.name || $t('chat.selectAgent') }}</span>
|
||||
<svg v-if="!convPanelCollapsed || isMobile" 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>
|
||||
</button>
|
||||
<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" :style="{ color: agentIconColor(agent.icon) }"><SkillIcon :value="agent.icon" :size="18" :fallback="'🤖'" /></span>
|
||||
<div class="agent-dropdown-item__info">
|
||||
<span class="agent-dropdown-item__name">{{ agent.name }}</span>
|
||||
<span class="agent-dropdown-item__desc">{{ agentTagline(agent) }}</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>
|
||||
<AgentPickerDialog
|
||||
block
|
||||
:model-value="selectedAgentId"
|
||||
:agents="agents"
|
||||
:compact="convPanelCollapsed && !isMobile"
|
||||
:placeholder="$t('chat.selectAgent')"
|
||||
@change="onAgentPicked"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="conversation-list">
|
||||
@ -356,6 +335,7 @@ import type { Conversation, Agent, ModelConfig, ProviderInfo, ActiveModelsInfo,
|
||||
import MessageList from '@/components/chat/MessageList.vue'
|
||||
import RecoverableModelBanner from '@/components/chat/RecoverableModelBanner.vue'
|
||||
import SkillIcon from '@/components/common/SkillIcon.vue'
|
||||
import AgentPickerDialog from '@/components/common/AgentPickerDialog.vue'
|
||||
import { parsePrompt, deriveTagline } from '@/utils/agentPromptProfile'
|
||||
import { agentIconColor } from '@/utils/agentIconColor'
|
||||
import ChatInput from '@/components/chat/ChatInput.vue'
|
||||
@ -467,14 +447,12 @@ const thinkingLevel = computed(() => thinkingEnabled.value ? 'high' : 'off')
|
||||
watch(thinkingEnabled, (v) => localStorage.setItem('mateclaw_thinking', v ? 'on' : 'off'))
|
||||
|
||||
// Dropdowns & menus
|
||||
const agentDropdownOpen = ref(false)
|
||||
|
||||
const headerMenuOpen = ref(false)
|
||||
|
||||
function selectAgent(agent: Agent) {
|
||||
agentDropdownOpen.value = false
|
||||
if (String(agent.id) !== String(selectedAgentId.value)) {
|
||||
selectedAgentId.value = agent.id
|
||||
function onAgentPicked(value: string | number | null) {
|
||||
if (value == null) return
|
||||
if (String(value) !== String(selectedAgentId.value)) {
|
||||
selectedAgentId.value = value
|
||||
newConversation()
|
||||
}
|
||||
}
|
||||
@ -1557,8 +1535,6 @@ async function clearMessages() {
|
||||
}
|
||||
}
|
||||
|
||||
// onAgentChange removed — replaced by selectAgent()
|
||||
|
||||
// onModelChange removed — replaced by selectModel()
|
||||
|
||||
function goToModelSettings(providerId?: string) {
|
||||
@ -2090,19 +2066,6 @@ function handleCodeCopy(e: MouseEvent) {
|
||||
padding: 10px 6px 12px;
|
||||
}
|
||||
|
||||
.conversation-panel.conv-collapsed .agent-select-trigger {
|
||||
justify-content: center;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.conversation-panel.conv-collapsed .agent-dropdown {
|
||||
position: fixed;
|
||||
top: auto;
|
||||
left: 62px;
|
||||
right: auto;
|
||||
min-width: 260px;
|
||||
}
|
||||
|
||||
.conversation-panel.conv-collapsed .conv-item {
|
||||
justify-content: center;
|
||||
padding: 10px 6px;
|
||||
@ -2187,52 +2150,6 @@ function handleCodeCopy(e: MouseEvent) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.agent-select-trigger {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--mc-border);
|
||||
border-radius: 12px;
|
||||
font-size: 13px;
|
||||
color: var(--mc-text-primary);
|
||||
background: var(--mc-bg-sunken);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.agent-select-trigger:hover {
|
||||
border-color: var(--mc-primary);
|
||||
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;
|
||||
@ -2240,80 +2157,6 @@ function handleCodeCopy(e: MouseEvent) {
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.agent-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
min-width: 240px;
|
||||
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;
|
||||
}
|
||||
|
||||
@ -75,7 +75,7 @@ import { mcToast } from '@/composables/useMcToast'
|
||||
import { http } from '@/api'
|
||||
import FactTrustBar from './FactTrustBar.vue'
|
||||
|
||||
const props = defineProps<{ agentId: number }>()
|
||||
const props = defineProps<{ agentId: string | number }>()
|
||||
const { t } = useI18n()
|
||||
|
||||
const facts = ref<any[]>([])
|
||||
|
||||
@ -66,7 +66,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { mcToast } from '@/composables/useMcToast'
|
||||
import { http, agentContextApi } from '@/api'
|
||||
|
||||
const props = defineProps<{ agentId: number }>()
|
||||
const props = defineProps<{ agentId: string | number }>()
|
||||
const { t } = useI18n()
|
||||
|
||||
interface FileInfo { filename: string; fileSize: number; enabled: boolean }
|
||||
|
||||
@ -22,7 +22,7 @@ import { ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { http } from '@/api'
|
||||
|
||||
const props = defineProps<{ agentId: number }>()
|
||||
const props = defineProps<{ agentId: string | number }>()
|
||||
const { t } = useI18n()
|
||||
const card = ref<any>(null)
|
||||
|
||||
|
||||
@ -14,36 +14,15 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Agent selector (ChatConsole pattern) -->
|
||||
<!-- Employee selector -->
|
||||
<div class="agent-selector">
|
||||
<button class="agent-select-trigger" @click="agentDropdownOpen = !agentDropdownOpen">
|
||||
<span class="agent-select-trigger__icon"><SkillIcon :value="currentAgent?.icon" :size="24" :fallback="'🧠'" /></span>
|
||||
<span class="agent-select-trigger__name">{{ currentAgent?.name || t('memory.selectAgent') }}</span>
|
||||
<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>
|
||||
</button>
|
||||
<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: agent.id === selectedAgentId }"
|
||||
@click="selectAgent(agent)"
|
||||
>
|
||||
<span class="agent-dropdown-item__icon"><SkillIcon :value="agent.icon" :size="18" :fallback="'🤖'" /></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="agent.id === 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>
|
||||
</Transition>
|
||||
<AgentPickerDialog
|
||||
v-model="selectedAgentId"
|
||||
:agents="agents"
|
||||
block
|
||||
:placeholder="t('memory.selectAgent')"
|
||||
@change="onAgentPicked"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Tab nav (segment control) -->
|
||||
@ -184,7 +163,7 @@ import { mcToast } from '@/composables/useMcToast'
|
||||
import { http } from '@/api'
|
||||
import { useAgentStore } from '@/stores/useAgentStore'
|
||||
import { useMemoryStore, type DreamReportItem } from '@/stores/useMemoryStore'
|
||||
import SkillIcon from '@/components/common/SkillIcon.vue'
|
||||
import AgentPickerDialog from '@/components/common/AgentPickerDialog.vue'
|
||||
import MorningCard from './components/MorningCard.vue'
|
||||
import FactList from './components/FactList.vue'
|
||||
import MemoryBrowser from './components/MemoryBrowser.vue'
|
||||
@ -194,9 +173,7 @@ const agentStore = useAgentStore()
|
||||
const store = useMemoryStore()
|
||||
|
||||
const agents = ref<any[]>([])
|
||||
const selectedAgentId = ref<number | null>(null)
|
||||
const currentAgent = computed(() => agents.value.find(a => a.id === selectedAgentId.value))
|
||||
const agentDropdownOpen = ref(false)
|
||||
const selectedAgentId = ref<string | number>('')
|
||||
const activeTab = ref('timeline')
|
||||
const selectedReportId = ref<string | null>(null)
|
||||
const currentPage = ref(1)
|
||||
@ -227,9 +204,7 @@ watch(selectedAgentId, (id) => {
|
||||
|
||||
onUnmounted(() => store.unsubscribeEvents())
|
||||
|
||||
function selectAgent(agent: any) {
|
||||
selectedAgentId.value = agent.id
|
||||
agentDropdownOpen.value = false
|
||||
function onAgentPicked() {
|
||||
selectedReportId.value = null
|
||||
currentPage.value = 1
|
||||
store.currentReport = null
|
||||
@ -332,36 +307,8 @@ function fmtTime(iso: string) {
|
||||
background: var(--mc-bg-elevated);
|
||||
}
|
||||
|
||||
/* Agent selector (same CSS as ChatConsole) */
|
||||
/* Employee selector */
|
||||
.agent-selector { position: relative; padding: 0 12px 8px; }
|
||||
.agent-select-trigger {
|
||||
width: 100%; display: flex; align-items: center; gap: 8px;
|
||||
padding: 8px 12px; border: 1px solid var(--mc-border); border-radius: 12px;
|
||||
font-size: 13px; color: var(--mc-text-primary); background: var(--mc-bg-sunken);
|
||||
cursor: pointer; outline: none; transition: all 0.15s;
|
||||
}
|
||||
.agent-select-trigger:hover { border-color: var(--mc-primary); 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 { position: fixed; inset: 0; z-index: 99; }
|
||||
.agent-dropdown {
|
||||
position: absolute; top: calc(100% + 4px); left: 12px; right: 12px; min-width: 240px; 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; }
|
||||
.agent-dropdown-item__name { font-size: 13px; font-weight: 500; 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 { color: var(--mc-primary); flex-shrink: 0; }
|
||||
|
||||
/* Segment control */
|
||||
.memory-nav {
|
||||
@ -504,12 +451,6 @@ function fmtTime(iso: string) {
|
||||
.detail-empty p { font-size: 14px; max-width: 260px; line-height: 1.5; }
|
||||
|
||||
/* ========== Transitions ========== */
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.15s; }
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||
.agent-dropdown-enter-active { transition: all 0.2s ease; }
|
||||
.agent-dropdown-leave-active { transition: all 0.12s ease; }
|
||||
.agent-dropdown-enter-from { opacity: 0; transform: translateY(-6px) scale(0.97); }
|
||||
.agent-dropdown-leave-to { opacity: 0; transform: translateY(-4px) scale(0.98); }
|
||||
.slide-down-enter-active, .slide-down-leave-active { transition: all 0.2s ease; }
|
||||
.slide-down-enter-from, .slide-down-leave-to { opacity: 0; transform: translateY(-8px); }
|
||||
|
||||
|
||||
@ -279,7 +279,7 @@ const resumingId = ref<number | null>(null)
|
||||
// Workspace agent list — fed to the StepPropertyPanel's agent picker
|
||||
// so authors stop typing free-form agent names that don't actually
|
||||
// exist. Loaded once on mount and on workspace switch.
|
||||
interface AgentOption { id: number; name: string; title?: string }
|
||||
interface AgentOption { id: number; name: string; title?: string; icon?: string; description?: string }
|
||||
const availableAgents = ref<AgentOption[]>([])
|
||||
interface ChannelOption {
|
||||
id: number | string
|
||||
@ -607,7 +607,7 @@ async function reloadAgents() {
|
||||
// via the X-Workspace-Id header, so no client-side filter is needed.
|
||||
availableAgents.value = rows
|
||||
.filter((a) => a && a.name)
|
||||
.map((a) => ({ id: a.id, name: a.name, title: a.title }))
|
||||
.map((a) => ({ id: a.id, name: a.name, title: a.title, icon: a.icon, description: a.description }))
|
||||
} catch (e) {
|
||||
console.error('listAgents failed', e)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user