@wopr-network/platform-ui-core 1.24.1 → 1.26.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.
@@ -0,0 +1,156 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { toast } from "sonner";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
7
+ import { Input } from "@/components/ui/input";
8
+ import { Label } from "@/components/ui/label";
9
+ import { toUserMessage } from "@/lib/errors";
10
+
11
+ interface BillingConfig {
12
+ stripePublishableKey: string | null;
13
+ creditPrices: Record<string, number>;
14
+ affiliateBaseUrl: string | null;
15
+ affiliateMatchRate: string;
16
+ affiliateMaxCap: number;
17
+ dividendRate: string;
18
+ }
19
+
20
+ interface BillingFormProps {
21
+ initial: BillingConfig;
22
+ onSave: (endpoint: string, data: unknown) => Promise<void>;
23
+ }
24
+
25
+ export function BillingForm({ initial, onSave }: BillingFormProps) {
26
+ const [form, setForm] = useState<BillingConfig>(initial);
27
+ const [saving, setSaving] = useState(false);
28
+
29
+ function setStr(key: keyof BillingConfig, value: string) {
30
+ setForm((prev) => ({ ...prev, [key]: value || null }));
31
+ }
32
+
33
+ function setRate(key: "affiliateMatchRate" | "dividendRate", value: string) {
34
+ setForm((prev) => ({ ...prev, [key]: value }));
35
+ }
36
+
37
+ function setNum(key: "affiliateMaxCap", value: string) {
38
+ setForm((prev) => ({ ...prev, [key]: value === "" ? 0 : Number.parseInt(value, 10) }));
39
+ }
40
+
41
+ function setCreditPrice(tier: string, value: string) {
42
+ const n = Number.parseFloat(value);
43
+ setForm((prev) => ({
44
+ ...prev,
45
+ creditPrices: { ...prev.creditPrices, [tier]: Number.isNaN(n) ? 0 : n },
46
+ }));
47
+ }
48
+
49
+ async function handleSave() {
50
+ setSaving(true);
51
+ try {
52
+ await onSave("updateBilling", form);
53
+ toast.success("Billing settings saved.");
54
+ } catch (err) {
55
+ toast.error(toUserMessage(err, "Failed to save billing settings"));
56
+ } finally {
57
+ setSaving(false);
58
+ }
59
+ }
60
+
61
+ const creditTiers = Object.keys(form.creditPrices);
62
+
63
+ return (
64
+ <Card>
65
+ <CardHeader>
66
+ <CardTitle>Billing Configuration</CardTitle>
67
+ </CardHeader>
68
+ <CardContent className="space-y-5">
69
+ <div className="space-y-1.5">
70
+ <Label htmlFor="billing-stripeKey">Stripe Publishable Key</Label>
71
+ <Input
72
+ id="billing-stripeKey"
73
+ value={form.stripePublishableKey ?? ""}
74
+ onChange={(e) => setStr("stripePublishableKey", e.target.value)}
75
+ placeholder="pk_live_..."
76
+ />
77
+ <p className="text-xs text-muted-foreground">Publishable key only — never the secret.</p>
78
+ </div>
79
+
80
+ {creditTiers.length > 0 && (
81
+ <div className="space-y-3">
82
+ <p className="text-sm font-medium">Credit Price Tiers</p>
83
+ <div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
84
+ {creditTiers.map((tier) => (
85
+ <div key={tier} className="space-y-1.5">
86
+ <Label htmlFor={`billing-price-${tier}`} className="capitalize">
87
+ {tier}
88
+ </Label>
89
+ <Input
90
+ id={`billing-price-${tier}`}
91
+ type="number"
92
+ step="0.0001"
93
+ value={form.creditPrices[tier]}
94
+ onChange={(e) => setCreditPrice(tier, e.target.value)}
95
+ min={0}
96
+ />
97
+ </div>
98
+ ))}
99
+ </div>
100
+ </div>
101
+ )}
102
+
103
+ <div className="border-t border-border pt-4 space-y-4">
104
+ <p className="text-sm font-medium text-muted-foreground">Affiliate &amp; Dividends</p>
105
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
106
+ <div className="col-span-full space-y-1.5">
107
+ <Label htmlFor="billing-affiliateUrl">Affiliate Base URL</Label>
108
+ <Input
109
+ id="billing-affiliateUrl"
110
+ value={form.affiliateBaseUrl ?? ""}
111
+ onChange={(e) => setStr("affiliateBaseUrl", e.target.value)}
112
+ placeholder="https://wopr.bot/ref"
113
+ />
114
+ </div>
115
+ <div className="space-y-1.5">
116
+ <Label htmlFor="billing-matchRate">Affiliate Match Rate</Label>
117
+ <Input
118
+ id="billing-matchRate"
119
+ value={form.affiliateMatchRate}
120
+ onChange={(e) => setRate("affiliateMatchRate", e.target.value)}
121
+ placeholder="0.10"
122
+ />
123
+ <p className="text-xs text-muted-foreground">Decimal, e.g. 0.10 = 10%</p>
124
+ </div>
125
+ <div className="space-y-1.5">
126
+ <Label htmlFor="billing-maxCap">Affiliate Max Cap (credits)</Label>
127
+ <Input
128
+ id="billing-maxCap"
129
+ type="number"
130
+ value={form.affiliateMaxCap}
131
+ onChange={(e) => setNum("affiliateMaxCap", e.target.value)}
132
+ min={0}
133
+ />
134
+ </div>
135
+ <div className="space-y-1.5">
136
+ <Label htmlFor="billing-dividendRate">Dividend Rate</Label>
137
+ <Input
138
+ id="billing-dividendRate"
139
+ value={form.dividendRate}
140
+ onChange={(e) => setRate("dividendRate", e.target.value)}
141
+ placeholder="0.05"
142
+ />
143
+ <p className="text-xs text-muted-foreground">Decimal, e.g. 0.05 = 5%</p>
144
+ </div>
145
+ </div>
146
+ </div>
147
+
148
+ <div className="flex justify-end pt-2">
149
+ <Button onClick={handleSave} disabled={saving}>
150
+ {saving ? "Saving…" : "Save Billing"}
151
+ </Button>
152
+ </div>
153
+ </CardContent>
154
+ </Card>
155
+ );
156
+ }
@@ -0,0 +1,102 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { toast } from "sonner";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
7
+ import { Input } from "@/components/ui/input";
8
+ import { Label } from "@/components/ui/label";
9
+ import { toUserMessage } from "@/lib/errors";
10
+
11
+ interface BrandConfig {
12
+ id: string;
13
+ slug: string;
14
+ brandName: string;
15
+ productName: string;
16
+ tagline: string;
17
+ domain: string;
18
+ appDomain: string;
19
+ cookieDomain: string;
20
+ companyLegal: string;
21
+ priceLabel: string;
22
+ defaultImage: string;
23
+ emailSupport: string;
24
+ emailPrivacy: string;
25
+ emailLegal: string;
26
+ fromEmail: string;
27
+ homePath: string;
28
+ storagePrefix: string;
29
+ }
30
+
31
+ interface BrandFormProps {
32
+ initial: BrandConfig;
33
+ onSave: (endpoint: string, data: unknown) => Promise<void>;
34
+ }
35
+
36
+ export function BrandForm({ initial, onSave }: BrandFormProps) {
37
+ const [form, setForm] = useState<BrandConfig>(initial);
38
+ const [saving, setSaving] = useState(false);
39
+
40
+ function set(key: keyof BrandConfig, value: string) {
41
+ setForm((prev) => ({ ...prev, [key]: value }));
42
+ }
43
+
44
+ async function handleSave() {
45
+ setSaving(true);
46
+ try {
47
+ await onSave("updateBrand", form);
48
+ toast.success("Brand settings saved.");
49
+ } catch (err) {
50
+ toast.error(toUserMessage(err, "Failed to save brand settings"));
51
+ } finally {
52
+ setSaving(false);
53
+ }
54
+ }
55
+
56
+ const fields: Array<{ key: keyof BrandConfig; label: string; placeholder?: string }> = [
57
+ { key: "brandName", label: "Brand Name", placeholder: "WOPR" },
58
+ { key: "productName", label: "Product Name", placeholder: "WOPR Platform" },
59
+ { key: "tagline", label: "Tagline", placeholder: "Your AI platform" },
60
+ { key: "slug", label: "Slug", placeholder: "wopr" },
61
+ { key: "domain", label: "Domain", placeholder: "wopr.bot" },
62
+ { key: "appDomain", label: "App Domain", placeholder: "app.wopr.bot" },
63
+ { key: "cookieDomain", label: "Cookie Domain", placeholder: ".wopr.bot" },
64
+ { key: "companyLegal", label: "Company Legal Name", placeholder: "WOPR Inc." },
65
+ { key: "priceLabel", label: "Price Label", placeholder: "credits" },
66
+ { key: "defaultImage", label: "Default Image URL", placeholder: "/og-image.png" },
67
+ { key: "emailSupport", label: "Support Email", placeholder: "support@wopr.bot" },
68
+ { key: "emailPrivacy", label: "Privacy Email", placeholder: "privacy@wopr.bot" },
69
+ { key: "emailLegal", label: "Legal Email", placeholder: "legal@wopr.bot" },
70
+ { key: "fromEmail", label: "From Email", placeholder: "noreply@wopr.bot" },
71
+ { key: "homePath", label: "Home Path", placeholder: "/dashboard" },
72
+ { key: "storagePrefix", label: "Storage Prefix", placeholder: "wopr" },
73
+ ];
74
+
75
+ return (
76
+ <Card>
77
+ <CardHeader>
78
+ <CardTitle>Brand Configuration</CardTitle>
79
+ </CardHeader>
80
+ <CardContent className="space-y-4">
81
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
82
+ {fields.map(({ key, label, placeholder }) => (
83
+ <div key={key} className="space-y-1.5">
84
+ <Label htmlFor={`brand-${key}`}>{label}</Label>
85
+ <Input
86
+ id={`brand-${key}`}
87
+ value={form[key]}
88
+ onChange={(e) => set(key, e.target.value)}
89
+ placeholder={placeholder}
90
+ />
91
+ </div>
92
+ ))}
93
+ </div>
94
+ <div className="flex justify-end pt-2">
95
+ <Button onClick={handleSave} disabled={saving}>
96
+ {saving ? "Saving…" : "Save Brand"}
97
+ </Button>
98
+ </div>
99
+ </CardContent>
100
+ </Card>
101
+ );
102
+ }
@@ -0,0 +1,126 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { toast } from "sonner";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
7
+ import { Checkbox } from "@/components/ui/checkbox";
8
+ import { Input } from "@/components/ui/input";
9
+ import { Label } from "@/components/ui/label";
10
+ import { toUserMessage } from "@/lib/errors";
11
+
12
+ interface FeaturesConfig {
13
+ chatEnabled: boolean;
14
+ onboardingEnabled: boolean;
15
+ onboardingDefaultModel: string | null;
16
+ onboardingMaxCredits: number;
17
+ onboardingWelcomeMsg: string | null;
18
+ sharedModuleBilling: boolean;
19
+ sharedModuleMonitoring: boolean;
20
+ sharedModuleAnalytics: boolean;
21
+ }
22
+
23
+ interface FeaturesFormProps {
24
+ initial: FeaturesConfig;
25
+ onSave: (endpoint: string, data: unknown) => Promise<void>;
26
+ }
27
+
28
+ export function FeaturesForm({ initial, onSave }: FeaturesFormProps) {
29
+ const [form, setForm] = useState<FeaturesConfig>(initial);
30
+ const [saving, setSaving] = useState(false);
31
+
32
+ function setBool(key: keyof FeaturesConfig, value: boolean) {
33
+ setForm((prev) => ({ ...prev, [key]: value }));
34
+ }
35
+
36
+ function setStr(key: keyof FeaturesConfig, value: string) {
37
+ setForm((prev) => ({ ...prev, [key]: value || null }));
38
+ }
39
+
40
+ function setNum(key: keyof FeaturesConfig, value: string) {
41
+ const n = Number.parseInt(value, 10);
42
+ if (!Number.isNaN(n)) setForm((prev) => ({ ...prev, [key]: n }));
43
+ }
44
+
45
+ async function handleSave() {
46
+ setSaving(true);
47
+ try {
48
+ await onSave("updateFeatures", form);
49
+ toast.success("Feature settings saved.");
50
+ } catch (err) {
51
+ toast.error(toUserMessage(err, "Failed to save feature settings"));
52
+ } finally {
53
+ setSaving(false);
54
+ }
55
+ }
56
+
57
+ const boolFields: Array<{ key: keyof FeaturesConfig; label: string }> = [
58
+ { key: "chatEnabled", label: "Chat Enabled" },
59
+ { key: "onboardingEnabled", label: "Onboarding Enabled" },
60
+ { key: "sharedModuleBilling", label: "Shared Module: Billing" },
61
+ { key: "sharedModuleMonitoring", label: "Shared Module: Monitoring" },
62
+ { key: "sharedModuleAnalytics", label: "Shared Module: Analytics" },
63
+ ];
64
+
65
+ return (
66
+ <Card>
67
+ <CardHeader>
68
+ <CardTitle>Feature Flags</CardTitle>
69
+ </CardHeader>
70
+ <CardContent className="space-y-5">
71
+ <div className="space-y-3">
72
+ {boolFields.map(({ key, label }) => (
73
+ <div key={key} className="flex items-center gap-3">
74
+ <Checkbox
75
+ id={`feature-${key}`}
76
+ checked={Boolean(form[key])}
77
+ onCheckedChange={(checked) => setBool(key, Boolean(checked))}
78
+ />
79
+ <Label htmlFor={`feature-${key}`}>{label}</Label>
80
+ </div>
81
+ ))}
82
+ </div>
83
+
84
+ <div className="border-t border-border pt-4 space-y-4">
85
+ <p className="text-sm font-medium text-muted-foreground">Onboarding</p>
86
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
87
+ <div className="space-y-1.5">
88
+ <Label htmlFor="feature-defaultModel">Default Model</Label>
89
+ <Input
90
+ id="feature-defaultModel"
91
+ value={form.onboardingDefaultModel ?? ""}
92
+ onChange={(e) => setStr("onboardingDefaultModel", e.target.value)}
93
+ placeholder="claude-3-5-sonnet"
94
+ />
95
+ </div>
96
+ <div className="space-y-1.5">
97
+ <Label htmlFor="feature-maxCredits">Max Onboarding Credits</Label>
98
+ <Input
99
+ id="feature-maxCredits"
100
+ type="number"
101
+ value={form.onboardingMaxCredits}
102
+ onChange={(e) => setNum("onboardingMaxCredits", e.target.value)}
103
+ min={0}
104
+ />
105
+ </div>
106
+ <div className="col-span-full space-y-1.5">
107
+ <Label htmlFor="feature-welcomeMsg">Welcome Message</Label>
108
+ <Input
109
+ id="feature-welcomeMsg"
110
+ value={form.onboardingWelcomeMsg ?? ""}
111
+ onChange={(e) => setStr("onboardingWelcomeMsg", e.target.value)}
112
+ placeholder="Welcome to the platform!"
113
+ />
114
+ </div>
115
+ </div>
116
+ </div>
117
+
118
+ <div className="flex justify-end pt-2">
119
+ <Button onClick={handleSave} disabled={saving}>
120
+ {saving ? "Saving…" : "Save Features"}
121
+ </Button>
122
+ </div>
123
+ </CardContent>
124
+ </Card>
125
+ );
126
+ }
@@ -0,0 +1,171 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { toast } from "sonner";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
7
+ import { Input } from "@/components/ui/input";
8
+ import { Label } from "@/components/ui/label";
9
+ import {
10
+ Select,
11
+ SelectContent,
12
+ SelectItem,
13
+ SelectTrigger,
14
+ SelectValue,
15
+ } from "@/components/ui/select";
16
+ import { toUserMessage } from "@/lib/errors";
17
+
18
+ interface FleetConfig {
19
+ containerImage: string;
20
+ containerPort: number;
21
+ lifecycle: string;
22
+ billingModel: string;
23
+ maxInstances: number;
24
+ dockerNetwork: string;
25
+ placementStrategy: string;
26
+ fleetDataDir: string;
27
+ }
28
+
29
+ interface FleetFormProps {
30
+ initial: FleetConfig;
31
+ onSave: (endpoint: string, data: unknown) => Promise<void>;
32
+ }
33
+
34
+ export function FleetForm({ initial, onSave }: FleetFormProps) {
35
+ const [form, setForm] = useState<FleetConfig>(initial);
36
+ const [saving, setSaving] = useState(false);
37
+
38
+ function setStr(key: keyof FleetConfig, value: string) {
39
+ setForm((prev) => ({ ...prev, [key]: value }));
40
+ }
41
+
42
+ function setNum(key: keyof FleetConfig, value: string) {
43
+ setForm((prev) => ({ ...prev, [key]: value === "" ? 0 : Number.parseInt(value, 10) }));
44
+ }
45
+
46
+ async function handleSave() {
47
+ setSaving(true);
48
+ try {
49
+ await onSave("updateFleet", form);
50
+ toast.success("Fleet settings saved.");
51
+ } catch (err) {
52
+ toast.error(toUserMessage(err, "Failed to save fleet settings"));
53
+ } finally {
54
+ setSaving(false);
55
+ }
56
+ }
57
+
58
+ return (
59
+ <Card>
60
+ <CardHeader>
61
+ <CardTitle>Fleet Configuration</CardTitle>
62
+ </CardHeader>
63
+ <CardContent className="space-y-4">
64
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
65
+ <div className="col-span-full space-y-1.5">
66
+ <Label htmlFor="fleet-image">Container Image</Label>
67
+ <Input
68
+ id="fleet-image"
69
+ value={form.containerImage}
70
+ onChange={(e) => setStr("containerImage", e.target.value)}
71
+ placeholder="ghcr.io/wopr-network/agent:latest"
72
+ />
73
+ </div>
74
+
75
+ <div className="space-y-1.5">
76
+ <Label htmlFor="fleet-port">Container Port</Label>
77
+ <Input
78
+ id="fleet-port"
79
+ type="number"
80
+ value={form.containerPort}
81
+ onChange={(e) => setNum("containerPort", e.target.value)}
82
+ min={1}
83
+ max={65535}
84
+ />
85
+ </div>
86
+
87
+ <div className="space-y-1.5">
88
+ <Label htmlFor="fleet-maxInstances">Max Instances</Label>
89
+ <Input
90
+ id="fleet-maxInstances"
91
+ type="number"
92
+ value={form.maxInstances}
93
+ onChange={(e) => setNum("maxInstances", e.target.value)}
94
+ min={1}
95
+ />
96
+ </div>
97
+
98
+ <div className="space-y-1.5">
99
+ <Label htmlFor="fleet-lifecycle">Lifecycle</Label>
100
+ <Select value={form.lifecycle} onValueChange={(v) => setStr("lifecycle", v)}>
101
+ <SelectTrigger id="fleet-lifecycle">
102
+ <SelectValue placeholder="Select lifecycle" />
103
+ </SelectTrigger>
104
+ <SelectContent>
105
+ <SelectItem value="managed">Managed</SelectItem>
106
+ <SelectItem value="ephemeral">Ephemeral</SelectItem>
107
+ </SelectContent>
108
+ </Select>
109
+ </div>
110
+
111
+ <div className="space-y-1.5">
112
+ <Label htmlFor="fleet-billingModel">Billing Model</Label>
113
+ <Select value={form.billingModel} onValueChange={(v) => setStr("billingModel", v)}>
114
+ <SelectTrigger id="fleet-billingModel">
115
+ <SelectValue placeholder="Select billing model" />
116
+ </SelectTrigger>
117
+ <SelectContent>
118
+ <SelectItem value="monthly">Monthly</SelectItem>
119
+ <SelectItem value="per_use">Per Use</SelectItem>
120
+ <SelectItem value="none">None</SelectItem>
121
+ </SelectContent>
122
+ </Select>
123
+ </div>
124
+
125
+ <div className="space-y-1.5">
126
+ <Label htmlFor="fleet-placementStrategy">Placement Strategy</Label>
127
+ <Select
128
+ value={form.placementStrategy}
129
+ onValueChange={(v) => setStr("placementStrategy", v)}
130
+ >
131
+ <SelectTrigger id="fleet-placementStrategy">
132
+ <SelectValue placeholder="Select strategy" />
133
+ </SelectTrigger>
134
+ <SelectContent>
135
+ <SelectItem value="round_robin">Round Robin</SelectItem>
136
+ <SelectItem value="least_loaded">Least Loaded</SelectItem>
137
+ <SelectItem value="random">Random</SelectItem>
138
+ </SelectContent>
139
+ </Select>
140
+ </div>
141
+
142
+ <div className="space-y-1.5">
143
+ <Label htmlFor="fleet-dockerNetwork">Docker Network</Label>
144
+ <Input
145
+ id="fleet-dockerNetwork"
146
+ value={form.dockerNetwork}
147
+ onChange={(e) => setStr("dockerNetwork", e.target.value)}
148
+ placeholder="platform"
149
+ />
150
+ </div>
151
+
152
+ <div className="col-span-full space-y-1.5">
153
+ <Label htmlFor="fleet-dataDir">Fleet Data Directory</Label>
154
+ <Input
155
+ id="fleet-dataDir"
156
+ value={form.fleetDataDir}
157
+ onChange={(e) => setStr("fleetDataDir", e.target.value)}
158
+ placeholder="/data/fleet"
159
+ />
160
+ </div>
161
+ </div>
162
+
163
+ <div className="flex justify-end pt-2">
164
+ <Button onClick={handleSave} disabled={saving}>
165
+ {saving ? "Saving…" : "Save Fleet"}
166
+ </Button>
167
+ </div>
168
+ </CardContent>
169
+ </Card>
170
+ );
171
+ }