Skip to content

Commit 801de3c

Browse files
author
linyuan.yang
committed
staginput stagfilter
1 parent a385447 commit 801de3c

9 files changed

Lines changed: 450 additions & 6 deletions

File tree

packages/admin/src/i18n/en.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,11 @@ export default {
634634
id_hint: 'Unique identifier, only lowercase letters, numbers, hyphens and dots',
635635
name: 'Name',
636636
name_placeholder: 'e.g. default, my-agent',
637+
tags: 'Tags',
638+
tags_placeholder: 'Type and press Enter / comma to add',
639+
tags_hint: 'Used for filtering the list; multiple allowed',
640+
tags_duplicate: 'Tag already exists',
641+
filter_by_tag: 'Filter by tag:',
637642
type_single: 'Single',
638643
type_react: 'ReAct',
639644
type_generative: 'Generative',

packages/admin/src/i18n/zh.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,11 @@ export default {
634634
id_hint: '唯一标识,仅支持小写字母、数字、连字符和点号',
635635
name: '名称',
636636
name_placeholder: '如 default, my-agent',
637+
tags: '标签',
638+
tags_placeholder: '输入后回车/逗号添加',
639+
tags_hint: '用于在列表中按标签过滤;可多个',
640+
tags_duplicate: '该标签已存在',
641+
filter_by_tag: '按标签过滤:',
637642
type_single: 'Single (单智能体)',
638643
type_react: 'ReAct (迭代决策)',
639644
type_generative: 'Generative (生成式)',

packages/admin/src/views/agents/AgentsView.vue

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ref, computed } from 'vue'
33
import { useI18n } from 'vue-i18n'
44
import { apiFetch } from '@/shared/api'
55
import { store, applyMcpList } from '@/shared/store'
6-
import { useToast, useConfirm, SButton, SCard, SPageToolbar, SPageContent, STable, SInfoTable, SInfoRow, SModal, SInput, type STableColumn } from 'sbot-ui'
6+
import { useToast, useConfirm, SButton, SCard, SPageToolbar, SPageContent, STable, SInfoTable, SInfoRow, SModal, SInput, STagFilter, type STableColumn } from 'sbot-ui'
77
import AgentModal from './modals/AgentModal.vue'
88
import AgentMcpModal from './modals/AgentMcpModal.vue'
99
import AgentSkillsModal from './modals/AgentSkillsModal.vue'
@@ -25,6 +25,25 @@ const sortedAgentRows = computed<AgentRow[]>(() => {
2525
.map(([id, a]) => ({ id, ...(a as any) }))
2626
.sort((a, b) => order(a.type) - order(b.type))
2727
})
28+
29+
const allTags = computed<string[]>(() => {
30+
const set = new Set<string>()
31+
for (const a of Object.values(agents.value)) {
32+
const tags = (a as any).tags
33+
if (Array.isArray(tags)) for (const t of tags) if (t) set.add(t)
34+
}
35+
return Array.from(set).sort()
36+
})
37+
38+
const activeTagFilters = ref<string[]>([])
39+
const filteredAgentRows = computed<AgentRow[]>(() => {
40+
if (activeTagFilters.value.length === 0) return sortedAgentRows.value
41+
return sortedAgentRows.value.filter(row => {
42+
const rowTags: string[] = Array.isArray(row.tags) ? row.tags : []
43+
// AND 过滤:所选标签都需命中
44+
return activeTagFilters.value.every(t => rowTags.includes(t))
45+
})
46+
})
2847
const modelName = (id: string) => (store.settings.models?.[id] as any)?.name || id
2948
3049
const columns = computed<STableColumn[]>(() => [
@@ -321,11 +340,15 @@ async function saveMcpParams() {
321340
<SPageToolbar>
322341
<SButton type="outline" size="sm" @click="refresh">{{ t('common.refresh') }}</SButton>
323342
<SButton type="primary" size="sm" @click="agentModal?.open()">{{ t('agents.add') }}</SButton>
343+
<div v-if="allTags.length" class="agents-tag-filter">
344+
<span class="agents-tag-filter-label">{{ t('agents.filter_by_tag') }}</span>
345+
<STagFilter v-model="activeTagFilters" :options="allTags" />
346+
</div>
324347
</SPageToolbar>
325348
<SPageContent>
326349
<STable
327350
:columns="columns"
328-
:rows="sortedAgentRows"
351+
:rows="filteredAgentRows"
329352
row-key="id"
330353
expandable
331354
v-model:expandedKeys="expandedIds"
@@ -338,6 +361,12 @@ async function saveMcpParams() {
338361
<span v-if="row.skills === '*'" class="config-badge config-badge-info">{{ t('agents.tab_skills') }} *</span>
339362
<span v-if="row.mcp === '*'" class="config-badge config-badge-info">{{ t('agents.tab_tools') }} *</span>
340363
<span v-if="row.autoApproveAllTools" class="config-badge config-badge-warn">{{ t('agents.auto_approve_all_tools') }}</span>
364+
<span
365+
v-for="tag in (row.tags || [])"
366+
:key="tag"
367+
class="config-badge config-badge-tag"
368+
:class="{ 'config-badge-tag-active': activeTagFilters.includes(tag) }"
369+
>{{ tag }}</span>
341370
</template>
342371

343372
<template #type="{ row }">
@@ -552,6 +581,20 @@ async function saveMcpParams() {
552581
.config-badge-id { background: #e0f2fe; color: #0369a1; }
553582
.config-badge-info { background: #dbeafe; color: #1d4ed8; }
554583
.config-badge-warn { background: #fef3c7; color: #b45309; }
584+
.config-badge-tag { background: #f1f5f9; color: #475569; }
585+
.config-badge-tag-active { background: #1f2937; color: #f9fafb; }
586+
587+
.agents-tag-filter {
588+
display: flex;
589+
align-items: center;
590+
gap: var(--sui-sp-3);
591+
margin-left: var(--sui-sp-4);
592+
flex-wrap: wrap;
593+
}
594+
.agents-tag-filter-label {
595+
font-size: var(--sui-fs-sm);
596+
color: var(--sui-fg-muted);
597+
}
555598
556599
/* Detail tabs */
557600
.agent-tab-bar {
@@ -687,5 +730,7 @@ html[data-theme="dark"] .agent-type-acp { background: #134e4a; color: #5eead4; }
687730
html[data-theme="dark"] .config-badge-id { background: #0c4a6e; color: #7dd3fc; }
688731
html[data-theme="dark"] .config-badge-info { background: #1e3a5f; color: #93c5fd; }
689732
html[data-theme="dark"] .config-badge-warn { background: #422006; color: #fcd34d; }
733+
html[data-theme="dark"] .config-badge-tag { background: #334155; color: #cbd5e1; }
734+
html[data-theme="dark"] .config-badge-tag-active { background: #e5e7eb; color: #1f2937; }
690735
html[data-theme="dark"] .acp-command { color: #5eead4; }
691736
</style>

packages/admin/src/views/agents/modals/AgentModal.vue

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { useToast, useConfirm } from 'sbot-ui'
77
import { AgentMode, ACPSessionMode, InsightScope, TodoScope } from '@/shared/types'
88
import type { Agent, SubAgentRef } from '@/shared/types'
99
import CreatePromptModal from '@/components/modals/CreatePromptModal.vue'
10-
import { SModal, SButton, SInput, STextarea, SSelect, SFormItem, SFormSection, SHint, SCheckCard } from 'sbot-ui'
10+
import { SModal, SButton, SInput, STextarea, SSelect, SFormItem, SFormSection, SHint, SCheckCard, STagInput } from 'sbot-ui'
1111
1212
const { t } = useI18n()
1313
const { confirm } = useConfirm()
@@ -37,6 +37,14 @@ function applyPreset(idx: number) {
3737
const { show } = useToast()
3838
3939
const agents = computed(() => store.settings.agents || {})
40+
const allTagSuggestions = computed(() => {
41+
const set = new Set<string>()
42+
for (const a of Object.values(store.settings.agents || {})) {
43+
const tags = (a as any).tags
44+
if (Array.isArray(tags)) for (const t of tags) if (t) set.add(t)
45+
}
46+
return Array.from(set).sort()
47+
})
4048
const modelOptions = computed(() =>
4149
Object.entries(store.settings.models || {}).map(([id, m]) => ({ id, label: m.name || id }))
4250
)
@@ -50,6 +58,7 @@ const editingId = ref<string | null>(null)
5058
const form = ref({
5159
id: '',
5260
name: '',
61+
tags: [] as string[],
5362
type: AgentMode.Single as string,
5463
model: '',
5564
compactModel: '',
@@ -78,6 +87,7 @@ function open(id?: string) {
7887
form.value = {
7988
id: id,
8089
name: (a as any).name || '',
90+
tags: Array.isArray((a as any).tags) ? [...(a as any).tags] : [],
8191
type: a.type || AgentMode.Single,
8292
model: a.model || '',
8393
compactModel: (a as any).compactModel || '',
@@ -101,7 +111,7 @@ function open(id?: string) {
101111
editingId.value = null
102112
tempSubAgents.value = []
103113
form.value = {
104-
id: '', name: '', type: AgentMode.Single, model: '', compactModel: '',
114+
id: '', name: '', tags: [], type: AgentMode.Single, model: '', compactModel: '',
105115
systemPrompt: '',
106116
insightScope: InsightScope.Disabled, insightExtractor: '', insightExtractorPromptFile: '',
107117
todoScope: TodoScope.Disabled, todoExtractor: '', todoExtractorPromptFile: '',
@@ -174,6 +184,7 @@ async function save() {
174184
}
175185
176186
if (form.value.name.trim()) (config as any).name = form.value.name.trim()
187+
if (form.value.tags.length > 0) (config as any).tags = [...form.value.tags]
177188
const id = editingId.value
178189
if (id) {
179190
await apiFetch(`/api/agents/${encodeURIComponent(id)}`, 'PUT', config)
@@ -291,6 +302,14 @@ defineExpose({ open })
291302
<SFormItem :label="t('agents.name') + ' *'">
292303
<SInput v-model="form.name" :placeholder="t('agents.name_placeholder')" />
293304
</SFormItem>
305+
<SFormItem :label="t('agents.tags')" :hint="t('agents.tags_hint')">
306+
<STagInput
307+
v-model="form.tags"
308+
:placeholder="t('agents.tags_placeholder')"
309+
:suggestions="allTagSuggestions"
310+
@invalid="(reason) => reason === 'duplicate' ? show(t('agents.tags_duplicate'), 'warning') : null"
311+
/>
312+
</SFormItem>
294313
<SFormItem :label="t('common.type') + ' *'">
295314
<SSelect v-model="form.type">
296315
<option :value="AgentMode.Single">{{ t('agents.type_single') }}</option>
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue'
3+
4+
const props = withDefaults(defineProps<{
5+
/** 已选中的标签 */
6+
modelValue: string[]
7+
/** 候选标签列表 */
8+
options: string[]
9+
/** 单选还是多选,默认多选 */
10+
multiple?: boolean
11+
size?: 'sm' | 'md'
12+
/** 选中态颜色,默认 primary(深色) */
13+
variant?: 'default' | 'primary'
14+
}>(), {
15+
multiple: true,
16+
size: 'sm',
17+
variant: 'primary',
18+
})
19+
20+
const emit = defineEmits<{
21+
'update:modelValue': [value: string[]]
22+
}>()
23+
24+
const selected = computed(() => new Set(props.modelValue))
25+
26+
function toggle(tag: string) {
27+
if (props.multiple) {
28+
const next = selected.value.has(tag)
29+
? props.modelValue.filter(t => t !== tag)
30+
: [...props.modelValue, tag]
31+
emit('update:modelValue', next)
32+
} else {
33+
emit('update:modelValue', selected.value.has(tag) ? [] : [tag])
34+
}
35+
}
36+
37+
function clearAll() {
38+
emit('update:modelValue', [])
39+
}
40+
41+
function chipClass(tag: string) {
42+
return [
43+
's-tag-filter__chip',
44+
`s-tag-filter__chip--${props.size}`,
45+
{
46+
's-tag-filter__chip--active': selected.value.has(tag),
47+
[`s-tag-filter__chip--${props.variant}`]: selected.value.has(tag),
48+
},
49+
]
50+
}
51+
</script>
52+
53+
<template>
54+
<div class="s-tag-filter" v-if="options.length > 0">
55+
<button
56+
v-for="tag in options"
57+
:key="tag"
58+
type="button"
59+
:class="chipClass(tag)"
60+
@click="toggle(tag)"
61+
>{{ tag }}</button>
62+
<button
63+
v-if="modelValue.length > 0"
64+
type="button"
65+
class="s-tag-filter__clear"
66+
@click="clearAll"
67+
>×</button>
68+
</div>
69+
</template>
70+
71+
<style scoped>
72+
.s-tag-filter {
73+
display: flex;
74+
flex-wrap: wrap;
75+
align-items: center;
76+
gap: var(--sui-sp-2);
77+
}
78+
.s-tag-filter__chip {
79+
border: 1px solid var(--sui-border);
80+
background: var(--sui-bg);
81+
color: var(--sui-fg-muted);
82+
border-radius: var(--sui-radius-pill);
83+
padding: 2px var(--sui-sp-3);
84+
font-size: var(--sui-fs-sm);
85+
font-family: inherit;
86+
cursor: pointer;
87+
transition: background var(--sui-transition-base), color var(--sui-transition-base), border-color var(--sui-transition-base);
88+
}
89+
.s-tag-filter__chip--sm {
90+
padding: 1px var(--sui-sp-3);
91+
font-size: var(--sui-fs-xs);
92+
}
93+
.s-tag-filter__chip:hover {
94+
background: var(--sui-bg-hover);
95+
border-color: var(--sui-border-strong);
96+
color: var(--sui-fg);
97+
}
98+
.s-tag-filter__chip--active {
99+
background: var(--sui-fg);
100+
border-color: var(--sui-fg);
101+
color: var(--sui-bg);
102+
}
103+
.s-tag-filter__chip--active:hover {
104+
background: var(--sui-fg);
105+
color: var(--sui-bg);
106+
}
107+
.s-tag-filter__chip--default.s-tag-filter__chip--active {
108+
background: var(--sui-bg-hover);
109+
border-color: var(--sui-border-strong);
110+
color: var(--sui-fg);
111+
}
112+
113+
.s-tag-filter__clear {
114+
border: none;
115+
background: none;
116+
cursor: pointer;
117+
font-size: 16px;
118+
line-height: 1;
119+
color: var(--sui-fg-disabled);
120+
padding: 0 var(--sui-sp-2);
121+
font-family: inherit;
122+
}
123+
.s-tag-filter__clear:hover { color: var(--sui-danger-strong); }
124+
</style>

0 commit comments

Comments
 (0)