@wopr-network/platform-ui-core 1.1.6 → 1.1.8
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 +1 -1
- package/src/__tests__/auto-topup-card.test.tsx +10 -2
- package/src/__tests__/onboarding-store-urls.test.ts +24 -0
- package/src/__tests__/plugins-catalog-error.test.tsx +233 -0
- package/src/__tests__/provider-docs.test.ts +35 -0
- package/src/__tests__/suspension-banner.test.tsx +20 -4
- package/src/app/plugins/page.tsx +52 -5
- package/src/config/provider-docs.ts +12 -1
- package/src/lib/onboarding-store.ts +6 -5
package/package.json
CHANGED
|
@@ -35,6 +35,14 @@ vi.mock("better-auth/react", () => ({
|
|
|
35
35
|
}),
|
|
36
36
|
}));
|
|
37
37
|
|
|
38
|
+
/** Return an ISO-8601 UTC date string N days from now, truncated to midnight. */
|
|
39
|
+
function daysFromNow(n: number): string {
|
|
40
|
+
const d = new Date();
|
|
41
|
+
d.setUTCDate(d.getUTCDate() + n);
|
|
42
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
43
|
+
return d.toISOString();
|
|
44
|
+
}
|
|
45
|
+
|
|
38
46
|
const MOCK_SETTINGS: AutoTopupSettings = {
|
|
39
47
|
usageBased: {
|
|
40
48
|
enabled: false,
|
|
@@ -61,7 +69,7 @@ const MOCK_SETTINGS_ENABLED: AutoTopupSettings = {
|
|
|
61
69
|
enabled: true,
|
|
62
70
|
amountCents: 2000,
|
|
63
71
|
interval: "weekly",
|
|
64
|
-
nextChargeDate:
|
|
72
|
+
nextChargeDate: daysFromNow(7),
|
|
65
73
|
},
|
|
66
74
|
paymentMethodLast4: "4242",
|
|
67
75
|
paymentMethodBrand: "Visa",
|
|
@@ -89,7 +97,7 @@ const MOCK_SETTINGS_MONTHLY: AutoTopupSettings = {
|
|
|
89
97
|
enabled: true,
|
|
90
98
|
amountCents: 2000,
|
|
91
99
|
interval: "monthly",
|
|
92
|
-
nextChargeDate:
|
|
100
|
+
nextChargeDate: daysFromNow(14),
|
|
93
101
|
},
|
|
94
102
|
};
|
|
95
103
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { PROVIDER_DOC_URLS } from "../config/provider-docs";
|
|
3
|
+
|
|
4
|
+
describe("AI_PROVIDERS keyHelpUrl consolidation", () => {
|
|
5
|
+
it("all keyHelpUrl values match PROVIDER_DOC_URLS entries", async () => {
|
|
6
|
+
const { AI_PROVIDERS } = await import("../lib/onboarding-store");
|
|
7
|
+
|
|
8
|
+
const urlToProviderDocKey: Record<string, string> = {
|
|
9
|
+
[PROVIDER_DOC_URLS.anthropic]: "anthropic",
|
|
10
|
+
[PROVIDER_DOC_URLS.openai]: "openai",
|
|
11
|
+
[PROVIDER_DOC_URLS.google]: "google",
|
|
12
|
+
[PROVIDER_DOC_URLS.xai]: "xai",
|
|
13
|
+
[PROVIDER_DOC_URLS.local]: "local",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
for (const provider of AI_PROVIDERS) {
|
|
17
|
+
expect(
|
|
18
|
+
provider.keyHelpUrl in urlToProviderDocKey ||
|
|
19
|
+
Object.values(PROVIDER_DOC_URLS).includes(provider.keyHelpUrl as never),
|
|
20
|
+
`${provider.id} keyHelpUrl "${provider.keyHelpUrl}" should come from PROVIDER_DOC_URLS`,
|
|
21
|
+
).toBe(true);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import type React from "react";
|
|
4
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
// Mock next/navigation
|
|
7
|
+
vi.mock("next/navigation", () => ({
|
|
8
|
+
useRouter: () => ({ push: vi.fn() }),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
// Mock framer-motion to avoid animation issues in tests
|
|
12
|
+
vi.mock("framer-motion", () => ({
|
|
13
|
+
motion: {
|
|
14
|
+
div: ({
|
|
15
|
+
children,
|
|
16
|
+
variants: _variants,
|
|
17
|
+
initial: _initial,
|
|
18
|
+
animate: _animate,
|
|
19
|
+
whileHover: _whileHover,
|
|
20
|
+
whileTap: _whileTap,
|
|
21
|
+
transition: _transition,
|
|
22
|
+
style,
|
|
23
|
+
className,
|
|
24
|
+
}: React.PropsWithChildren<{
|
|
25
|
+
variants?: unknown;
|
|
26
|
+
initial?: unknown;
|
|
27
|
+
animate?: unknown;
|
|
28
|
+
whileHover?: unknown;
|
|
29
|
+
whileTap?: unknown;
|
|
30
|
+
transition?: unknown;
|
|
31
|
+
style?: React.CSSProperties;
|
|
32
|
+
className?: string;
|
|
33
|
+
}>) => (
|
|
34
|
+
<div style={style} className={className}>
|
|
35
|
+
{children}
|
|
36
|
+
</div>
|
|
37
|
+
),
|
|
38
|
+
},
|
|
39
|
+
AnimatePresence: ({ children }: React.PropsWithChildren) => <>{children}</>,
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
// Mock plugin setup chat hook
|
|
43
|
+
vi.mock("@/hooks/use-plugin-setup-chat", () => ({
|
|
44
|
+
usePluginSetupChat: () => ({
|
|
45
|
+
state: {
|
|
46
|
+
isOpen: false,
|
|
47
|
+
pluginName: "",
|
|
48
|
+
messages: [],
|
|
49
|
+
isConnected: false,
|
|
50
|
+
isTyping: false,
|
|
51
|
+
isComplete: false,
|
|
52
|
+
},
|
|
53
|
+
sendMessage: vi.fn(),
|
|
54
|
+
closeSetup: vi.fn(),
|
|
55
|
+
openSetup: vi.fn(),
|
|
56
|
+
}),
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
// Mock brand config
|
|
60
|
+
vi.mock("@/lib/brand-config", () => ({
|
|
61
|
+
getBrandConfig: () => ({ productName: "TestProduct", brandName: "TestBrand" }),
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
// Mock plugin-setup component
|
|
65
|
+
vi.mock("@/components/plugin-setup", () => ({
|
|
66
|
+
SetupChatPanel: () => null,
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
const mockListMarketplacePlugins = vi.fn();
|
|
70
|
+
|
|
71
|
+
vi.mock("@/lib/marketplace-data", () => ({
|
|
72
|
+
listBots: vi.fn().mockResolvedValue([{ id: "bot-1", name: "Test Bot" }]),
|
|
73
|
+
listMarketplacePlugins: (...args: unknown[]) => mockListMarketplacePlugins(...args),
|
|
74
|
+
listInstalledPlugins: vi.fn().mockResolvedValue([]),
|
|
75
|
+
togglePluginEnabled: vi.fn(),
|
|
76
|
+
formatInstallCount: (n: number) => String(n),
|
|
77
|
+
hasHostedOption: () => false,
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
import PluginsPage from "@/app/plugins/page";
|
|
81
|
+
|
|
82
|
+
describe("plugins catalog error state", () => {
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
vi.clearAllMocks();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("shows error message with retry when catalog load fails", async () => {
|
|
88
|
+
mockListMarketplacePlugins.mockRejectedValue(new Error("Network error"));
|
|
89
|
+
|
|
90
|
+
render(<PluginsPage />);
|
|
91
|
+
|
|
92
|
+
// Switch to the Catalog tab
|
|
93
|
+
const catalogTab = await screen.findByRole("tab", { name: /catalog/i });
|
|
94
|
+
await userEvent.click(catalogTab);
|
|
95
|
+
|
|
96
|
+
// Should show the error message
|
|
97
|
+
await waitFor(() => {
|
|
98
|
+
expect(screen.getByText(/CATALOG LOAD FAILED/i)).toBeInTheDocument();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Should show the retry button
|
|
102
|
+
expect(screen.getByRole("button", { name: /retry/i })).toBeInTheDocument();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("retries loading when retry button is clicked", async () => {
|
|
106
|
+
const catalogPlugin = {
|
|
107
|
+
id: "plugin-1",
|
|
108
|
+
name: "Test Plugin",
|
|
109
|
+
description: "A test plugin",
|
|
110
|
+
version: "1.0.0",
|
|
111
|
+
color: "#ff0000",
|
|
112
|
+
tags: [],
|
|
113
|
+
capabilities: [],
|
|
114
|
+
installCount: 100,
|
|
115
|
+
author: "Test",
|
|
116
|
+
category: "integration",
|
|
117
|
+
configSchema: [],
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// First call fails, second succeeds
|
|
121
|
+
mockListMarketplacePlugins
|
|
122
|
+
.mockRejectedValueOnce(new Error("Network error"))
|
|
123
|
+
.mockResolvedValueOnce([catalogPlugin]);
|
|
124
|
+
|
|
125
|
+
render(<PluginsPage />);
|
|
126
|
+
|
|
127
|
+
// Switch to the Catalog tab
|
|
128
|
+
const catalogTab = await screen.findByRole("tab", { name: /catalog/i });
|
|
129
|
+
await userEvent.click(catalogTab);
|
|
130
|
+
|
|
131
|
+
// Wait for error state
|
|
132
|
+
await waitFor(() => {
|
|
133
|
+
expect(screen.getByText(/CATALOG LOAD FAILED/i)).toBeInTheDocument();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Click retry
|
|
137
|
+
const retryButton = screen.getByRole("button", { name: /retry/i });
|
|
138
|
+
await userEvent.click(retryButton);
|
|
139
|
+
|
|
140
|
+
// After retry, the error should be gone and plugin visible in catalog tab (no tab reset)
|
|
141
|
+
await waitFor(() => {
|
|
142
|
+
expect(screen.getByText("Test Plugin")).toBeInTheDocument();
|
|
143
|
+
});
|
|
144
|
+
expect(screen.queryByText(/CATALOG LOAD FAILED/i)).not.toBeInTheDocument();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("shows error banner and retry button when catalog error occurs", async () => {
|
|
148
|
+
// This test validates the corrected rendering condition:
|
|
149
|
+
// The error banner renders whenever catalogError=true, not only when catalogTotal===0.
|
|
150
|
+
// The previous condition (catalogError && catalogTotal === 0) would hide the error
|
|
151
|
+
// if a prior catalog existed — this test verifies the simpler, correct condition.
|
|
152
|
+
mockListMarketplacePlugins.mockRejectedValue(new Error("Network error"));
|
|
153
|
+
|
|
154
|
+
render(<PluginsPage />);
|
|
155
|
+
|
|
156
|
+
const catalogTab = await screen.findByRole("tab", { name: /catalog/i });
|
|
157
|
+
await userEvent.click(catalogTab);
|
|
158
|
+
|
|
159
|
+
// Error banner shown
|
|
160
|
+
await waitFor(() => {
|
|
161
|
+
expect(screen.getByText(/CATALOG LOAD FAILED/i)).toBeInTheDocument();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Retry button visible (not hidden by incorrect catalogTotal condition)
|
|
165
|
+
expect(screen.getByRole("button", { name: /retry/i })).toBeInTheDocument();
|
|
166
|
+
|
|
167
|
+
// No empty-state message rendered at the same time
|
|
168
|
+
expect(screen.queryByText(/NO MATCHING PLUGINS FOUND/i)).not.toBeInTheDocument();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("retry does not reset active tab to Installed", async () => {
|
|
172
|
+
const catalogPlugin = {
|
|
173
|
+
id: "plugin-1",
|
|
174
|
+
name: "Test Plugin",
|
|
175
|
+
description: "A test plugin",
|
|
176
|
+
version: "1.0.0",
|
|
177
|
+
color: "#ff0000",
|
|
178
|
+
tags: [],
|
|
179
|
+
capabilities: [],
|
|
180
|
+
installCount: 100,
|
|
181
|
+
author: "Test",
|
|
182
|
+
category: "integration",
|
|
183
|
+
configSchema: [],
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// First call fails, second succeeds
|
|
187
|
+
mockListMarketplacePlugins
|
|
188
|
+
.mockRejectedValueOnce(new Error("Network error"))
|
|
189
|
+
.mockResolvedValueOnce([catalogPlugin]);
|
|
190
|
+
|
|
191
|
+
render(<PluginsPage />);
|
|
192
|
+
|
|
193
|
+
// Switch to the Catalog tab
|
|
194
|
+
const catalogTab = await screen.findByRole("tab", { name: /catalog/i });
|
|
195
|
+
await userEvent.click(catalogTab);
|
|
196
|
+
|
|
197
|
+
// Wait for error state
|
|
198
|
+
await waitFor(() => {
|
|
199
|
+
expect(screen.getByText(/CATALOG LOAD FAILED/i)).toBeInTheDocument();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Click retry
|
|
203
|
+
const retryButton = screen.getByRole("button", { name: /retry/i });
|
|
204
|
+
await userEvent.click(retryButton);
|
|
205
|
+
|
|
206
|
+
// After retry succeeds, the Catalog tab content should be visible
|
|
207
|
+
// WITHOUT having to re-click the tab (no tab reset)
|
|
208
|
+
await waitFor(() => {
|
|
209
|
+
expect(screen.getByText("Test Plugin")).toBeInTheDocument();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// The active tab should still be Catalog — verify the catalog content is visible
|
|
213
|
+
// without re-clicking the tab (i.e., no tab reset occurred)
|
|
214
|
+
expect(screen.queryByText(/CATALOG LOAD FAILED/i)).not.toBeInTheDocument();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("shows empty state (not error) when catalog loads with zero plugins", async () => {
|
|
218
|
+
mockListMarketplacePlugins.mockResolvedValue([]);
|
|
219
|
+
|
|
220
|
+
render(<PluginsPage />);
|
|
221
|
+
|
|
222
|
+
// Switch to the Catalog tab
|
|
223
|
+
const catalogTab = await screen.findByRole("tab", { name: /catalog/i });
|
|
224
|
+
await userEvent.click(catalogTab);
|
|
225
|
+
|
|
226
|
+
// Should show the normal empty state, not the error state
|
|
227
|
+
await waitFor(() => {
|
|
228
|
+
expect(screen.getByText(/NO MATCHING PLUGINS FOUND/i)).toBeInTheDocument();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
expect(screen.queryByText(/CATALOG LOAD FAILED/i)).not.toBeInTheDocument();
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { PROVIDER_DOC_URLS } from "../config/provider-docs";
|
|
3
|
+
|
|
4
|
+
describe("PROVIDER_DOC_URLS", () => {
|
|
5
|
+
it("contains all expected provider keys", () => {
|
|
6
|
+
const expectedKeys = [
|
|
7
|
+
"openai",
|
|
8
|
+
"openrouter",
|
|
9
|
+
"anthropic",
|
|
10
|
+
"replicate",
|
|
11
|
+
"elevenlabs",
|
|
12
|
+
"elevenlabsHome",
|
|
13
|
+
"deepgram",
|
|
14
|
+
"discord",
|
|
15
|
+
"slack",
|
|
16
|
+
"telegram",
|
|
17
|
+
"whatsapp",
|
|
18
|
+
"msTeams",
|
|
19
|
+
"moonshot",
|
|
20
|
+
"github",
|
|
21
|
+
"google",
|
|
22
|
+
"xai",
|
|
23
|
+
"local",
|
|
24
|
+
];
|
|
25
|
+
for (const key of expectedKeys) {
|
|
26
|
+
expect(PROVIDER_DOC_URLS).toHaveProperty(key);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("all values are valid https or http URLs", () => {
|
|
31
|
+
for (const [key, url] of Object.entries(PROVIDER_DOC_URLS)) {
|
|
32
|
+
expect(url, `${key} should be a valid URL`).toMatch(/^https?:\/\//);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -19,6 +19,22 @@ vi.mock("better-auth/react", () => ({
|
|
|
19
19
|
}),
|
|
20
20
|
}));
|
|
21
21
|
|
|
22
|
+
/** Return an ISO-8601 UTC date string N days from now, truncated to midnight. */
|
|
23
|
+
function daysFromNow(n: number): string {
|
|
24
|
+
const d = new Date();
|
|
25
|
+
d.setUTCDate(d.getUTCDate() + n);
|
|
26
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
27
|
+
return d.toISOString();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Return an ISO-8601 UTC date string N days from now, at end-of-day. */
|
|
31
|
+
function endOfDayFromNow(n: number): string {
|
|
32
|
+
const d = new Date();
|
|
33
|
+
d.setUTCDate(d.getUTCDate() + n);
|
|
34
|
+
d.setUTCHours(23, 59, 59, 0);
|
|
35
|
+
return d.toISOString();
|
|
36
|
+
}
|
|
37
|
+
|
|
22
38
|
const MOCK_BALANCE: CreditBalance = {
|
|
23
39
|
balance: 0.5,
|
|
24
40
|
dailyBurn: 0.33,
|
|
@@ -32,8 +48,8 @@ vi.mock("@/lib/api", async (importOriginal) => {
|
|
|
32
48
|
getCreditBalance: vi.fn().mockResolvedValue(MOCK_BALANCE),
|
|
33
49
|
getAccountStatus: vi.fn().mockResolvedValue(null),
|
|
34
50
|
getBillingUsageSummary: vi.fn().mockResolvedValue({
|
|
35
|
-
periodStart:
|
|
36
|
-
periodEnd:
|
|
51
|
+
periodStart: daysFromNow(-30),
|
|
52
|
+
periodEnd: endOfDayFromNow(0),
|
|
37
53
|
totalSpend: 30,
|
|
38
54
|
includedCredit: 50,
|
|
39
55
|
amountDue: 0,
|
|
@@ -105,8 +121,8 @@ describe("SuspensionBanner", () => {
|
|
|
105
121
|
runway: 5,
|
|
106
122
|
});
|
|
107
123
|
vi.mocked(api.getBillingUsageSummary).mockResolvedValueOnce({
|
|
108
|
-
periodStart:
|
|
109
|
-
periodEnd:
|
|
124
|
+
periodStart: daysFromNow(-30),
|
|
125
|
+
periodEnd: endOfDayFromNow(0),
|
|
110
126
|
totalSpend: 120,
|
|
111
127
|
includedCredit: 50,
|
|
112
128
|
amountDue: 70,
|
package/src/app/plugins/page.tsx
CHANGED
|
@@ -82,6 +82,8 @@ export default function PluginsPage() {
|
|
|
82
82
|
const [toggling, setToggling] = useState<string | null>(null);
|
|
83
83
|
const togglingRef = useRef<string | null>(null);
|
|
84
84
|
const [toggleError, setToggleError] = useState<string | null>(null);
|
|
85
|
+
const [catalogError, setCatalogError] = useState(false);
|
|
86
|
+
const [catalogLoading, setCatalogLoading] = useState(false);
|
|
85
87
|
const [installedPage, setInstalledPage] = useState(1);
|
|
86
88
|
const [catalogPage, setCatalogPage] = useState(1);
|
|
87
89
|
|
|
@@ -114,15 +116,24 @@ export default function PluginsPage() {
|
|
|
114
116
|
});
|
|
115
117
|
}, []);
|
|
116
118
|
|
|
117
|
-
const loadCatalog = useCallback(async () => {
|
|
118
|
-
|
|
119
|
+
const loadCatalog = useCallback(async (isRetry = false) => {
|
|
120
|
+
setCatalogError(false);
|
|
121
|
+
if (isRetry) {
|
|
122
|
+
setCatalogLoading(true);
|
|
123
|
+
} else {
|
|
124
|
+
setLoading(true);
|
|
125
|
+
}
|
|
119
126
|
try {
|
|
120
127
|
const data = await listMarketplacePlugins();
|
|
121
128
|
setCatalog(data);
|
|
122
129
|
} catch {
|
|
123
|
-
|
|
130
|
+
setCatalogError(true);
|
|
124
131
|
} finally {
|
|
125
|
-
|
|
132
|
+
if (isRetry) {
|
|
133
|
+
setCatalogLoading(false);
|
|
134
|
+
} else {
|
|
135
|
+
setLoading(false);
|
|
136
|
+
}
|
|
126
137
|
}
|
|
127
138
|
}, []);
|
|
128
139
|
|
|
@@ -446,7 +457,43 @@ export default function PluginsPage() {
|
|
|
446
457
|
className="max-w-sm bg-black/50 border-terminal/30 placeholder:text-terminal/30 focus-visible:border-terminal focus-visible:ring-terminal/20"
|
|
447
458
|
/>
|
|
448
459
|
|
|
449
|
-
{
|
|
460
|
+
{catalogLoading ? (
|
|
461
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
462
|
+
{Array.from({ length: 6 }, (_, n) => `csk-${n}`).map((skId) => (
|
|
463
|
+
<Card key={skId}>
|
|
464
|
+
<CardHeader>
|
|
465
|
+
<div className="flex items-start gap-3">
|
|
466
|
+
<Skeleton className="h-10 w-10 rounded-lg" />
|
|
467
|
+
<div className="flex-1 space-y-2">
|
|
468
|
+
<Skeleton className="h-5 w-28" />
|
|
469
|
+
<Skeleton className="h-4 w-full" />
|
|
470
|
+
</div>
|
|
471
|
+
</div>
|
|
472
|
+
</CardHeader>
|
|
473
|
+
<CardContent>
|
|
474
|
+
<div className="flex items-center justify-between">
|
|
475
|
+
<Skeleton className="h-3 w-24" />
|
|
476
|
+
<Skeleton className="h-5 w-10 rounded-full" />
|
|
477
|
+
</div>
|
|
478
|
+
</CardContent>
|
|
479
|
+
</Card>
|
|
480
|
+
))}
|
|
481
|
+
</div>
|
|
482
|
+
) : catalogError ? (
|
|
483
|
+
<div className="flex h-40 flex-col items-center justify-center gap-3 rounded-sm border border-dashed border-red-500/25 bg-red-500/5">
|
|
484
|
+
<p className="font-mono text-sm text-red-500">
|
|
485
|
+
> CATALOG LOAD FAILED. CHECK CONNECTION AND RETRY.
|
|
486
|
+
</p>
|
|
487
|
+
<Button
|
|
488
|
+
variant="outline"
|
|
489
|
+
size="sm"
|
|
490
|
+
className="border-red-500/30 text-red-500 hover:bg-red-500/10"
|
|
491
|
+
onClick={() => loadCatalog(true)}
|
|
492
|
+
>
|
|
493
|
+
Retry
|
|
494
|
+
</Button>
|
|
495
|
+
</div>
|
|
496
|
+
) : catalogTotal === 0 ? (
|
|
450
497
|
<div className="flex h-40 items-center justify-center rounded-sm border border-dashed border-terminal/20">
|
|
451
498
|
<p className="font-mono text-sm text-terminal/60">> NO MATCHING PLUGINS FOUND.</p>
|
|
452
499
|
</div>
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* External provider documentation URLs used in onboarding config fields.
|
|
3
|
+
*
|
|
4
|
+
* These are intentional external links to provider API key dashboards —
|
|
5
|
+
* no secret material. They live in code (not fetched from the API) because
|
|
6
|
+
* major provider dashboard URLs change extremely rarely (years, not weeks).
|
|
7
|
+
* The cost of an API round-trip, loading state, and DB migration is not
|
|
8
|
+
* justified. If a URL changes, update this map and publish a new version.
|
|
9
|
+
*/
|
|
2
10
|
export const PROVIDER_DOC_URLS = {
|
|
3
11
|
openai: "https://platform.openai.com/api-keys",
|
|
4
12
|
openrouter: "https://openrouter.ai/keys",
|
|
@@ -14,4 +22,7 @@ export const PROVIDER_DOC_URLS = {
|
|
|
14
22
|
msTeams: "https://dev.teams.microsoft.com/",
|
|
15
23
|
moonshot: "https://platform.moonshot.cn/",
|
|
16
24
|
github: "https://github.com/settings/tokens",
|
|
25
|
+
google: "https://aistudio.google.com/apikey",
|
|
26
|
+
xai: "https://console.x.ai/",
|
|
27
|
+
local: "https://ollama.com/download",
|
|
17
28
|
} as const;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Client-side onboarding state with localStorage persistence.
|
|
3
3
|
* Enables resume from last completed step.
|
|
4
4
|
*/
|
|
5
|
+
import { PROVIDER_DOC_URLS } from "../config/provider-docs";
|
|
5
6
|
import { storageKey } from "./brand-config";
|
|
6
7
|
|
|
7
8
|
export interface ProviderConfig {
|
|
@@ -137,7 +138,7 @@ export const AI_PROVIDERS = [
|
|
|
137
138
|
recommended: true,
|
|
138
139
|
keyPattern: "^sk-ant-[a-zA-Z0-9_-]+$",
|
|
139
140
|
keyPlaceholder: "sk-ant-api03-...",
|
|
140
|
-
keyHelpUrl:
|
|
141
|
+
keyHelpUrl: PROVIDER_DOC_URLS.anthropic,
|
|
141
142
|
},
|
|
142
143
|
{
|
|
143
144
|
id: "openai",
|
|
@@ -148,7 +149,7 @@ export const AI_PROVIDERS = [
|
|
|
148
149
|
recommended: false,
|
|
149
150
|
keyPattern: "^sk-[a-zA-Z0-9_-]+$",
|
|
150
151
|
keyPlaceholder: "sk-proj-...",
|
|
151
|
-
keyHelpUrl:
|
|
152
|
+
keyHelpUrl: PROVIDER_DOC_URLS.openai,
|
|
152
153
|
},
|
|
153
154
|
{
|
|
154
155
|
id: "google",
|
|
@@ -159,7 +160,7 @@ export const AI_PROVIDERS = [
|
|
|
159
160
|
recommended: false,
|
|
160
161
|
keyPattern: "^AIza[a-zA-Z0-9_-]+$",
|
|
161
162
|
keyPlaceholder: "AIzaSy...",
|
|
162
|
-
keyHelpUrl:
|
|
163
|
+
keyHelpUrl: PROVIDER_DOC_URLS.google,
|
|
163
164
|
},
|
|
164
165
|
{
|
|
165
166
|
id: "xai",
|
|
@@ -170,7 +171,7 @@ export const AI_PROVIDERS = [
|
|
|
170
171
|
recommended: false,
|
|
171
172
|
keyPattern: "^xai-[a-zA-Z0-9_-]+$",
|
|
172
173
|
keyPlaceholder: "xai-...",
|
|
173
|
-
keyHelpUrl:
|
|
174
|
+
keyHelpUrl: PROVIDER_DOC_URLS.xai,
|
|
174
175
|
},
|
|
175
176
|
{
|
|
176
177
|
id: "local",
|
|
@@ -181,7 +182,7 @@ export const AI_PROVIDERS = [
|
|
|
181
182
|
recommended: false,
|
|
182
183
|
keyPattern: ".*",
|
|
183
184
|
keyPlaceholder: "http://localhost:11434",
|
|
184
|
-
keyHelpUrl:
|
|
185
|
+
keyHelpUrl: PROVIDER_DOC_URLS.local,
|
|
185
186
|
},
|
|
186
187
|
] as const;
|
|
187
188
|
|