vue-agent-start 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +30 -0
- package/pnpm-workspace.yaml +3 -0
- package/src/agent-flow/adapter/backend.ts +99 -0
- package/src/agent-flow/components/ChatIframePanel.vue +773 -0
- package/src/agent-flow/components/DatasetItemCard.vue +313 -0
- package/src/agent-flow/components/MemoryWindow.vue +104 -0
- package/src/agent-flow/components/PromptEditor.vue +804 -0
- package/src/agent-flow/components/PromptEditorTagPanel.vue +460 -0
- package/src/agent-flow/components/SvgIcon.vue +32 -0
- package/src/agent-flow/components/ToolItemCard.vue +169 -0
- package/src/agent-flow/components/VariableCard.vue +161 -0
- package/src/agent-flow/components/VariableSelector.vue +162 -0
- package/src/agent-flow/components/chat-iframe-types.ts +35 -0
- package/src/agent-flow/components/stubs/DatasetChooser.vue +70 -0
- package/src/agent-flow/components/stubs/Embedding.vue +22 -0
- package/src/agent-flow/components/stubs/ModelSelect.vue +24 -0
- package/src/agent-flow/components/stubs/RestfulSelectCard.vue +24 -0
- package/src/agent-flow/components/stubs/ToolChooser.vue +96 -0
- package/src/agent-flow/components/stubs/TriggersItemCard.vue +126 -0
- package/src/agent-flow/components/stubs/VariableModal.vue +115 -0
- package/src/agent-flow/index.ts +73 -0
- package/src/agent-flow/stores/workflow.ts +72 -0
- package/src/agent-flow/utils/langUtils.ts +173 -0
- package/src/agent-flow/workflow/Ability.vue +65 -0
- package/src/agent-flow/workflow/AuthorizationItem.vue +101 -0
- package/src/agent-flow/workflow/ConversationVar.vue +42 -0
- package/src/agent-flow/workflow/CustomEdge.vue +94 -0
- package/src/agent-flow/workflow/EnvironmentVar.vue +40 -0
- package/src/agent-flow/workflow/FlowDesigner.vue +2802 -0
- package/src/agent-flow/workflow/Icon.vue +179 -0
- package/src/agent-flow/workflow/NodeConfig.vue +509 -0
- package/src/agent-flow/workflow/NodeConfigCard.vue +460 -0
- package/src/agent-flow/workflow/NodeHandle.vue +147 -0
- package/src/agent-flow/workflow/NodeHeaderToolbar.vue +77 -0
- package/src/agent-flow/workflow/NodeHoverToolbar.vue +176 -0
- package/src/agent-flow/workflow/OutputItemCard.vue +668 -0
- package/src/agent-flow/workflow/ParamTableItem.vue +200 -0
- package/src/agent-flow/workflow/StructTable.vue +202 -0
- package/src/agent-flow/workflow/VarInsertField.vue +517 -0
- package/src/agent-flow/workflow/VarSelectField.vue +223 -0
- package/src/agent-flow/workflow/VariableSelectorItem.vue +48 -0
- package/src/agent-flow/workflow/WfField.vue +206 -0
- package/src/agent-flow/workflow/_config-base.css +470 -0
- package/src/agent-flow/workflow/composables/useNode.js +55 -0
- package/src/agent-flow/workflow/index.vue +53 -0
- package/src/agent-flow/workflow/nodeCard/AgentNodeCard.vue +180 -0
- package/src/agent-flow/workflow/nodeCard/AnswerNodeCard.vue +65 -0
- package/src/agent-flow/workflow/nodeCard/ClassifierCard.vue +152 -0
- package/src/agent-flow/workflow/nodeCard/CodeNodeCard.vue +139 -0
- package/src/agent-flow/workflow/nodeCard/ConditionNodeCard.vue +323 -0
- package/src/agent-flow/workflow/nodeCard/DocumentExtractorCard.vue +83 -0
- package/src/agent-flow/workflow/nodeCard/EndNodeCard.vue +123 -0
- package/src/agent-flow/workflow/nodeCard/HttpNodeCard.vue +451 -0
- package/src/agent-flow/workflow/nodeCard/HumanInputCard.vue +152 -0
- package/src/agent-flow/workflow/nodeCard/IterationCard.vue +66 -0
- package/src/agent-flow/workflow/nodeCard/KnowledgeRetrievalCard.vue +48 -0
- package/src/agent-flow/workflow/nodeCard/LLMNodeCard.vue +206 -0
- package/src/agent-flow/workflow/nodeCard/ListOperatorCard.vue +188 -0
- package/src/agent-flow/workflow/nodeCard/ParameterExtractorCard.vue +352 -0
- package/src/agent-flow/workflow/nodeCard/ServiceApiNodeCard.vue +188 -0
- package/src/agent-flow/workflow/nodeCard/StartNodeCard.vue +216 -0
- package/src/agent-flow/workflow/nodeCard/VariableAssignerCard.vue +125 -0
- package/src/agent-flow/workflow/nodeCard/VariableNodeCard.vue +121 -0
- package/src/agent-flow/workflow/nodes/AgentNode.vue +122 -0
- package/src/agent-flow/workflow/nodes/AnswerNode.vue +99 -0
- package/src/agent-flow/workflow/nodes/ClassifierNode.vue +199 -0
- package/src/agent-flow/workflow/nodes/CodeNode.vue +152 -0
- package/src/agent-flow/workflow/nodes/ConditionNode.vue +232 -0
- package/src/agent-flow/workflow/nodes/DocumentExtractorNode.vue +96 -0
- package/src/agent-flow/workflow/nodes/EndNode.vue +119 -0
- package/src/agent-flow/workflow/nodes/FileUploadNode.vue +97 -0
- package/src/agent-flow/workflow/nodes/HttpNode.vue +204 -0
- package/src/agent-flow/workflow/nodes/HumanInputNode.vue +110 -0
- package/src/agent-flow/workflow/nodes/IterationNode.vue +98 -0
- package/src/agent-flow/workflow/nodes/KnowledgeRetrievalNode.vue +253 -0
- package/src/agent-flow/workflow/nodes/LLMNode.vue +134 -0
- package/src/agent-flow/workflow/nodes/ListOperatorNode.vue +111 -0
- package/src/agent-flow/workflow/nodes/LoopNode.vue +94 -0
- package/src/agent-flow/workflow/nodes/ParameterExtractorNode.vue +147 -0
- package/src/agent-flow/workflow/nodes/ServiceApiNode.vue +161 -0
- package/src/agent-flow/workflow/nodes/StartNode.vue +199 -0
- package/src/agent-flow/workflow/nodes/TemplateNode.vue +103 -0
- package/src/agent-flow/workflow/nodes/UserInputNode.vue +92 -0
- package/src/agent-flow/workflow/nodes/VariableAssignerNode.vue +145 -0
- package/src/agent-flow/workflow/nodes/VariableNode.vue +130 -0
- package/src/agent-flow/workflow/nodes/_node-base.css +338 -0
- package/src/agent-flow/workflow/utils/node_card_form.ts +335 -0
- package/src/agent-flow/workflow/utils/node_config.ts +63 -0
- package/src/agent-flow/workflow/utils/workflow_utils.ts +151 -0
- package/src/agent-studio/adapters/springAgentStart.ts +390 -0
- package/src/agent-studio/api/index.ts +1 -0
- package/src/agent-studio/api/types.ts +173 -0
- package/src/agent-studio/components/AgentApiDocs.vue +500 -0
- package/src/agent-studio/components/AgentAppsPage.vue +1601 -0
- package/src/agent-studio/components/AgentCardGrid.vue +259 -0
- package/src/agent-studio/components/AgentChatPage.vue +553 -0
- package/src/agent-studio/components/AgentDebugPanel.vue +350 -0
- package/src/agent-studio/components/AgentLogsPanel.vue +391 -0
- package/src/agent-studio/components/AgentMonitorPanel.vue +204 -0
- package/src/agent-studio/components/AgentOrchestrate.vue +505 -0
- package/src/agent-studio/components/AgentPromptEditor.vue +237 -0
- package/src/agent-studio/components/AgentStudioShell.vue +227 -0
- package/src/agent-studio/components/AgentToolsPanel.vue +394 -0
- package/src/agent-studio/components/AgentVariablesPanel.vue +205 -0
- package/src/agent-studio/components/ApiKeyManager.vue +251 -0
- package/src/agent-studio/components/AppDesignDrawer.vue +3391 -0
- package/src/agent-studio/components/CreateAppModal.vue +737 -0
- package/src/agent-studio/components/SparkChart.vue +200 -0
- package/src/agent-studio/components/VariableEditorModal.vue +499 -0
- package/src/agent-studio/composables/useAgentStudio.ts +347 -0
- package/src/agent-studio/index.ts +60 -0
- package/src/agent-studio/panels/DrawerFlowDesigner.vue +203 -0
- package/src/agent-studio/panels/LogAnnotationPanel.vue +490 -0
- package/src/agent-studio/panels/MonitorPanel.vue +535 -0
- package/src/agent-studio/types.ts +242 -0
- package/src/index.ts +155 -0
- package/src/knowledge-hub/adapters/springAgentStart.ts +472 -0
- package/src/knowledge-hub/components/AddDocumentsWizard.vue +1452 -0
- package/src/knowledge-hub/components/CreateDatasetWizard.vue +1723 -0
- package/src/knowledge-hub/components/DatasetCardGrid.vue +467 -0
- package/src/knowledge-hub/components/DatasetDetailDrawer.vue +888 -0
- package/src/knowledge-hub/components/DatasetPicker.vue +65 -0
- package/src/knowledge-hub/components/DatasetPickerModal.vue +526 -0
- package/src/knowledge-hub/components/DatasetSettingsPanel.vue +908 -0
- package/src/knowledge-hub/components/DatasetSidebar.vue +781 -0
- package/src/knowledge-hub/components/DocumentChunksView.vue +802 -0
- package/src/knowledge-hub/components/DocumentTable.vue +552 -0
- package/src/knowledge-hub/components/HitTestingPanel.vue +74 -0
- package/src/knowledge-hub/components/KnowledgeApp.vue +66 -0
- package/src/knowledge-hub/components/KnowledgeHubApp.vue +504 -0
- package/src/knowledge-hub/components/RecallTestingPanelV2.vue +515 -0
- package/src/knowledge-hub/components/RetrievalConfigPopover.vue +270 -0
- package/src/knowledge-hub/components/RetrievalMethodPicker.vue +547 -0
- package/src/knowledge-hub/components/RetrievedList.vue +24 -0
- package/src/knowledge-hub/components/internal/KhDialog.vue +195 -0
- package/src/knowledge-hub/composables/useKnowledge.ts +89 -0
- package/src/knowledge-hub/i18n/index.ts +2 -0
- package/src/knowledge-hub/i18n/messages.ts +422 -0
- package/src/knowledge-hub/i18n/useKhI18n.ts +107 -0
- package/src/knowledge-hub/index.ts +46 -0
- package/src/knowledge-hub/styles/index.css +4 -0
- package/src/knowledge-hub/styles/tokens.css +138 -0
- package/src/knowledge-hub/types/api.ts +213 -0
- package/src/knowledge-hub/types/dataset.ts +88 -0
- package/src/knowledge-hub/types/document.ts +45 -0
- package/src/knowledge-hub/types/index.ts +9 -0
- package/src/knowledge-hub/types/retrieval.ts +46 -0
- package/src/provider-hub/components/CredentialForm.vue +103 -0
- package/src/provider-hub/components/DefaultModelsPanel.vue +270 -0
- package/src/provider-hub/components/GroupedModelSelect.vue +445 -0
- package/src/provider-hub/components/ModelCardGrid.vue +332 -0
- package/src/provider-hub/components/ModelParameterDrawer.vue +739 -0
- package/src/provider-hub/components/ModelPickerPopover.vue +1282 -0
- package/src/provider-hub/components/ProviderApp.vue +159 -0
- package/src/provider-hub/components/ProviderCredentialModal.vue +321 -0
- package/src/provider-hub/components/ProviderGallery.vue +131 -0
- package/src/provider-hub/components/ProviderHubShell.vue +1992 -0
- package/src/provider-hub/components/ProviderIcon.vue +130 -0
- package/src/provider-hub/composables/useProviderHub.ts +358 -0
- package/src/provider-hub/index.ts +43 -0
- package/src/provider-hub/svg/MiniMax.svg +1 -0
- package/src/provider-hub/svg/Volcengine.svg +1 -0
- package/src/provider-hub/svg/Zhipu.svg +1 -0
- package/src/provider-hub/svg/deepseek.svg +1 -0
- package/src/provider-hub/svg/moonshot.svg +1 -0
- package/src/provider-hub/svg/openai.svg +1 -0
- package/src/provider-hub/svg/qwen.svg +1 -0
- package/src/provider-hub/svg/siliconflow.svg +1 -0
- package/src/provider-hub/types.ts +256 -0
|
@@ -0,0 +1,1992 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* ProviderHubShell — Dify-parity three-section settings page.
|
|
4
|
+
*
|
|
5
|
+
* Section 1 · 模型列表 : compact card grid (~5 per row, responsive). Click a
|
|
6
|
+
* card → slide-in right drawer shows the catalog
|
|
7
|
+
* grouped by model type, each entry a scrollable
|
|
8
|
+
* pill row with an enable/disable toggle. Header
|
|
9
|
+
* actions live in the drawer: 重新拉取 · 手动添加 ·
|
|
10
|
+
* 编辑凭证 · 删除凭证.
|
|
11
|
+
* Section 2 · 待配置 : providers with credential but empty catalog.
|
|
12
|
+
* Section 3 · 安装模型供应商 : providers without credentials — catalog grid.
|
|
13
|
+
*
|
|
14
|
+
* Catalog display cost: each provider row is a fixed-height scrollable panel, so
|
|
15
|
+
* a provider with 100+ models doesn't stretch the page — the shell stays scannable.
|
|
16
|
+
*/
|
|
17
|
+
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
|
18
|
+
|
|
19
|
+
import { Popover } from 'ant-design-vue';
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
modelTypeColor,
|
|
23
|
+
modelTypeLabel,
|
|
24
|
+
useProviderHub,
|
|
25
|
+
} from '../composables/useProviderHub';
|
|
26
|
+
import type { CatalogRow, ModelEntity, ModelType, ProviderView } from '../types';
|
|
27
|
+
import DefaultModelsPanel from './DefaultModelsPanel.vue';
|
|
28
|
+
import ModelParameterDrawer from './ModelParameterDrawer.vue';
|
|
29
|
+
import ProviderCredentialModal from './ProviderCredentialModal.vue';
|
|
30
|
+
import ProviderIcon from './ProviderIcon.vue';
|
|
31
|
+
|
|
32
|
+
interface Props {
|
|
33
|
+
tenantId?: string;
|
|
34
|
+
/** Optional external override — if omitted the shell fetches models itself. */
|
|
35
|
+
models?: ModelEntity[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const props = defineProps<Props>();
|
|
39
|
+
|
|
40
|
+
const emit = defineEmits<{
|
|
41
|
+
(e: 'change'): void;
|
|
42
|
+
}>();
|
|
43
|
+
|
|
44
|
+
const {
|
|
45
|
+
listProviders,
|
|
46
|
+
listModels,
|
|
47
|
+
deleteProviderCredential,
|
|
48
|
+
refreshCatalog,
|
|
49
|
+
setModelEnabled,
|
|
50
|
+
setModelEnabledByName,
|
|
51
|
+
getProviderCatalog,
|
|
52
|
+
registerModel,
|
|
53
|
+
} = useProviderHub();
|
|
54
|
+
|
|
55
|
+
const providers = ref<ProviderView[]>([]);
|
|
56
|
+
const localModels = ref<ModelEntity[]>([]);
|
|
57
|
+
const loading = ref(false);
|
|
58
|
+
const search = ref('');
|
|
59
|
+
|
|
60
|
+
const models = computed(() => props.models ?? localModels.value);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Lazily-loaded catalog per provider. Populated by the new Dify-parity
|
|
64
|
+
* /catalog endpoint on demand (openPopover) — that endpoint returns the
|
|
65
|
+
* union of predefined DB rows + custom agent_model rows with the tenant's
|
|
66
|
+
* enable/default state pre-joined. Legacy `models` list stays around as a
|
|
67
|
+
* fallback for the compact top-card counts and pending-section derivation.
|
|
68
|
+
*/
|
|
69
|
+
const catalogByProvider = ref<Record<string, CatalogRow[]>>({});
|
|
70
|
+
|
|
71
|
+
async function refresh() {
|
|
72
|
+
loading.value = true;
|
|
73
|
+
try {
|
|
74
|
+
const ps = await listProviders(props.tenantId);
|
|
75
|
+
providers.value = ps;
|
|
76
|
+
if (!props.models) {
|
|
77
|
+
localModels.value = await listModels(props.tenantId);
|
|
78
|
+
}
|
|
79
|
+
} finally {
|
|
80
|
+
loading.value = false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
onMounted(refresh);
|
|
85
|
+
watch(() => props.tenantId, refresh);
|
|
86
|
+
|
|
87
|
+
// -------------------------------------------------- helpers
|
|
88
|
+
/**
|
|
89
|
+
* Bind icon props for {@link ProviderIcon} from whatever shape the backend
|
|
90
|
+
* gave us. Explicit {@code svgIcon} / {@code iconUrl} win over the legacy
|
|
91
|
+
* single-slot {@code icon} column; when only {@code icon} is present we
|
|
92
|
+
* sniff the payload — inline SVG markup vs. URL — and route to the right
|
|
93
|
+
* prop. Returns {@code {}} when no icon is known so ProviderIcon falls
|
|
94
|
+
* back to its own local file lookup (matched against {@code p.name}).
|
|
95
|
+
*/
|
|
96
|
+
function providerIconProps(p: ProviderView): {
|
|
97
|
+
svg?: string;
|
|
98
|
+
iconUrl?: string;
|
|
99
|
+
} {
|
|
100
|
+
if (p.svgIcon) return { svg: p.svgIcon };
|
|
101
|
+
if (p.iconUrl) return { iconUrl: p.iconUrl };
|
|
102
|
+
const raw = p.icon?.trim();
|
|
103
|
+
if (!raw) return {};
|
|
104
|
+
if (/^<(\?xml|svg\b)/i.test(raw)) return { svg: raw };
|
|
105
|
+
if (/^(https?:|data:|\/)/i.test(raw)) return { iconUrl: raw };
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Total known model count for the compact card. Combines the provider's
|
|
111
|
+
* predefined catalog (visible without opening) + tenant-registered custom
|
|
112
|
+
* models. Used ONLY for the pending/configured section split and the top
|
|
113
|
+
* card's "N 个模型" summary — the popover uses the authoritative catalog.
|
|
114
|
+
*/
|
|
115
|
+
function knownModelCount(p: ProviderView): number {
|
|
116
|
+
const predef = p.predefinedModels?.length ?? 0;
|
|
117
|
+
const custom = p.installedModelCount ?? 0;
|
|
118
|
+
return predef + custom;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Catalog rows for a provider — either the lazily-loaded /catalog result
|
|
122
|
+
* or an empty array if not fetched yet (popover shows loading state). */
|
|
123
|
+
function catalogRows(providerName: string): CatalogRow[] {
|
|
124
|
+
return catalogByProvider.value[providerName] ?? [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function enabledCount(p: ProviderView): number {
|
|
128
|
+
const rows = catalogRows(p.name);
|
|
129
|
+
if (rows.length > 0) return rows.filter((r) => r.enabled).length;
|
|
130
|
+
// Fallback before the catalog was fetched: predefined default to enabled
|
|
131
|
+
// (Dify semantics) + all custom rows we know about.
|
|
132
|
+
return knownModelCount(p);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const configured = computed(() =>
|
|
136
|
+
providers.value.filter(
|
|
137
|
+
(p) => p.credentialConfigured && knownModelCount(p) > 0,
|
|
138
|
+
),
|
|
139
|
+
);
|
|
140
|
+
const pending = computed(() =>
|
|
141
|
+
providers.value.filter(
|
|
142
|
+
(p) => p.credentialConfigured && knownModelCount(p) === 0,
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
const installable = computed(() => {
|
|
146
|
+
const q = search.value.trim().toLowerCase();
|
|
147
|
+
return providers.value
|
|
148
|
+
.filter((p) => !p.credentialConfigured)
|
|
149
|
+
.filter(
|
|
150
|
+
(p) =>
|
|
151
|
+
!q ||
|
|
152
|
+
p.name.toLowerCase().includes(q) ||
|
|
153
|
+
p.label.toLowerCase().includes(q),
|
|
154
|
+
)
|
|
155
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Group the popover catalog by model type. Uses the authoritative /catalog
|
|
160
|
+
* response, so predefined + custom are merged with correct enabled/isDefault
|
|
161
|
+
* flags coming from the settings tables.
|
|
162
|
+
*/
|
|
163
|
+
function groupCatalogByType(providerName: string): Record<string, CatalogRow[]> {
|
|
164
|
+
const acc: Record<string, CatalogRow[]> = {};
|
|
165
|
+
for (const r of catalogRows(providerName)) {
|
|
166
|
+
(acc[r.modelType] ??= []).push(r);
|
|
167
|
+
}
|
|
168
|
+
for (const key of Object.keys(acc)) {
|
|
169
|
+
acc[key]!.sort((a, b) => a.model.localeCompare(b.model));
|
|
170
|
+
}
|
|
171
|
+
return acc;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function distinctModelTypes(p: ProviderView): string[] {
|
|
175
|
+
const rows = catalogRows(p.name);
|
|
176
|
+
if (rows.length > 0) {
|
|
177
|
+
const set = new Set<string>();
|
|
178
|
+
for (const r of rows) set.add(r.modelType);
|
|
179
|
+
return [...set];
|
|
180
|
+
}
|
|
181
|
+
// Fallback to what the provider advertises via predefined models.
|
|
182
|
+
const set = new Set<string>();
|
|
183
|
+
for (const pd of p.predefinedModels ?? []) set.add(pd.modelType);
|
|
184
|
+
return [...set];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// -------------------------------------------------- credential modal
|
|
188
|
+
const modalOpen = ref(false);
|
|
189
|
+
const modalProvider = ref<ProviderView | null>(null);
|
|
190
|
+
const modalEditMode = ref(false);
|
|
191
|
+
|
|
192
|
+
function openCredentialModal(p: ProviderView, editMode = false) {
|
|
193
|
+
modalProvider.value = p;
|
|
194
|
+
modalEditMode.value = editMode;
|
|
195
|
+
modalOpen.value = true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function onCredentialSaved() {
|
|
199
|
+
const name = modalProvider.value?.name;
|
|
200
|
+
// Invalidate every per-provider cache before refetching — otherwise the
|
|
201
|
+
// popover briefly shows the previous credential's rows (bug: "添加之后
|
|
202
|
+
// 数据没有刷新过来"). Belt-and-suspenders: we also refetch below.
|
|
203
|
+
if (name) invalidateProviderCache(name);
|
|
204
|
+
await refresh();
|
|
205
|
+
emit('change');
|
|
206
|
+
// Auto-load the fresh catalog for the newly-configured provider so the
|
|
207
|
+
// popover has data ready the moment the user clicks it. We do this even
|
|
208
|
+
// when knownModelCount === 0 so a 0-catalog provider still shows an empty
|
|
209
|
+
// popover (with the "点击 重新拉取" hint) instead of falling back to the
|
|
210
|
+
// legacy count derived from a stale cache.
|
|
211
|
+
if (name) {
|
|
212
|
+
const p = providers.value.find((x) => x.name === name);
|
|
213
|
+
if (p) {
|
|
214
|
+
popoverProvider.value = p;
|
|
215
|
+
await loadCatalog(p.name);
|
|
216
|
+
await defaultsPanel.value?.refresh();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function clearCredential(p: ProviderView) {
|
|
222
|
+
if (
|
|
223
|
+
!confirm(
|
|
224
|
+
`确认删除 ${p.label} 的 API Key?已启用的模型 / 默认模型 / 自定义模型条目会一起清除,下次重新添加从零开始。`,
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
return;
|
|
228
|
+
await deleteProviderCredential(p.name, props.tenantId);
|
|
229
|
+
popoverProvider.value = null;
|
|
230
|
+
// Purge every cache keyed by this provider — stale catalog rows would
|
|
231
|
+
// otherwise resurface on the next popover open before the network refetch
|
|
232
|
+
// finishes.
|
|
233
|
+
invalidateProviderCache(p.name);
|
|
234
|
+
await refresh();
|
|
235
|
+
await defaultsPanel.value?.refresh();
|
|
236
|
+
emit('change');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Drop every provider-hub client-side cache keyed by {@code name} so a
|
|
241
|
+
* subsequent open / refetch reads a clean slate. Called after credential
|
|
242
|
+
* save AND delete because both invalidate the tenant-scoped catalog + toggle
|
|
243
|
+
* state that lives behind that provider.
|
|
244
|
+
*/
|
|
245
|
+
function invalidateProviderCache(name: string) {
|
|
246
|
+
if (name in catalogByProvider.value) {
|
|
247
|
+
const next = { ...catalogByProvider.value };
|
|
248
|
+
delete next[name];
|
|
249
|
+
catalogByProvider.value = next;
|
|
250
|
+
}
|
|
251
|
+
if (catalogLoading.value.has(name)) {
|
|
252
|
+
const s = new Set(catalogLoading.value);
|
|
253
|
+
s.delete(name);
|
|
254
|
+
catalogLoading.value = s;
|
|
255
|
+
}
|
|
256
|
+
if (refreshingCatalog.value.has(name)) {
|
|
257
|
+
const s = new Set(refreshingCatalog.value);
|
|
258
|
+
s.delete(name);
|
|
259
|
+
refreshingCatalog.value = s;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// -------------------------------------------------- catalog drawer
|
|
264
|
+
const popoverProvider = ref<ProviderView | null>(null);
|
|
265
|
+
const catalogLoading = ref<Set<string>>(new Set());
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Fetch the full Dify-parity catalog view for a provider (predefined + custom
|
|
269
|
+
* union, with enabled/isDefault flags pre-joined from the settings tables).
|
|
270
|
+
* Cached in {@link catalogByProvider} so re-opens don't re-hit the network.
|
|
271
|
+
*/
|
|
272
|
+
async function loadCatalog(name: string) {
|
|
273
|
+
const next = new Set(catalogLoading.value);
|
|
274
|
+
next.add(name);
|
|
275
|
+
catalogLoading.value = next;
|
|
276
|
+
try {
|
|
277
|
+
const rows = await getProviderCatalog(name, props.tenantId);
|
|
278
|
+
catalogByProvider.value = { ...catalogByProvider.value, [name]: rows };
|
|
279
|
+
} finally {
|
|
280
|
+
const after = new Set(catalogLoading.value);
|
|
281
|
+
after.delete(name);
|
|
282
|
+
catalogLoading.value = after;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function openPopover(p: ProviderView) {
|
|
287
|
+
popoverProvider.value = p;
|
|
288
|
+
loadCatalog(p.name);
|
|
289
|
+
}
|
|
290
|
+
function closePopover() {
|
|
291
|
+
popoverProvider.value = null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Keep the popover's `p` in sync with fresh `providers` after refetch so
|
|
295
|
+
// installedModelCount / credentialConfigured reflect current state.
|
|
296
|
+
watch(providers, (ps) => {
|
|
297
|
+
if (popoverProvider.value) {
|
|
298
|
+
const still = ps.find((x) => x.name === popoverProvider.value!.name);
|
|
299
|
+
popoverProvider.value = still ?? null;
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// -------------------------------------------------- catalog operations
|
|
304
|
+
const refreshingCatalog = ref<Set<string>>(new Set());
|
|
305
|
+
async function onRefreshCatalog(p: ProviderView) {
|
|
306
|
+
const next = new Set(refreshingCatalog.value);
|
|
307
|
+
next.add(p.name);
|
|
308
|
+
refreshingCatalog.value = next;
|
|
309
|
+
try {
|
|
310
|
+
// Two-step: (1) reload persisted catalog so any user changes propagate;
|
|
311
|
+
// (2) hit the vendor for the live list and merge transient entries in as
|
|
312
|
+
// source='remote'. Nothing is persisted at DB level from the refresh.
|
|
313
|
+
const [remoteList] = await Promise.all([
|
|
314
|
+
refreshCatalog(p.name, props.tenantId),
|
|
315
|
+
loadCatalog(p.name),
|
|
316
|
+
]);
|
|
317
|
+
mergeRemoteIntoCatalog(p.name, remoteList);
|
|
318
|
+
emit('change');
|
|
319
|
+
} finally {
|
|
320
|
+
const after = new Set(refreshingCatalog.value);
|
|
321
|
+
after.delete(p.name);
|
|
322
|
+
refreshingCatalog.value = after;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Add live-fetched entries into the local catalog cache as source='remote' rows,
|
|
328
|
+
* unless an existing persisted (predefined/custom) row already covers the same
|
|
329
|
+
* (model, modelType). Remote rows default to disabled — user enables them via
|
|
330
|
+
* the toggle, which triggers the settings-table INSERT.
|
|
331
|
+
*/
|
|
332
|
+
function mergeRemoteIntoCatalog(providerName: string, remote: any[]) {
|
|
333
|
+
if (!remote || remote.length === 0) return;
|
|
334
|
+
const existing = catalogByProvider.value[providerName] ?? [];
|
|
335
|
+
const seen = new Set<string>();
|
|
336
|
+
for (const r of existing) seen.add(`${r.model}::${r.modelType}`);
|
|
337
|
+
const additions: CatalogRow[] = [];
|
|
338
|
+
for (const rm of remote) {
|
|
339
|
+
const key = `${rm.modelId}::${rm.modelType}`;
|
|
340
|
+
if (seen.has(key)) continue;
|
|
341
|
+
if (!rm.modelType) continue;
|
|
342
|
+
additions.push({
|
|
343
|
+
id: `remote::${providerName}::${rm.modelId}::${rm.modelType}`,
|
|
344
|
+
model: rm.modelId,
|
|
345
|
+
label: rm.label ?? rm.modelId,
|
|
346
|
+
modelType: rm.modelType,
|
|
347
|
+
contextLength: rm.contextLength,
|
|
348
|
+
dimensions: rm.dimensions,
|
|
349
|
+
features: rm.features ?? [],
|
|
350
|
+
source: 'remote',
|
|
351
|
+
enabled: false,
|
|
352
|
+
isDefault: false,
|
|
353
|
+
});
|
|
354
|
+
seen.add(key);
|
|
355
|
+
}
|
|
356
|
+
catalogByProvider.value = {
|
|
357
|
+
...catalogByProvider.value,
|
|
358
|
+
[providerName]: [...existing, ...additions],
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const togglingKey = ref<Set<string>>(new Set());
|
|
363
|
+
function toggleKey(row: CatalogRow): string {
|
|
364
|
+
return `${row.source}::${row.id}`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Dify-parity toggle. Routes by source:
|
|
369
|
+
* · predefined → PUT /model-providers/{name}/models/{model}/enabled — writes to
|
|
370
|
+
* agent_provider_model_setting (missing row = enabled).
|
|
371
|
+
* · custom → PATCH /models/{id}/enabled — writes to agent_model.enabled.
|
|
372
|
+
* Updates the local catalog row optimistically so the pill flips instantly.
|
|
373
|
+
*/
|
|
374
|
+
async function onToggleEnabled(providerName: string, row: CatalogRow, next: boolean) {
|
|
375
|
+
const key = toggleKey(row);
|
|
376
|
+
const set = new Set(togglingKey.value);
|
|
377
|
+
set.add(key);
|
|
378
|
+
togglingKey.value = set;
|
|
379
|
+
try {
|
|
380
|
+
if (row.source === 'custom') {
|
|
381
|
+
// Custom (agent_model) row — legacy id-based enable path.
|
|
382
|
+
await setModelEnabled(row.id, next);
|
|
383
|
+
} else {
|
|
384
|
+
// Predefined or remote-fetched — new (provider, model, modelType) path.
|
|
385
|
+
// For 'remote' rows the backend materializes an agent_predefined_model
|
|
386
|
+
// row on enable so the entry survives page reload.
|
|
387
|
+
await setModelEnabledByName(
|
|
388
|
+
providerName,
|
|
389
|
+
row.model,
|
|
390
|
+
row.modelType,
|
|
391
|
+
next,
|
|
392
|
+
props.tenantId,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
// Optimistic update — replace just this row in the cached catalog.
|
|
396
|
+
const list = catalogByProvider.value[providerName] ?? [];
|
|
397
|
+
const patched = list.map((r) =>
|
|
398
|
+
r.id === row.id && r.source === row.source ? { ...r, enabled: next } : r,
|
|
399
|
+
);
|
|
400
|
+
catalogByProvider.value = { ...catalogByProvider.value, [providerName]: patched };
|
|
401
|
+
// Also keep legacy models list in sync for the compact card counters.
|
|
402
|
+
if (row.source === 'custom' && !props.models) {
|
|
403
|
+
localModels.value = localModels.value.map((x) =>
|
|
404
|
+
x.id === row.id ? { ...x, enabled: next } : x,
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
// Defaults dropdown pool = enabled catalog rows across all providers, so a
|
|
408
|
+
// toggle can add/remove candidates — refresh so the Select stays in sync.
|
|
409
|
+
await defaultsPanel.value?.refresh();
|
|
410
|
+
emit('change');
|
|
411
|
+
} finally {
|
|
412
|
+
const after = new Set(togglingKey.value);
|
|
413
|
+
after.delete(key);
|
|
414
|
+
togglingKey.value = after;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// -------------------------------------------------- manual add
|
|
419
|
+
const manualAddOpen = ref(false);
|
|
420
|
+
const manualAddProvider = ref<ProviderView | null>(null);
|
|
421
|
+
const manualAddForm = reactive({
|
|
422
|
+
modelName: '',
|
|
423
|
+
modelType: 'LLM' as ModelType,
|
|
424
|
+
});
|
|
425
|
+
const manualAddSaving = ref(false);
|
|
426
|
+
const manualAddError = ref<string | null>(null);
|
|
427
|
+
|
|
428
|
+
function openManualAdd(p: ProviderView) {
|
|
429
|
+
manualAddProvider.value = p;
|
|
430
|
+
manualAddForm.modelName = '';
|
|
431
|
+
manualAddForm.modelType = (p.supportedModelTypes?.[0] as ModelType) ?? 'LLM';
|
|
432
|
+
manualAddError.value = null;
|
|
433
|
+
manualAddOpen.value = true;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function submitManualAdd() {
|
|
437
|
+
if (!manualAddProvider.value) return;
|
|
438
|
+
if (!manualAddForm.modelName.trim()) {
|
|
439
|
+
manualAddError.value = '请填入模型名称';
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const p = manualAddProvider.value;
|
|
443
|
+
manualAddSaving.value = true;
|
|
444
|
+
manualAddError.value = null;
|
|
445
|
+
try {
|
|
446
|
+
await registerModel({
|
|
447
|
+
tenantId: props.tenantId,
|
|
448
|
+
providerName: p.name,
|
|
449
|
+
modelName: manualAddForm.modelName.trim(),
|
|
450
|
+
modelType: manualAddForm.modelType,
|
|
451
|
+
credentialId: p.credentialId,
|
|
452
|
+
credentials: {},
|
|
453
|
+
});
|
|
454
|
+
manualAddOpen.value = false;
|
|
455
|
+
await refresh();
|
|
456
|
+
emit('change');
|
|
457
|
+
} catch (e: any) {
|
|
458
|
+
manualAddError.value = e?.message ?? String(e);
|
|
459
|
+
} finally {
|
|
460
|
+
manualAddSaving.value = false;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// -------------------------------------------------- parameter drawer
|
|
465
|
+
const drawerOpen = ref(false);
|
|
466
|
+
const drawerModel = ref<ModelEntity | null>(null);
|
|
467
|
+
const drawerProvider = ref<ProviderView | null>(null);
|
|
468
|
+
const defaultsPanel = ref<InstanceType<typeof DefaultModelsPanel> | null>(null);
|
|
469
|
+
|
|
470
|
+
function openDrawer(m: ModelEntity, p: ProviderView) {
|
|
471
|
+
drawerModel.value = m;
|
|
472
|
+
drawerProvider.value = p;
|
|
473
|
+
drawerOpen.value = true;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function onDrawerSaved() {
|
|
477
|
+
await refresh();
|
|
478
|
+
await defaultsPanel.value?.refresh();
|
|
479
|
+
emit('change');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function onDrawerDeleted() {
|
|
483
|
+
await refresh();
|
|
484
|
+
await defaultsPanel.value?.refresh();
|
|
485
|
+
emit('change');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Fires after DefaultModelsPanel auto-saves a new default. Sync own state so
|
|
489
|
+
* the popover's "默认" tag and card counters reflect the change. */
|
|
490
|
+
async function onDefaultsChanged() {
|
|
491
|
+
await refresh();
|
|
492
|
+
emit('change');
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// -------------------------------------------------- feature helpers
|
|
496
|
+
function featureLabel(f: string): string {
|
|
497
|
+
if (f === 'VISION') return '视觉';
|
|
498
|
+
if (f === 'TOOL_CALL' || f === 'STREAM_TOOL_CALL') return '工具';
|
|
499
|
+
if (f === 'STRUCTURED_OUTPUT') return 'JSON';
|
|
500
|
+
if (f === 'AGENT_THOUGHT') return '推理';
|
|
501
|
+
return '';
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** Feature badges for a catalog row (predefined-sourced or custom). */
|
|
505
|
+
function primaryFeaturesForRow(row: CatalogRow): string[] {
|
|
506
|
+
const raw = row.features ?? [];
|
|
507
|
+
const seen = new Set<string>();
|
|
508
|
+
const out: string[] = [];
|
|
509
|
+
for (const f of raw) {
|
|
510
|
+
const l = featureLabel(f);
|
|
511
|
+
if (l && !seen.has(l)) {
|
|
512
|
+
seen.add(l);
|
|
513
|
+
out.push(f);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return out;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Materialize a catalog row into a ModelEntity-shaped object for the
|
|
521
|
+
* parameter drawer. For custom rows this is trivial (real agent_model id);
|
|
522
|
+
* for predefined rows we synthesize a placeholder — the drawer's save path
|
|
523
|
+
* will need a materialization endpoint later (deferred).
|
|
524
|
+
*/
|
|
525
|
+
function catalogRowToModelEntity(row: CatalogRow, providerName: string): ModelEntity {
|
|
526
|
+
return {
|
|
527
|
+
id: row.id,
|
|
528
|
+
tenantId: props.tenantId ?? 'default',
|
|
529
|
+
providerName,
|
|
530
|
+
modelName: row.model,
|
|
531
|
+
modelType: row.modelType,
|
|
532
|
+
enabled: row.enabled,
|
|
533
|
+
isDefault: row.isDefault,
|
|
534
|
+
credentialId: row.credentialId,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// -------------------------------------------------- card ⋯ dropdown menu
|
|
539
|
+
const openMenuFor = ref<string | null>(null);
|
|
540
|
+
function toggleCardMenu(providerName: string, e: Event) {
|
|
541
|
+
e.stopPropagation();
|
|
542
|
+
openMenuFor.value = openMenuFor.value === providerName ? null : providerName;
|
|
543
|
+
}
|
|
544
|
+
function closeCardMenu() {
|
|
545
|
+
openMenuFor.value = null;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// -------------------------------------------------- popover filter tabs
|
|
549
|
+
type EnabledFilter = 'all' | 'disabled' | 'enabled';
|
|
550
|
+
const enabledFilter = ref<EnabledFilter>('all');
|
|
551
|
+
const typeFilter = ref<'all' | ModelType>('all');
|
|
552
|
+
|
|
553
|
+
function filteredCatalogRows(providerName: string): CatalogRow[] {
|
|
554
|
+
const rows = catalogRows(providerName);
|
|
555
|
+
return rows.filter((r) => {
|
|
556
|
+
if (enabledFilter.value === 'enabled' && !r.enabled) return false;
|
|
557
|
+
if (enabledFilter.value === 'disabled' && r.enabled) return false;
|
|
558
|
+
if (typeFilter.value !== 'all' && r.modelType !== typeFilter.value) return false;
|
|
559
|
+
return true;
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function groupFilteredByType(providerName: string): Record<string, CatalogRow[]> {
|
|
564
|
+
const acc: Record<string, CatalogRow[]> = {};
|
|
565
|
+
for (const r of filteredCatalogRows(providerName)) {
|
|
566
|
+
(acc[r.modelType] ??= []).push(r);
|
|
567
|
+
}
|
|
568
|
+
for (const key of Object.keys(acc)) {
|
|
569
|
+
acc[key]!.sort((a, b) => a.model.localeCompare(b.model));
|
|
570
|
+
}
|
|
571
|
+
return acc;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function availableTypesInCatalog(providerName: string): ModelType[] {
|
|
575
|
+
const set = new Set<ModelType>();
|
|
576
|
+
for (const r of catalogRows(providerName)) set.add(r.modelType);
|
|
577
|
+
return [...set];
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function countBy(providerName: string, kind: EnabledFilter): number {
|
|
581
|
+
const rows = catalogRows(providerName);
|
|
582
|
+
if (kind === 'all') return rows.length;
|
|
583
|
+
return rows.filter((r) => (kind === 'enabled' ? r.enabled : !r.enabled)).length;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function resetFilters() {
|
|
587
|
+
enabledFilter.value = 'all';
|
|
588
|
+
typeFilter.value = 'all';
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
watch(popoverProvider, () => resetFilters());
|
|
592
|
+
|
|
593
|
+
// -------------------------------------------------- row display helpers
|
|
594
|
+
function modelTypeIcon(t: ModelType | string): string {
|
|
595
|
+
if (t === 'LLM') return '💬';
|
|
596
|
+
if (t === 'TEXT_EMBEDDING') return '🧮';
|
|
597
|
+
if (t === 'RERANK') return '🔀';
|
|
598
|
+
if (t === 'SPEECH2TEXT') return '🎤';
|
|
599
|
+
if (t === 'TTS') return '🔊';
|
|
600
|
+
if (t === 'MODERATION') return '🛡';
|
|
601
|
+
return '🤖';
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Format a context window size like Dify does: 1024→"1K", 128000→"128K",
|
|
606
|
+
* 1000000→"1M". Returns without units when < 1000 (rare for context windows).
|
|
607
|
+
*/
|
|
608
|
+
function formatContext(n?: number): string {
|
|
609
|
+
if (!n || n <= 0) return '';
|
|
610
|
+
if (n >= 1_000_000) {
|
|
611
|
+
const m = n / 1_000_000;
|
|
612
|
+
return `${Math.round(m * 10) / 10}M`;
|
|
613
|
+
}
|
|
614
|
+
if (n >= 1000) {
|
|
615
|
+
const k = Math.round(n / 1000);
|
|
616
|
+
return `${k}K`;
|
|
617
|
+
}
|
|
618
|
+
return String(n);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Compact capability chips shown next to the model name (Dify parity: CHAT /
|
|
623
|
+
* Vision / etc.). Distinct from the "features" pillbox — we add a few
|
|
624
|
+
* type-inferred defaults on top so all LLMs get "CHAT" without needing every
|
|
625
|
+
* predefined row to carry it.
|
|
626
|
+
*/
|
|
627
|
+
function capabilityTags(
|
|
628
|
+
row: CatalogRow,
|
|
629
|
+
): { color: string; key: string; label: string }[] {
|
|
630
|
+
const out: { color: string; key: string; label: string }[] = [];
|
|
631
|
+
// LLMs default to CHAT mode (Dify convention). If the row explicitly declares
|
|
632
|
+
// COMPLETION or something else via features, we'd render that too.
|
|
633
|
+
if (row.modelType === 'LLM') {
|
|
634
|
+
out.push({ key: 'CHAT', label: 'CHAT', color: 'blue' });
|
|
635
|
+
}
|
|
636
|
+
const seen = new Set(out.map((t) => t.key));
|
|
637
|
+
for (const f of row.features ?? []) {
|
|
638
|
+
if (f === 'VISION' && !seen.has('VISION')) {
|
|
639
|
+
out.push({ key: 'VISION', label: 'Vision', color: 'purple' });
|
|
640
|
+
seen.add('VISION');
|
|
641
|
+
} else if (
|
|
642
|
+
(f === 'TOOL_CALL' || f === 'STREAM_TOOL_CALL') &&
|
|
643
|
+
!seen.has('TOOL')
|
|
644
|
+
) {
|
|
645
|
+
out.push({ key: 'TOOL', label: 'Tools', color: 'green' });
|
|
646
|
+
seen.add('TOOL');
|
|
647
|
+
} else if (f === 'STRUCTURED_OUTPUT' && !seen.has('JSON')) {
|
|
648
|
+
out.push({ key: 'JSON', label: 'JSON', color: 'orange' });
|
|
649
|
+
seen.add('JSON');
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return out;
|
|
653
|
+
}
|
|
654
|
+
</script>
|
|
655
|
+
|
|
656
|
+
<template>
|
|
657
|
+
<div class="phs-root">
|
|
658
|
+
<div class="phs-toolbar">
|
|
659
|
+
<div class="phs-toolbar-title">模型供应商</div>
|
|
660
|
+
<input
|
|
661
|
+
v-model="search"
|
|
662
|
+
class="phs-search"
|
|
663
|
+
placeholder="搜索"
|
|
664
|
+
/>
|
|
665
|
+
</div>
|
|
666
|
+
|
|
667
|
+
<!-- Section 0 · 系统默认模型 -->
|
|
668
|
+
<DefaultModelsPanel
|
|
669
|
+
ref="defaultsPanel"
|
|
670
|
+
:tenant-id="tenantId"
|
|
671
|
+
@change="onDefaultsChanged"
|
|
672
|
+
/>
|
|
673
|
+
|
|
674
|
+
<!-- Section 1 · 模型列表 (compact card grid; no click-to-open) -->
|
|
675
|
+
<div v-if="configured.length > 0" class="phs-section">
|
|
676
|
+
<div class="phs-section-title">模型列表</div>
|
|
677
|
+
<div class="phs-configured-grid">
|
|
678
|
+
<div v-for="p in configured" :key="p.name" class="phs-compact-card">
|
|
679
|
+
<div class="phs-compact-head">
|
|
680
|
+
<ProviderIcon
|
|
681
|
+
:name="p.name"
|
|
682
|
+
:size="36"
|
|
683
|
+
v-bind="providerIconProps(p)"
|
|
684
|
+
/>
|
|
685
|
+
<div class="phs-compact-title" :title="p.label">
|
|
686
|
+
{{ p.label }}
|
|
687
|
+
</div>
|
|
688
|
+
<button
|
|
689
|
+
class="phs-card-menu-btn"
|
|
690
|
+
title="更多操作"
|
|
691
|
+
@click.stop="toggleCardMenu(p.name, $event)"
|
|
692
|
+
>
|
|
693
|
+
⋯
|
|
694
|
+
</button>
|
|
695
|
+
<div
|
|
696
|
+
v-if="openMenuFor === p.name"
|
|
697
|
+
class="phs-card-menu"
|
|
698
|
+
@click.stop
|
|
699
|
+
>
|
|
700
|
+
<button class="phs-card-menu-item" @click="onRefreshCatalog(p); closeCardMenu()">
|
|
701
|
+
↻ 重新拉取模型
|
|
702
|
+
</button>
|
|
703
|
+
<button class="phs-card-menu-item" @click="openCredentialModal(p, true); closeCardMenu()">
|
|
704
|
+
🔑 编辑凭证
|
|
705
|
+
</button>
|
|
706
|
+
<button class="phs-card-menu-item" @click="openManualAdd(p); closeCardMenu()">
|
|
707
|
+
+ 手动添加模型
|
|
708
|
+
</button>
|
|
709
|
+
<div class="phs-card-menu-sep" />
|
|
710
|
+
<button class="phs-card-menu-item phs-card-menu-item--danger" @click="clearCredential(p); closeCardMenu()">
|
|
711
|
+
🗑 删除凭证
|
|
712
|
+
</button>
|
|
713
|
+
</div>
|
|
714
|
+
</div>
|
|
715
|
+
<div class="phs-compact-caps">
|
|
716
|
+
<span
|
|
717
|
+
v-for="t in distinctModelTypes(p)"
|
|
718
|
+
:key="t"
|
|
719
|
+
class="phs-cap"
|
|
720
|
+
:data-color="modelTypeColor(t)"
|
|
721
|
+
>
|
|
722
|
+
{{ modelTypeLabel(t) }}
|
|
723
|
+
</span>
|
|
724
|
+
</div>
|
|
725
|
+
|
|
726
|
+
<!-- Antdv 气泡卡片:click count → floating popover, NOT a modal -->
|
|
727
|
+
<Popover
|
|
728
|
+
:open="popoverProvider?.name === p.name"
|
|
729
|
+
trigger="click"
|
|
730
|
+
placement="rightTop"
|
|
731
|
+
overlay-class-name="phs-model-popover-overlay"
|
|
732
|
+
:arrow="false"
|
|
733
|
+
:destroy-tooltip-on-hide="true"
|
|
734
|
+
@update:open="(v: boolean) => (v ? openPopover(p) : closePopover())"
|
|
735
|
+
>
|
|
736
|
+
<template #content>
|
|
737
|
+
<div
|
|
738
|
+
v-if="popoverProvider?.name === p.name"
|
|
739
|
+
class="phs-popover-inner"
|
|
740
|
+
@click.stop
|
|
741
|
+
>
|
|
742
|
+
<header class="phs-popover-head">
|
|
743
|
+
<ProviderIcon
|
|
744
|
+
:name="p.name"
|
|
745
|
+
:size="32"
|
|
746
|
+
v-bind="providerIconProps(p)"
|
|
747
|
+
/>
|
|
748
|
+
<div class="phs-popover-head-body">
|
|
749
|
+
<div class="phs-popover-title">{{ p.label }}</div>
|
|
750
|
+
<div class="phs-popover-subtitle">
|
|
751
|
+
{{ enabledCount(p) }} 已启用 · 共
|
|
752
|
+
{{ catalogRows(p.name).length || knownModelCount(p) }} 个模型
|
|
753
|
+
</div>
|
|
754
|
+
</div>
|
|
755
|
+
</header>
|
|
756
|
+
|
|
757
|
+
<div class="phs-popover-actions">
|
|
758
|
+
<button
|
|
759
|
+
v-if="p.supportsRemoteModelListing"
|
|
760
|
+
class="phs-btn phs-btn-ghost"
|
|
761
|
+
:disabled="refreshingCatalog.has(p.name)"
|
|
762
|
+
@click="onRefreshCatalog(p)"
|
|
763
|
+
>
|
|
764
|
+
{{ refreshingCatalog.has(p.name) ? '拉取中…' : '↻ 重新拉取' }}
|
|
765
|
+
</button>
|
|
766
|
+
<button class="phs-btn phs-btn-ghost" @click="openManualAdd(p)">
|
|
767
|
+
+ 手动添加
|
|
768
|
+
</button>
|
|
769
|
+
</div>
|
|
770
|
+
|
|
771
|
+
<div class="phs-filter-bar">
|
|
772
|
+
<div class="phs-filter-group">
|
|
773
|
+
<button
|
|
774
|
+
class="phs-filter-tab"
|
|
775
|
+
:class="{ 'phs-filter-tab--on': enabledFilter === 'all' }"
|
|
776
|
+
@click="enabledFilter = 'all'"
|
|
777
|
+
>
|
|
778
|
+
全部 <span class="phs-filter-count">{{ countBy(p.name, 'all') }}</span>
|
|
779
|
+
</button>
|
|
780
|
+
<button
|
|
781
|
+
class="phs-filter-tab"
|
|
782
|
+
:class="{ 'phs-filter-tab--on': enabledFilter === 'enabled' }"
|
|
783
|
+
@click="enabledFilter = 'enabled'"
|
|
784
|
+
>
|
|
785
|
+
已启用 <span class="phs-filter-count">{{ countBy(p.name, 'enabled') }}</span>
|
|
786
|
+
</button>
|
|
787
|
+
<button
|
|
788
|
+
class="phs-filter-tab"
|
|
789
|
+
:class="{ 'phs-filter-tab--on': enabledFilter === 'disabled' }"
|
|
790
|
+
@click="enabledFilter = 'disabled'"
|
|
791
|
+
>
|
|
792
|
+
未启用 <span class="phs-filter-count">{{ countBy(p.name, 'disabled') }}</span>
|
|
793
|
+
</button>
|
|
794
|
+
</div>
|
|
795
|
+
<div class="phs-filter-group">
|
|
796
|
+
<button
|
|
797
|
+
class="phs-filter-tab"
|
|
798
|
+
:class="{ 'phs-filter-tab--on': typeFilter === 'all' }"
|
|
799
|
+
@click="typeFilter = 'all'"
|
|
800
|
+
>
|
|
801
|
+
所有类型
|
|
802
|
+
</button>
|
|
803
|
+
<button
|
|
804
|
+
v-for="t in availableTypesInCatalog(p.name)"
|
|
805
|
+
:key="t"
|
|
806
|
+
class="phs-filter-tab"
|
|
807
|
+
:class="{ 'phs-filter-tab--on': typeFilter === t }"
|
|
808
|
+
:data-color="modelTypeColor(t)"
|
|
809
|
+
@click="typeFilter = t"
|
|
810
|
+
>
|
|
811
|
+
{{ modelTypeLabel(t) }}
|
|
812
|
+
</button>
|
|
813
|
+
</div>
|
|
814
|
+
</div>
|
|
815
|
+
|
|
816
|
+
<div class="phs-popover-body">
|
|
817
|
+
<div v-if="catalogLoading.has(p.name)" class="phs-popover-empty">
|
|
818
|
+
加载目录中…
|
|
819
|
+
</div>
|
|
820
|
+
<template v-else-if="filteredCatalogRows(p.name).length === 0">
|
|
821
|
+
<div class="phs-popover-empty">
|
|
822
|
+
<template v-if="catalogRows(p.name).length === 0">
|
|
823
|
+
目录为空 —
|
|
824
|
+
{{
|
|
825
|
+
p.supportsRemoteModelListing
|
|
826
|
+
? '点击 "重新拉取" 从供应商获取列表'
|
|
827
|
+
: '点击 "手动添加" 录入具体模型名称'
|
|
828
|
+
}}
|
|
829
|
+
</template>
|
|
830
|
+
<template v-else>
|
|
831
|
+
当前过滤条件下没有匹配的模型
|
|
832
|
+
</template>
|
|
833
|
+
</div>
|
|
834
|
+
</template>
|
|
835
|
+
<template v-else>
|
|
836
|
+
<div class="phs-popover-list">
|
|
837
|
+
<div
|
|
838
|
+
v-for="row in filteredCatalogRows(p.name)"
|
|
839
|
+
:key="`${row.source}::${row.id}`"
|
|
840
|
+
class="phs-row"
|
|
841
|
+
:class="{ 'phs-row--on': row.enabled }"
|
|
842
|
+
>
|
|
843
|
+
<span
|
|
844
|
+
class="phs-row-icon"
|
|
845
|
+
:data-color="modelTypeColor(row.modelType)"
|
|
846
|
+
:title="modelTypeLabel(row.modelType)"
|
|
847
|
+
>
|
|
848
|
+
{{ modelTypeIcon(row.modelType) }}
|
|
849
|
+
</span>
|
|
850
|
+
<span class="phs-row-name" :title="row.model">
|
|
851
|
+
{{ row.model }}
|
|
852
|
+
</span>
|
|
853
|
+
<span
|
|
854
|
+
class="phs-row-cap"
|
|
855
|
+
:data-color="modelTypeColor(row.modelType)"
|
|
856
|
+
>
|
|
857
|
+
{{ modelTypeLabel(row.modelType) }}
|
|
858
|
+
</span>
|
|
859
|
+
<span
|
|
860
|
+
v-for="c in capabilityTags(row)"
|
|
861
|
+
:key="c.key"
|
|
862
|
+
class="phs-row-cap"
|
|
863
|
+
:data-color="c.color"
|
|
864
|
+
>
|
|
865
|
+
{{ c.label }}
|
|
866
|
+
</span>
|
|
867
|
+
<span
|
|
868
|
+
v-if="row.contextLength"
|
|
869
|
+
class="phs-row-cap phs-row-cap--ctx"
|
|
870
|
+
>
|
|
871
|
+
{{ formatContext(row.contextLength) }}
|
|
872
|
+
</span>
|
|
873
|
+
<span
|
|
874
|
+
v-if="row.source === 'remote'"
|
|
875
|
+
class="phs-row-cap phs-row-cap--remote"
|
|
876
|
+
title="从供应商 /models 接口新拉取的模型 · 启用后才写入数据库"
|
|
877
|
+
>
|
|
878
|
+
新增
|
|
879
|
+
</span>
|
|
880
|
+
<span
|
|
881
|
+
v-if="row.source === 'custom'"
|
|
882
|
+
class="phs-row-cap phs-row-cap--custom"
|
|
883
|
+
>
|
|
884
|
+
自定义
|
|
885
|
+
</span>
|
|
886
|
+
<span
|
|
887
|
+
v-if="row.isDefault"
|
|
888
|
+
class="phs-row-cap phs-row-cap--default"
|
|
889
|
+
>
|
|
890
|
+
默认
|
|
891
|
+
</span>
|
|
892
|
+
<button
|
|
893
|
+
v-if="row.source === 'custom'"
|
|
894
|
+
class="phs-row-gear"
|
|
895
|
+
title="参数 / 设为默认 / 删除"
|
|
896
|
+
@click.stop="openDrawer(catalogRowToModelEntity(row, p.name), p)"
|
|
897
|
+
>
|
|
898
|
+
⚙
|
|
899
|
+
</button>
|
|
900
|
+
<label class="phs-switch">
|
|
901
|
+
<input
|
|
902
|
+
type="checkbox"
|
|
903
|
+
:checked="row.enabled"
|
|
904
|
+
:disabled="togglingKey.has(toggleKey(row))"
|
|
905
|
+
@change="(e) => onToggleEnabled(p.name, row, (e.target as HTMLInputElement).checked)"
|
|
906
|
+
/>
|
|
907
|
+
<span class="phs-switch-slider" />
|
|
908
|
+
</label>
|
|
909
|
+
</div>
|
|
910
|
+
</div>
|
|
911
|
+
</template>
|
|
912
|
+
</div>
|
|
913
|
+
|
|
914
|
+
<footer class="phs-popover-foot">
|
|
915
|
+
<button
|
|
916
|
+
class="phs-btn phs-btn-ghost"
|
|
917
|
+
@click="openCredentialModal(p, true)"
|
|
918
|
+
>
|
|
919
|
+
编辑凭证
|
|
920
|
+
</button>
|
|
921
|
+
<button
|
|
922
|
+
class="phs-btn phs-btn-ghost phs-btn-danger"
|
|
923
|
+
@click="clearCredential(p)"
|
|
924
|
+
>
|
|
925
|
+
删除凭证
|
|
926
|
+
</button>
|
|
927
|
+
</footer>
|
|
928
|
+
</div>
|
|
929
|
+
</template>
|
|
930
|
+
|
|
931
|
+
<button
|
|
932
|
+
class="phs-count-btn"
|
|
933
|
+
title="点击查看模型列表"
|
|
934
|
+
@click.stop
|
|
935
|
+
>
|
|
936
|
+
<span class="phs-count-enabled">{{ enabledCount(p) }}</span>
|
|
937
|
+
<span class="phs-count-sep">/</span>
|
|
938
|
+
<span class="phs-count-total">{{ knownModelCount(p) }}</span>
|
|
939
|
+
<span class="phs-count-label">个模型 →</span>
|
|
940
|
+
</button>
|
|
941
|
+
</Popover>
|
|
942
|
+
</div>
|
|
943
|
+
</div>
|
|
944
|
+
</div>
|
|
945
|
+
|
|
946
|
+
<!-- Section 2 · 待配置 (same compact grid style) -->
|
|
947
|
+
<div v-if="pending.length > 0" class="phs-section">
|
|
948
|
+
<div class="phs-section-title">待配置</div>
|
|
949
|
+
<div class="phs-configured-grid">
|
|
950
|
+
<div
|
|
951
|
+
v-for="p in pending"
|
|
952
|
+
:key="p.name"
|
|
953
|
+
class="phs-compact-card phs-compact-card--pending"
|
|
954
|
+
>
|
|
955
|
+
<div class="phs-compact-head">
|
|
956
|
+
<ProviderIcon
|
|
957
|
+
:name="p.name"
|
|
958
|
+
:size="32"
|
|
959
|
+
v-bind="providerIconProps(p)"
|
|
960
|
+
/>
|
|
961
|
+
<div class="phs-compact-title" :title="p.label">
|
|
962
|
+
{{ p.label }}
|
|
963
|
+
</div>
|
|
964
|
+
</div>
|
|
965
|
+
<div class="phs-compact-caps">
|
|
966
|
+
<span
|
|
967
|
+
v-for="t in p.supportedModelTypes"
|
|
968
|
+
:key="t"
|
|
969
|
+
class="phs-cap"
|
|
970
|
+
:data-color="modelTypeColor(t)"
|
|
971
|
+
>
|
|
972
|
+
{{ modelTypeLabel(t) }}
|
|
973
|
+
</span>
|
|
974
|
+
</div>
|
|
975
|
+
<button
|
|
976
|
+
class="phs-compact-pending"
|
|
977
|
+
@click.stop="p.supportsRemoteModelListing ? onRefreshCatalog(p) : openManualAdd(p)"
|
|
978
|
+
>
|
|
979
|
+
{{ p.supportsRemoteModelListing ? '↻ 点击拉取模型' : '+ 手动添加模型' }}
|
|
980
|
+
</button>
|
|
981
|
+
</div>
|
|
982
|
+
</div>
|
|
983
|
+
</div>
|
|
984
|
+
|
|
985
|
+
<!-- Section 3 · 安装模型供应商 -->
|
|
986
|
+
<div v-if="installable.length > 0" class="phs-section">
|
|
987
|
+
<div class="phs-section-title">安装模型供应商</div>
|
|
988
|
+
<div class="phs-installable-grid">
|
|
989
|
+
<div
|
|
990
|
+
v-for="p in installable"
|
|
991
|
+
:key="p.name"
|
|
992
|
+
class="phs-installable-card"
|
|
993
|
+
>
|
|
994
|
+
<div class="phs-installable-head">
|
|
995
|
+
<ProviderIcon
|
|
996
|
+
:name="p.name"
|
|
997
|
+
:size="32"
|
|
998
|
+
v-bind="providerIconProps(p)"
|
|
999
|
+
/>
|
|
1000
|
+
<div class="phs-installable-name">{{ p.label }}</div>
|
|
1001
|
+
</div>
|
|
1002
|
+
<div class="phs-installable-desc">
|
|
1003
|
+
{{ p.name }}
|
|
1004
|
+
<span v-if="p.supportsRemoteModelListing" class="phs-installable-badge">
|
|
1005
|
+
自动拉取
|
|
1006
|
+
</span>
|
|
1007
|
+
</div>
|
|
1008
|
+
<div class="phs-installable-caps">
|
|
1009
|
+
<span
|
|
1010
|
+
v-for="t in p.supportedModelTypes"
|
|
1011
|
+
:key="t"
|
|
1012
|
+
class="phs-cap"
|
|
1013
|
+
:data-color="modelTypeColor(t)"
|
|
1014
|
+
>
|
|
1015
|
+
{{ modelTypeLabel(t) }}
|
|
1016
|
+
</span>
|
|
1017
|
+
</div>
|
|
1018
|
+
<div class="phs-installable-footer">
|
|
1019
|
+
<button
|
|
1020
|
+
class="phs-btn phs-btn-ghost"
|
|
1021
|
+
@click="openCredentialModal(p)"
|
|
1022
|
+
>
|
|
1023
|
+
添加 API 密钥
|
|
1024
|
+
</button>
|
|
1025
|
+
</div>
|
|
1026
|
+
</div>
|
|
1027
|
+
</div>
|
|
1028
|
+
</div>
|
|
1029
|
+
|
|
1030
|
+
<div v-if="loading" class="phs-loading">加载中…</div>
|
|
1031
|
+
<div v-else-if="providers.length === 0" class="phs-empty">
|
|
1032
|
+
没有可用的供应商
|
|
1033
|
+
</div>
|
|
1034
|
+
|
|
1035
|
+
<!-- Catalog popover is now rendered inline per-card via <Popover>. -->
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
<ProviderCredentialModal
|
|
1039
|
+
v-model:open="modalOpen"
|
|
1040
|
+
:provider="modalProvider"
|
|
1041
|
+
:edit-mode="modalEditMode"
|
|
1042
|
+
:tenant-id="tenantId"
|
|
1043
|
+
@saved="onCredentialSaved"
|
|
1044
|
+
/>
|
|
1045
|
+
|
|
1046
|
+
<ModelParameterDrawer
|
|
1047
|
+
v-model:open="drawerOpen"
|
|
1048
|
+
:model="drawerModel"
|
|
1049
|
+
:provider="drawerProvider"
|
|
1050
|
+
@saved="onDrawerSaved"
|
|
1051
|
+
@deleted="onDrawerDeleted"
|
|
1052
|
+
@defaulted="onDrawerSaved"
|
|
1053
|
+
/>
|
|
1054
|
+
|
|
1055
|
+
<!-- Manual add mini-modal -->
|
|
1056
|
+
<div
|
|
1057
|
+
v-if="manualAddOpen"
|
|
1058
|
+
class="phs-mini-backdrop"
|
|
1059
|
+
@click.self="manualAddOpen = false"
|
|
1060
|
+
>
|
|
1061
|
+
<div class="phs-mini-modal">
|
|
1062
|
+
<div class="phs-mini-header">
|
|
1063
|
+
<div class="phs-mini-title">
|
|
1064
|
+
手动添加模型 · {{ manualAddProvider?.label }}
|
|
1065
|
+
</div>
|
|
1066
|
+
<button class="phs-mini-close" @click="manualAddOpen = false">✕</button>
|
|
1067
|
+
</div>
|
|
1068
|
+
<div class="phs-mini-body">
|
|
1069
|
+
<div class="phs-mini-hint">
|
|
1070
|
+
用于目录里没有的自定义模型(自建 endpoint / 未公开的模型名)。
|
|
1071
|
+
</div>
|
|
1072
|
+
<div class="phs-mini-field">
|
|
1073
|
+
<label>模型名称 <span class="phs-req">*</span></label>
|
|
1074
|
+
<input
|
|
1075
|
+
v-model="manualAddForm.modelName"
|
|
1076
|
+
class="phs-mini-input"
|
|
1077
|
+
placeholder="例如 gpt-4o-2025-preview"
|
|
1078
|
+
/>
|
|
1079
|
+
</div>
|
|
1080
|
+
<div class="phs-mini-field">
|
|
1081
|
+
<label>模型类型</label>
|
|
1082
|
+
<select v-model="manualAddForm.modelType" class="phs-mini-input">
|
|
1083
|
+
<option
|
|
1084
|
+
v-for="t in manualAddProvider?.supportedModelTypes ?? []"
|
|
1085
|
+
:key="t"
|
|
1086
|
+
:value="t"
|
|
1087
|
+
>
|
|
1088
|
+
{{ modelTypeLabel(t) }}
|
|
1089
|
+
</option>
|
|
1090
|
+
</select>
|
|
1091
|
+
</div>
|
|
1092
|
+
<div v-if="manualAddError" class="phs-mini-error">
|
|
1093
|
+
{{ manualAddError }}
|
|
1094
|
+
</div>
|
|
1095
|
+
</div>
|
|
1096
|
+
<div class="phs-mini-footer">
|
|
1097
|
+
<button class="phs-btn" @click="manualAddOpen = false">取消</button>
|
|
1098
|
+
<button
|
|
1099
|
+
class="phs-btn phs-btn-primary"
|
|
1100
|
+
:disabled="manualAddSaving"
|
|
1101
|
+
@click="submitManualAdd"
|
|
1102
|
+
>
|
|
1103
|
+
{{ manualAddSaving ? '保存中…' : '添加' }}
|
|
1104
|
+
</button>
|
|
1105
|
+
</div>
|
|
1106
|
+
</div>
|
|
1107
|
+
</div>
|
|
1108
|
+
</div>
|
|
1109
|
+
</template>
|
|
1110
|
+
|
|
1111
|
+
<style scoped>
|
|
1112
|
+
.phs-root {
|
|
1113
|
+
display: flex;
|
|
1114
|
+
flex-direction: column;
|
|
1115
|
+
gap: 20px;
|
|
1116
|
+
}
|
|
1117
|
+
.phs-toolbar {
|
|
1118
|
+
display: flex;
|
|
1119
|
+
justify-content: space-between;
|
|
1120
|
+
align-items: center;
|
|
1121
|
+
gap: 12px;
|
|
1122
|
+
}
|
|
1123
|
+
.phs-toolbar-title {
|
|
1124
|
+
font-size: 18px;
|
|
1125
|
+
font-weight: 600;
|
|
1126
|
+
color: #111827;
|
|
1127
|
+
}
|
|
1128
|
+
:global(.dark) .phs-toolbar-title {
|
|
1129
|
+
color: #f3f4f6;
|
|
1130
|
+
}
|
|
1131
|
+
.phs-search {
|
|
1132
|
+
padding: 6px 12px;
|
|
1133
|
+
font-size: 13px;
|
|
1134
|
+
border: 1px solid #d1d5db;
|
|
1135
|
+
border-radius: 6px;
|
|
1136
|
+
width: 220px;
|
|
1137
|
+
background: #fff;
|
|
1138
|
+
}
|
|
1139
|
+
:global(.dark) .phs-search {
|
|
1140
|
+
background: #2d2d2d;
|
|
1141
|
+
color: #f3f4f6;
|
|
1142
|
+
border-color: #3d3d3d;
|
|
1143
|
+
}
|
|
1144
|
+
.phs-section {
|
|
1145
|
+
display: flex;
|
|
1146
|
+
flex-direction: column;
|
|
1147
|
+
gap: 8px;
|
|
1148
|
+
}
|
|
1149
|
+
.phs-section-title {
|
|
1150
|
+
font-size: 14px;
|
|
1151
|
+
font-weight: 600;
|
|
1152
|
+
color: #374151;
|
|
1153
|
+
margin-bottom: 4px;
|
|
1154
|
+
}
|
|
1155
|
+
:global(.dark) .phs-section-title {
|
|
1156
|
+
color: #d1d5db;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/* -------- compact grid (larger cards, ~3-4 per row) -------- */
|
|
1160
|
+
.phs-configured-grid {
|
|
1161
|
+
display: grid;
|
|
1162
|
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
1163
|
+
gap: 12px;
|
|
1164
|
+
}
|
|
1165
|
+
.phs-compact-card {
|
|
1166
|
+
padding: 16px 18px;
|
|
1167
|
+
background: #fff;
|
|
1168
|
+
border: 1px solid #e5e7eb;
|
|
1169
|
+
border-radius: 12px;
|
|
1170
|
+
cursor: pointer;
|
|
1171
|
+
display: flex;
|
|
1172
|
+
flex-direction: column;
|
|
1173
|
+
gap: 12px;
|
|
1174
|
+
min-height: 110px;
|
|
1175
|
+
position: relative;
|
|
1176
|
+
transition:
|
|
1177
|
+
box-shadow 0.15s ease,
|
|
1178
|
+
transform 0.15s ease,
|
|
1179
|
+
border-color 0.15s ease;
|
|
1180
|
+
}
|
|
1181
|
+
.phs-compact-card:hover {
|
|
1182
|
+
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08);
|
|
1183
|
+
border-color: #a5b4fc;
|
|
1184
|
+
transform: translateY(-1px);
|
|
1185
|
+
}
|
|
1186
|
+
:global(.dark) .phs-compact-card {
|
|
1187
|
+
background: #1f1f1f;
|
|
1188
|
+
border-color: #2d2d2d;
|
|
1189
|
+
}
|
|
1190
|
+
:global(.dark) .phs-compact-card:hover {
|
|
1191
|
+
border-color: #6366f1;
|
|
1192
|
+
}
|
|
1193
|
+
.phs-compact-card--pending {
|
|
1194
|
+
border-style: dashed;
|
|
1195
|
+
background: #fafafa;
|
|
1196
|
+
}
|
|
1197
|
+
:global(.dark) .phs-compact-card--pending {
|
|
1198
|
+
background: #191919;
|
|
1199
|
+
}
|
|
1200
|
+
.phs-compact-head {
|
|
1201
|
+
display: flex;
|
|
1202
|
+
align-items: center;
|
|
1203
|
+
gap: 8px;
|
|
1204
|
+
min-width: 0;
|
|
1205
|
+
}
|
|
1206
|
+
.phs-compact-title {
|
|
1207
|
+
font-size: 14px;
|
|
1208
|
+
font-weight: 600;
|
|
1209
|
+
color: #111827;
|
|
1210
|
+
white-space: nowrap;
|
|
1211
|
+
overflow: hidden;
|
|
1212
|
+
text-overflow: ellipsis;
|
|
1213
|
+
flex: 1;
|
|
1214
|
+
min-width: 0;
|
|
1215
|
+
}
|
|
1216
|
+
:global(.dark) .phs-compact-title {
|
|
1217
|
+
color: #f3f4f6;
|
|
1218
|
+
}
|
|
1219
|
+
.phs-compact-caps {
|
|
1220
|
+
display: flex;
|
|
1221
|
+
gap: 4px;
|
|
1222
|
+
flex-wrap: wrap;
|
|
1223
|
+
}
|
|
1224
|
+
.phs-count-btn {
|
|
1225
|
+
background: transparent;
|
|
1226
|
+
border: 1px dashed #d1d5db;
|
|
1227
|
+
border-radius: 8px;
|
|
1228
|
+
padding: 8px 12px;
|
|
1229
|
+
font-size: 13px;
|
|
1230
|
+
color: #6b7280;
|
|
1231
|
+
display: flex;
|
|
1232
|
+
align-items: baseline;
|
|
1233
|
+
gap: 3px;
|
|
1234
|
+
cursor: pointer;
|
|
1235
|
+
transition: border-color 0.15s ease, background 0.15s ease;
|
|
1236
|
+
margin-top: auto;
|
|
1237
|
+
}
|
|
1238
|
+
.phs-count-btn:hover {
|
|
1239
|
+
border-color: #6366f1;
|
|
1240
|
+
background: #eef2ff;
|
|
1241
|
+
}
|
|
1242
|
+
.phs-count-enabled {
|
|
1243
|
+
font-weight: 700;
|
|
1244
|
+
color: #059669;
|
|
1245
|
+
font-size: 16px;
|
|
1246
|
+
}
|
|
1247
|
+
.phs-count-sep,
|
|
1248
|
+
.phs-count-total {
|
|
1249
|
+
color: #9ca3af;
|
|
1250
|
+
font-size: 14px;
|
|
1251
|
+
}
|
|
1252
|
+
.phs-count-label {
|
|
1253
|
+
margin-left: 6px;
|
|
1254
|
+
color: #6b7280;
|
|
1255
|
+
}
|
|
1256
|
+
.phs-compact-pending {
|
|
1257
|
+
font-size: 13px;
|
|
1258
|
+
color: #b45309;
|
|
1259
|
+
background: transparent;
|
|
1260
|
+
border: 1px dashed #fbbf24;
|
|
1261
|
+
border-radius: 8px;
|
|
1262
|
+
padding: 8px 12px;
|
|
1263
|
+
cursor: pointer;
|
|
1264
|
+
transition: background 0.15s ease, border-color 0.15s ease;
|
|
1265
|
+
margin-top: auto;
|
|
1266
|
+
}
|
|
1267
|
+
.phs-compact-pending:hover {
|
|
1268
|
+
background: #fef3c7;
|
|
1269
|
+
border-color: #d97706;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/* ⋯ menu on each card */
|
|
1273
|
+
.phs-card-menu-btn {
|
|
1274
|
+
background: transparent;
|
|
1275
|
+
border: none;
|
|
1276
|
+
color: #9ca3af;
|
|
1277
|
+
cursor: pointer;
|
|
1278
|
+
font-size: 20px;
|
|
1279
|
+
padding: 0 6px;
|
|
1280
|
+
border-radius: 6px;
|
|
1281
|
+
line-height: 1;
|
|
1282
|
+
flex-shrink: 0;
|
|
1283
|
+
}
|
|
1284
|
+
.phs-card-menu-btn:hover {
|
|
1285
|
+
background: #f3f4f6;
|
|
1286
|
+
color: #374151;
|
|
1287
|
+
}
|
|
1288
|
+
.phs-card-menu {
|
|
1289
|
+
position: absolute;
|
|
1290
|
+
top: 44px;
|
|
1291
|
+
right: 12px;
|
|
1292
|
+
min-width: 180px;
|
|
1293
|
+
background: #fff;
|
|
1294
|
+
border: 1px solid #e5e7eb;
|
|
1295
|
+
border-radius: 8px;
|
|
1296
|
+
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
|
|
1297
|
+
z-index: 10;
|
|
1298
|
+
padding: 4px;
|
|
1299
|
+
display: flex;
|
|
1300
|
+
flex-direction: column;
|
|
1301
|
+
gap: 2px;
|
|
1302
|
+
}
|
|
1303
|
+
:global(.dark) .phs-card-menu {
|
|
1304
|
+
background: #1f1f1f;
|
|
1305
|
+
border-color: #2d2d2d;
|
|
1306
|
+
}
|
|
1307
|
+
.phs-card-menu-item {
|
|
1308
|
+
padding: 8px 12px;
|
|
1309
|
+
font-size: 13px;
|
|
1310
|
+
text-align: left;
|
|
1311
|
+
background: transparent;
|
|
1312
|
+
border: none;
|
|
1313
|
+
border-radius: 6px;
|
|
1314
|
+
cursor: pointer;
|
|
1315
|
+
color: #374151;
|
|
1316
|
+
}
|
|
1317
|
+
.phs-card-menu-item:hover {
|
|
1318
|
+
background: #f3f4f6;
|
|
1319
|
+
}
|
|
1320
|
+
:global(.dark) .phs-card-menu-item {
|
|
1321
|
+
color: #d1d5db;
|
|
1322
|
+
}
|
|
1323
|
+
:global(.dark) .phs-card-menu-item:hover {
|
|
1324
|
+
background: #2d2d2d;
|
|
1325
|
+
}
|
|
1326
|
+
.phs-card-menu-item--danger {
|
|
1327
|
+
color: #dc2626;
|
|
1328
|
+
}
|
|
1329
|
+
.phs-card-menu-item--danger:hover {
|
|
1330
|
+
background: #fef2f2;
|
|
1331
|
+
}
|
|
1332
|
+
.phs-card-menu-sep {
|
|
1333
|
+
height: 1px;
|
|
1334
|
+
background: #e5e7eb;
|
|
1335
|
+
margin: 4px 0;
|
|
1336
|
+
}
|
|
1337
|
+
:global(.dark) .phs-card-menu-sep {
|
|
1338
|
+
background: #2d2d2d;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
/* Popover filter tab bar */
|
|
1342
|
+
.phs-filter-bar {
|
|
1343
|
+
padding: 10px 18px;
|
|
1344
|
+
border-bottom: 1px solid #e5e7eb;
|
|
1345
|
+
display: flex;
|
|
1346
|
+
flex-direction: column;
|
|
1347
|
+
gap: 6px;
|
|
1348
|
+
}
|
|
1349
|
+
:global(.dark) .phs-filter-bar {
|
|
1350
|
+
border-bottom-color: #2d2d2d;
|
|
1351
|
+
}
|
|
1352
|
+
.phs-filter-group {
|
|
1353
|
+
display: flex;
|
|
1354
|
+
gap: 4px;
|
|
1355
|
+
flex-wrap: wrap;
|
|
1356
|
+
}
|
|
1357
|
+
.phs-filter-tab {
|
|
1358
|
+
background: #f3f4f6;
|
|
1359
|
+
border: 1px solid transparent;
|
|
1360
|
+
border-radius: 6px;
|
|
1361
|
+
padding: 4px 10px;
|
|
1362
|
+
font-size: 12px;
|
|
1363
|
+
color: #374151;
|
|
1364
|
+
cursor: pointer;
|
|
1365
|
+
display: flex;
|
|
1366
|
+
align-items: center;
|
|
1367
|
+
gap: 4px;
|
|
1368
|
+
}
|
|
1369
|
+
.phs-filter-tab:hover {
|
|
1370
|
+
background: #e5e7eb;
|
|
1371
|
+
}
|
|
1372
|
+
.phs-filter-tab--on {
|
|
1373
|
+
background: #eef2ff;
|
|
1374
|
+
border-color: #6366f1;
|
|
1375
|
+
color: #4338ca;
|
|
1376
|
+
font-weight: 500;
|
|
1377
|
+
}
|
|
1378
|
+
:global(.dark) .phs-filter-tab {
|
|
1379
|
+
background: #262626;
|
|
1380
|
+
color: #d1d5db;
|
|
1381
|
+
}
|
|
1382
|
+
:global(.dark) .phs-filter-tab:hover {
|
|
1383
|
+
background: #2d2d2d;
|
|
1384
|
+
}
|
|
1385
|
+
:global(.dark) .phs-filter-tab--on {
|
|
1386
|
+
background: #312e81;
|
|
1387
|
+
color: #a5b4fc;
|
|
1388
|
+
}
|
|
1389
|
+
.phs-filter-count {
|
|
1390
|
+
background: rgba(255, 255, 255, 0.6);
|
|
1391
|
+
padding: 0 5px;
|
|
1392
|
+
border-radius: 999px;
|
|
1393
|
+
font-size: 10.5px;
|
|
1394
|
+
color: #6b7280;
|
|
1395
|
+
min-width: 18px;
|
|
1396
|
+
text-align: center;
|
|
1397
|
+
}
|
|
1398
|
+
.phs-filter-tab--on .phs-filter-count {
|
|
1399
|
+
background: #fff;
|
|
1400
|
+
color: #4338ca;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
|
|
1404
|
+
.phs-cap {
|
|
1405
|
+
padding: 1px 8px;
|
|
1406
|
+
font-size: 10px;
|
|
1407
|
+
font-weight: 500;
|
|
1408
|
+
border-radius: 4px;
|
|
1409
|
+
background: #eef2ff;
|
|
1410
|
+
color: #4338ca;
|
|
1411
|
+
letter-spacing: 0.5px;
|
|
1412
|
+
}
|
|
1413
|
+
.phs-cap[data-color='green'] {
|
|
1414
|
+
background: #ecfdf5;
|
|
1415
|
+
color: #059669;
|
|
1416
|
+
}
|
|
1417
|
+
.phs-cap[data-color='orange'] {
|
|
1418
|
+
background: #fff7ed;
|
|
1419
|
+
color: #c2410c;
|
|
1420
|
+
}
|
|
1421
|
+
.phs-cap[data-color='purple'] {
|
|
1422
|
+
background: #faf5ff;
|
|
1423
|
+
color: #7c3aed;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
.phs-btn {
|
|
1427
|
+
padding: 5px 12px;
|
|
1428
|
+
font-size: 12px;
|
|
1429
|
+
border-radius: 6px;
|
|
1430
|
+
border: 1px solid #d1d5db;
|
|
1431
|
+
background: #fff;
|
|
1432
|
+
color: #374151;
|
|
1433
|
+
cursor: pointer;
|
|
1434
|
+
white-space: nowrap;
|
|
1435
|
+
}
|
|
1436
|
+
.phs-btn:hover {
|
|
1437
|
+
background: #f9fafb;
|
|
1438
|
+
}
|
|
1439
|
+
.phs-btn:disabled {
|
|
1440
|
+
opacity: 0.5;
|
|
1441
|
+
cursor: not-allowed;
|
|
1442
|
+
}
|
|
1443
|
+
.phs-btn-ghost {
|
|
1444
|
+
background: transparent;
|
|
1445
|
+
}
|
|
1446
|
+
.phs-btn-primary {
|
|
1447
|
+
background: #6366f1;
|
|
1448
|
+
border-color: #6366f1;
|
|
1449
|
+
color: #fff;
|
|
1450
|
+
}
|
|
1451
|
+
.phs-btn-primary:hover {
|
|
1452
|
+
background: #4f46e5;
|
|
1453
|
+
}
|
|
1454
|
+
.phs-btn-danger {
|
|
1455
|
+
color: #dc2626;
|
|
1456
|
+
}
|
|
1457
|
+
.phs-btn-danger:hover {
|
|
1458
|
+
background: #fef2f2;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
/* -------- installable grid -------- */
|
|
1462
|
+
.phs-installable-grid {
|
|
1463
|
+
display: grid;
|
|
1464
|
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
1465
|
+
gap: 10px;
|
|
1466
|
+
}
|
|
1467
|
+
.phs-installable-card {
|
|
1468
|
+
padding: 12px 14px;
|
|
1469
|
+
background: #fff;
|
|
1470
|
+
border: 1px solid #e5e7eb;
|
|
1471
|
+
border-radius: 10px;
|
|
1472
|
+
display: flex;
|
|
1473
|
+
flex-direction: column;
|
|
1474
|
+
gap: 8px;
|
|
1475
|
+
}
|
|
1476
|
+
:global(.dark) .phs-installable-card {
|
|
1477
|
+
background: #1f1f1f;
|
|
1478
|
+
border-color: #2d2d2d;
|
|
1479
|
+
}
|
|
1480
|
+
.phs-installable-head {
|
|
1481
|
+
display: flex;
|
|
1482
|
+
align-items: center;
|
|
1483
|
+
gap: 8px;
|
|
1484
|
+
}
|
|
1485
|
+
.phs-installable-name {
|
|
1486
|
+
font-size: 14px;
|
|
1487
|
+
font-weight: 600;
|
|
1488
|
+
color: #111827;
|
|
1489
|
+
white-space: nowrap;
|
|
1490
|
+
overflow: hidden;
|
|
1491
|
+
text-overflow: ellipsis;
|
|
1492
|
+
flex: 1;
|
|
1493
|
+
min-width: 0;
|
|
1494
|
+
}
|
|
1495
|
+
:global(.dark) .phs-installable-name {
|
|
1496
|
+
color: #f3f4f6;
|
|
1497
|
+
}
|
|
1498
|
+
.phs-installable-desc {
|
|
1499
|
+
font-size: 11px;
|
|
1500
|
+
color: #6b7280;
|
|
1501
|
+
display: flex;
|
|
1502
|
+
align-items: center;
|
|
1503
|
+
gap: 6px;
|
|
1504
|
+
}
|
|
1505
|
+
.phs-installable-badge {
|
|
1506
|
+
padding: 0 6px;
|
|
1507
|
+
background: #ecfdf5;
|
|
1508
|
+
color: #059669;
|
|
1509
|
+
border-radius: 3px;
|
|
1510
|
+
font-weight: 500;
|
|
1511
|
+
}
|
|
1512
|
+
.phs-installable-caps {
|
|
1513
|
+
display: flex;
|
|
1514
|
+
gap: 4px;
|
|
1515
|
+
flex-wrap: wrap;
|
|
1516
|
+
}
|
|
1517
|
+
.phs-installable-footer {
|
|
1518
|
+
margin-top: auto;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
.phs-loading,
|
|
1522
|
+
.phs-empty {
|
|
1523
|
+
padding: 40px 0;
|
|
1524
|
+
text-align: center;
|
|
1525
|
+
color: #9ca3af;
|
|
1526
|
+
font-size: 13px;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/* -------- Shared popover header / body / actions styling -------- */
|
|
1530
|
+
.phs-popover-head {
|
|
1531
|
+
display: flex;
|
|
1532
|
+
align-items: center;
|
|
1533
|
+
gap: 12px;
|
|
1534
|
+
padding: 14px 18px;
|
|
1535
|
+
border-bottom: 1px solid #e5e7eb;
|
|
1536
|
+
}
|
|
1537
|
+
:global(.dark) .phs-popover-head {
|
|
1538
|
+
border-bottom-color: #2d2d2d;
|
|
1539
|
+
}
|
|
1540
|
+
.phs-popover-head-body {
|
|
1541
|
+
flex: 1;
|
|
1542
|
+
min-width: 0;
|
|
1543
|
+
}
|
|
1544
|
+
.phs-popover-title {
|
|
1545
|
+
font-size: 16px;
|
|
1546
|
+
font-weight: 600;
|
|
1547
|
+
color: #111827;
|
|
1548
|
+
white-space: nowrap;
|
|
1549
|
+
overflow: hidden;
|
|
1550
|
+
text-overflow: ellipsis;
|
|
1551
|
+
}
|
|
1552
|
+
:global(.dark) .phs-popover-title {
|
|
1553
|
+
color: #f3f4f6;
|
|
1554
|
+
}
|
|
1555
|
+
.phs-popover-subtitle {
|
|
1556
|
+
margin-top: 2px;
|
|
1557
|
+
font-size: 12px;
|
|
1558
|
+
color: #6b7280;
|
|
1559
|
+
}
|
|
1560
|
+
.phs-popover-actions {
|
|
1561
|
+
display: flex;
|
|
1562
|
+
gap: 6px;
|
|
1563
|
+
padding: 10px 18px;
|
|
1564
|
+
border-bottom: 1px solid #e5e7eb;
|
|
1565
|
+
flex-wrap: wrap;
|
|
1566
|
+
flex-shrink: 0;
|
|
1567
|
+
}
|
|
1568
|
+
:global(.dark) .phs-popover-actions {
|
|
1569
|
+
border-bottom-color: #2d2d2d;
|
|
1570
|
+
}
|
|
1571
|
+
.phs-popover-body {
|
|
1572
|
+
flex: 1;
|
|
1573
|
+
overflow-y: auto;
|
|
1574
|
+
padding: 14px 18px 18px;
|
|
1575
|
+
}
|
|
1576
|
+
.phs-popover-empty {
|
|
1577
|
+
padding: 40px 0;
|
|
1578
|
+
text-align: center;
|
|
1579
|
+
color: #9ca3af;
|
|
1580
|
+
font-size: 13px;
|
|
1581
|
+
line-height: 1.6;
|
|
1582
|
+
}
|
|
1583
|
+
.phs-popover-list {
|
|
1584
|
+
display: flex;
|
|
1585
|
+
flex-direction: column;
|
|
1586
|
+
gap: 6px;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/* Antdv Popover container — override defaults so our content fits nicely */
|
|
1590
|
+
:global(.phs-model-popover-overlay .ant-popover-inner) {
|
|
1591
|
+
padding: 0;
|
|
1592
|
+
border-radius: 12px;
|
|
1593
|
+
box-shadow: 0 12px 40px rgba(15, 23, 42, 0.18);
|
|
1594
|
+
}
|
|
1595
|
+
:global(.phs-model-popover-overlay .ant-popover-inner-content) {
|
|
1596
|
+
padding: 0;
|
|
1597
|
+
}
|
|
1598
|
+
.phs-popover-inner {
|
|
1599
|
+
width: 480px;
|
|
1600
|
+
max-width: 90vw;
|
|
1601
|
+
max-height: 78vh;
|
|
1602
|
+
display: flex;
|
|
1603
|
+
flex-direction: column;
|
|
1604
|
+
overflow: hidden;
|
|
1605
|
+
}
|
|
1606
|
+
.phs-popover-inner .phs-popover-head {
|
|
1607
|
+
padding: 12px 16px;
|
|
1608
|
+
border-bottom: 1px solid #e5e7eb;
|
|
1609
|
+
}
|
|
1610
|
+
.phs-popover-inner .phs-popover-body {
|
|
1611
|
+
flex: 1;
|
|
1612
|
+
overflow-y: auto;
|
|
1613
|
+
padding: 12px 16px;
|
|
1614
|
+
}
|
|
1615
|
+
.phs-popover-inner .phs-popover-actions {
|
|
1616
|
+
padding: 8px 16px;
|
|
1617
|
+
border-bottom: 1px solid #e5e7eb;
|
|
1618
|
+
display: flex;
|
|
1619
|
+
gap: 6px;
|
|
1620
|
+
}
|
|
1621
|
+
.phs-popover-foot {
|
|
1622
|
+
padding: 8px 16px;
|
|
1623
|
+
border-top: 1px solid #e5e7eb;
|
|
1624
|
+
display: flex;
|
|
1625
|
+
justify-content: flex-end;
|
|
1626
|
+
gap: 6px;
|
|
1627
|
+
flex-shrink: 0;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
/* Row: single-line — icon | name (flex:1) | tags | gear | switch */
|
|
1631
|
+
.phs-row {
|
|
1632
|
+
display: flex;
|
|
1633
|
+
align-items: center;
|
|
1634
|
+
gap: 6px;
|
|
1635
|
+
padding: 6px 10px;
|
|
1636
|
+
background: #fff;
|
|
1637
|
+
border: 1px solid #e5e7eb;
|
|
1638
|
+
border-radius: 8px;
|
|
1639
|
+
transition: background 0.15s ease, border-color 0.15s ease;
|
|
1640
|
+
overflow: hidden;
|
|
1641
|
+
}
|
|
1642
|
+
.phs-row:hover {
|
|
1643
|
+
background: #fafafa;
|
|
1644
|
+
}
|
|
1645
|
+
.phs-row--on {
|
|
1646
|
+
background: #eef2ff;
|
|
1647
|
+
border-color: #a5b4fc;
|
|
1648
|
+
}
|
|
1649
|
+
:global(.dark) .phs-row {
|
|
1650
|
+
background: #1f1f1f;
|
|
1651
|
+
border-color: #2d2d2d;
|
|
1652
|
+
}
|
|
1653
|
+
:global(.dark) .phs-row:hover {
|
|
1654
|
+
background: #262626;
|
|
1655
|
+
}
|
|
1656
|
+
:global(.dark) .phs-row--on {
|
|
1657
|
+
background: #312e81;
|
|
1658
|
+
border-color: #6366f1;
|
|
1659
|
+
}
|
|
1660
|
+
.phs-row-icon {
|
|
1661
|
+
width: 32px;
|
|
1662
|
+
height: 32px;
|
|
1663
|
+
border-radius: 8px;
|
|
1664
|
+
background: #eef2ff;
|
|
1665
|
+
color: #4338ca;
|
|
1666
|
+
font-size: 16px;
|
|
1667
|
+
display: inline-flex;
|
|
1668
|
+
align-items: center;
|
|
1669
|
+
justify-content: center;
|
|
1670
|
+
flex-shrink: 0;
|
|
1671
|
+
}
|
|
1672
|
+
.phs-row-icon[data-color='green'] {
|
|
1673
|
+
background: #ecfdf5;
|
|
1674
|
+
color: #059669;
|
|
1675
|
+
}
|
|
1676
|
+
.phs-row-icon[data-color='orange'] {
|
|
1677
|
+
background: #fff7ed;
|
|
1678
|
+
color: #c2410c;
|
|
1679
|
+
}
|
|
1680
|
+
.phs-row-icon[data-color='purple'] {
|
|
1681
|
+
background: #faf5ff;
|
|
1682
|
+
color: #7c3aed;
|
|
1683
|
+
}
|
|
1684
|
+
.phs-row-name {
|
|
1685
|
+
flex: 1;
|
|
1686
|
+
min-width: 0;
|
|
1687
|
+
font-size: 13px;
|
|
1688
|
+
font-weight: 500;
|
|
1689
|
+
color: #111827;
|
|
1690
|
+
font-family:
|
|
1691
|
+
ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
|
|
1692
|
+
white-space: nowrap;
|
|
1693
|
+
overflow: hidden;
|
|
1694
|
+
text-overflow: ellipsis;
|
|
1695
|
+
}
|
|
1696
|
+
:global(.dark) .phs-row-name {
|
|
1697
|
+
color: #f3f4f6;
|
|
1698
|
+
}
|
|
1699
|
+
.phs-row-cap {
|
|
1700
|
+
flex-shrink: 0;
|
|
1701
|
+
padding: 1px 6px;
|
|
1702
|
+
font-size: 10px;
|
|
1703
|
+
font-weight: 500;
|
|
1704
|
+
border-radius: 3px;
|
|
1705
|
+
background: #eef2ff;
|
|
1706
|
+
color: #4338ca;
|
|
1707
|
+
letter-spacing: 0.3px;
|
|
1708
|
+
white-space: nowrap;
|
|
1709
|
+
}
|
|
1710
|
+
.phs-row-cap[data-color='green'] {
|
|
1711
|
+
background: #ecfdf5;
|
|
1712
|
+
color: #059669;
|
|
1713
|
+
}
|
|
1714
|
+
.phs-row-cap[data-color='orange'] {
|
|
1715
|
+
background: #fff7ed;
|
|
1716
|
+
color: #c2410c;
|
|
1717
|
+
}
|
|
1718
|
+
.phs-row-cap[data-color='purple'] {
|
|
1719
|
+
background: #faf5ff;
|
|
1720
|
+
color: #7c3aed;
|
|
1721
|
+
}
|
|
1722
|
+
.phs-row-cap[data-color='blue'] {
|
|
1723
|
+
background: #dbeafe;
|
|
1724
|
+
color: #1e40af;
|
|
1725
|
+
}
|
|
1726
|
+
.phs-row-cap--ctx {
|
|
1727
|
+
background: #f3f4f6;
|
|
1728
|
+
color: #4b5563;
|
|
1729
|
+
font-family:
|
|
1730
|
+
ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
|
|
1731
|
+
}
|
|
1732
|
+
.phs-row-cap--custom {
|
|
1733
|
+
background: #fef3c7;
|
|
1734
|
+
color: #b45309;
|
|
1735
|
+
}
|
|
1736
|
+
.phs-row-cap--remote {
|
|
1737
|
+
background: #dcfce7;
|
|
1738
|
+
color: #15803d;
|
|
1739
|
+
}
|
|
1740
|
+
.phs-row-cap--default {
|
|
1741
|
+
background: #dbeafe;
|
|
1742
|
+
color: #1e40af;
|
|
1743
|
+
}
|
|
1744
|
+
.phs-row-gear {
|
|
1745
|
+
background: transparent;
|
|
1746
|
+
border: none;
|
|
1747
|
+
color: #9ca3af;
|
|
1748
|
+
cursor: pointer;
|
|
1749
|
+
font-size: 14px;
|
|
1750
|
+
padding: 0 6px;
|
|
1751
|
+
border-radius: 4px;
|
|
1752
|
+
flex-shrink: 0;
|
|
1753
|
+
}
|
|
1754
|
+
.phs-row-gear:hover {
|
|
1755
|
+
background: #eef2ff;
|
|
1756
|
+
color: #6366f1;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
/* Pill row — legacy compact list style, kept for potential future use */
|
|
1760
|
+
.phs-pill {
|
|
1761
|
+
display: flex;
|
|
1762
|
+
align-items: center;
|
|
1763
|
+
gap: 8px;
|
|
1764
|
+
padding: 6px 10px;
|
|
1765
|
+
background: #f9fafb;
|
|
1766
|
+
border: 1px solid transparent;
|
|
1767
|
+
border-radius: 999px;
|
|
1768
|
+
transition: background 0.15s ease, border-color 0.15s ease;
|
|
1769
|
+
}
|
|
1770
|
+
.phs-pill:hover {
|
|
1771
|
+
background: #f3f4f6;
|
|
1772
|
+
}
|
|
1773
|
+
.phs-pill--on {
|
|
1774
|
+
background: #eef2ff;
|
|
1775
|
+
border-color: #a5b4fc;
|
|
1776
|
+
}
|
|
1777
|
+
:global(.dark) .phs-pill {
|
|
1778
|
+
background: #262626;
|
|
1779
|
+
}
|
|
1780
|
+
:global(.dark) .phs-pill:hover {
|
|
1781
|
+
background: #2d2d2d;
|
|
1782
|
+
}
|
|
1783
|
+
:global(.dark) .phs-pill--on {
|
|
1784
|
+
background: #312e81;
|
|
1785
|
+
border-color: #6366f1;
|
|
1786
|
+
}
|
|
1787
|
+
.phs-pill-name {
|
|
1788
|
+
flex: 1;
|
|
1789
|
+
min-width: 0;
|
|
1790
|
+
font-size: 12.5px;
|
|
1791
|
+
font-family:
|
|
1792
|
+
ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
|
|
1793
|
+
color: #111827;
|
|
1794
|
+
white-space: nowrap;
|
|
1795
|
+
overflow: hidden;
|
|
1796
|
+
text-overflow: ellipsis;
|
|
1797
|
+
}
|
|
1798
|
+
:global(.dark) .phs-pill-name {
|
|
1799
|
+
color: #f3f4f6;
|
|
1800
|
+
}
|
|
1801
|
+
.phs-pill-meta {
|
|
1802
|
+
display: flex;
|
|
1803
|
+
gap: 4px;
|
|
1804
|
+
}
|
|
1805
|
+
.phs-pill-badge {
|
|
1806
|
+
padding: 0 6px;
|
|
1807
|
+
font-size: 10px;
|
|
1808
|
+
border-radius: 999px;
|
|
1809
|
+
background: #dbeafe;
|
|
1810
|
+
color: #1e40af;
|
|
1811
|
+
}
|
|
1812
|
+
.phs-pill-badge--default {
|
|
1813
|
+
background: #dbeafe;
|
|
1814
|
+
color: #1e40af;
|
|
1815
|
+
}
|
|
1816
|
+
.phs-pill-badge--feat {
|
|
1817
|
+
background: #f5f3ff;
|
|
1818
|
+
color: #6d28d9;
|
|
1819
|
+
}
|
|
1820
|
+
.phs-pill-badge--custom {
|
|
1821
|
+
background: #fef3c7;
|
|
1822
|
+
color: #b45309;
|
|
1823
|
+
}
|
|
1824
|
+
.phs-pill-badge--remote {
|
|
1825
|
+
background: #dcfce7;
|
|
1826
|
+
color: #15803d;
|
|
1827
|
+
}
|
|
1828
|
+
.phs-pill-gear {
|
|
1829
|
+
background: transparent;
|
|
1830
|
+
border: none;
|
|
1831
|
+
color: #9ca3af;
|
|
1832
|
+
cursor: pointer;
|
|
1833
|
+
font-size: 13px;
|
|
1834
|
+
padding: 0 4px;
|
|
1835
|
+
border-radius: 4px;
|
|
1836
|
+
flex-shrink: 0;
|
|
1837
|
+
}
|
|
1838
|
+
.phs-pill-gear:hover {
|
|
1839
|
+
background: #eef2ff;
|
|
1840
|
+
color: #6366f1;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
/* toggle switch */
|
|
1844
|
+
.phs-switch {
|
|
1845
|
+
position: relative;
|
|
1846
|
+
display: inline-block;
|
|
1847
|
+
width: 30px;
|
|
1848
|
+
height: 16px;
|
|
1849
|
+
flex-shrink: 0;
|
|
1850
|
+
}
|
|
1851
|
+
.phs-switch input {
|
|
1852
|
+
opacity: 0;
|
|
1853
|
+
width: 0;
|
|
1854
|
+
height: 0;
|
|
1855
|
+
}
|
|
1856
|
+
.phs-switch-slider {
|
|
1857
|
+
position: absolute;
|
|
1858
|
+
cursor: pointer;
|
|
1859
|
+
inset: 0;
|
|
1860
|
+
background: #cbd5e1;
|
|
1861
|
+
border-radius: 999px;
|
|
1862
|
+
transition: 0.15s;
|
|
1863
|
+
}
|
|
1864
|
+
.phs-switch-slider:before {
|
|
1865
|
+
position: absolute;
|
|
1866
|
+
content: '';
|
|
1867
|
+
height: 12px;
|
|
1868
|
+
width: 12px;
|
|
1869
|
+
left: 2px;
|
|
1870
|
+
bottom: 2px;
|
|
1871
|
+
background: #fff;
|
|
1872
|
+
border-radius: 50%;
|
|
1873
|
+
transition: 0.15s;
|
|
1874
|
+
}
|
|
1875
|
+
.phs-switch input:checked + .phs-switch-slider {
|
|
1876
|
+
background: #6366f1;
|
|
1877
|
+
}
|
|
1878
|
+
.phs-switch input:checked + .phs-switch-slider:before {
|
|
1879
|
+
transform: translateX(14px);
|
|
1880
|
+
}
|
|
1881
|
+
.phs-switch input:disabled + .phs-switch-slider {
|
|
1882
|
+
opacity: 0.5;
|
|
1883
|
+
cursor: not-allowed;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
/* -------- manual add mini-modal -------- */
|
|
1887
|
+
.phs-mini-backdrop {
|
|
1888
|
+
position: fixed;
|
|
1889
|
+
inset: 0;
|
|
1890
|
+
background: rgba(15, 23, 42, 0.35);
|
|
1891
|
+
display: flex;
|
|
1892
|
+
align-items: center;
|
|
1893
|
+
justify-content: center;
|
|
1894
|
+
z-index: 1000;
|
|
1895
|
+
}
|
|
1896
|
+
.phs-mini-modal {
|
|
1897
|
+
width: 420px;
|
|
1898
|
+
max-width: 92vw;
|
|
1899
|
+
background: #fff;
|
|
1900
|
+
border-radius: 12px;
|
|
1901
|
+
box-shadow: 0 12px 40px rgba(15, 23, 42, 0.25);
|
|
1902
|
+
display: flex;
|
|
1903
|
+
flex-direction: column;
|
|
1904
|
+
}
|
|
1905
|
+
:global(.dark) .phs-mini-modal {
|
|
1906
|
+
background: #1f1f1f;
|
|
1907
|
+
color: #f3f4f6;
|
|
1908
|
+
}
|
|
1909
|
+
.phs-mini-header {
|
|
1910
|
+
display: flex;
|
|
1911
|
+
justify-content: space-between;
|
|
1912
|
+
align-items: center;
|
|
1913
|
+
padding: 12px 16px;
|
|
1914
|
+
border-bottom: 1px solid #e5e7eb;
|
|
1915
|
+
}
|
|
1916
|
+
.phs-mini-title {
|
|
1917
|
+
font-size: 14px;
|
|
1918
|
+
font-weight: 600;
|
|
1919
|
+
color: #111827;
|
|
1920
|
+
}
|
|
1921
|
+
:global(.dark) .phs-mini-title {
|
|
1922
|
+
color: #f3f4f6;
|
|
1923
|
+
}
|
|
1924
|
+
.phs-mini-close {
|
|
1925
|
+
background: transparent;
|
|
1926
|
+
border: none;
|
|
1927
|
+
font-size: 16px;
|
|
1928
|
+
color: #6b7280;
|
|
1929
|
+
cursor: pointer;
|
|
1930
|
+
padding: 2px 6px;
|
|
1931
|
+
border-radius: 4px;
|
|
1932
|
+
}
|
|
1933
|
+
.phs-mini-close:hover {
|
|
1934
|
+
background: #f3f4f6;
|
|
1935
|
+
}
|
|
1936
|
+
.phs-mini-body {
|
|
1937
|
+
padding: 12px 16px;
|
|
1938
|
+
display: flex;
|
|
1939
|
+
flex-direction: column;
|
|
1940
|
+
gap: 10px;
|
|
1941
|
+
}
|
|
1942
|
+
.phs-mini-hint {
|
|
1943
|
+
font-size: 12px;
|
|
1944
|
+
color: #6b7280;
|
|
1945
|
+
line-height: 1.5;
|
|
1946
|
+
}
|
|
1947
|
+
.phs-mini-field {
|
|
1948
|
+
display: flex;
|
|
1949
|
+
flex-direction: column;
|
|
1950
|
+
gap: 4px;
|
|
1951
|
+
}
|
|
1952
|
+
.phs-mini-field label {
|
|
1953
|
+
font-size: 12px;
|
|
1954
|
+
color: #374151;
|
|
1955
|
+
font-weight: 500;
|
|
1956
|
+
}
|
|
1957
|
+
.phs-mini-input {
|
|
1958
|
+
padding: 6px 10px;
|
|
1959
|
+
font-size: 13px;
|
|
1960
|
+
border: 1px solid #d1d5db;
|
|
1961
|
+
border-radius: 6px;
|
|
1962
|
+
background: #fff;
|
|
1963
|
+
color: #111827;
|
|
1964
|
+
}
|
|
1965
|
+
:global(.dark) .phs-mini-input {
|
|
1966
|
+
background: #2d2d2d;
|
|
1967
|
+
border-color: #3d3d3d;
|
|
1968
|
+
color: #f3f4f6;
|
|
1969
|
+
}
|
|
1970
|
+
.phs-mini-input:focus {
|
|
1971
|
+
outline: none;
|
|
1972
|
+
border-color: #6366f1;
|
|
1973
|
+
}
|
|
1974
|
+
.phs-mini-error {
|
|
1975
|
+
padding: 6px 10px;
|
|
1976
|
+
background: #fef2f2;
|
|
1977
|
+
border: 1px solid #fecaca;
|
|
1978
|
+
border-radius: 6px;
|
|
1979
|
+
color: #b91c1c;
|
|
1980
|
+
font-size: 12px;
|
|
1981
|
+
}
|
|
1982
|
+
.phs-mini-footer {
|
|
1983
|
+
padding: 10px 16px;
|
|
1984
|
+
border-top: 1px solid #e5e7eb;
|
|
1985
|
+
display: flex;
|
|
1986
|
+
justify-content: flex-end;
|
|
1987
|
+
gap: 8px;
|
|
1988
|
+
}
|
|
1989
|
+
.phs-req {
|
|
1990
|
+
color: #dc2626;
|
|
1991
|
+
}
|
|
1992
|
+
</style>
|