bloby-bot 0.47.8 → 0.47.10

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.
@@ -1 +1 @@
1
- import{c as e,r as t,t as n}from"./jsx-runtime-C0W9Wf2W.js";import{n as r,r as i,t as a}from"./bloby-E-QLmQDW.js";var o=e(t(),1),s=n(),c=({code:e,language:t,raw:n,className:c,startLine:l,lineNumbers:u,...d})=>{let{shikiTheme:f}=(0,o.useContext)(i),p=r(),[m,h]=(0,o.useState)(n);return(0,o.useEffect)(()=>{if(!p){h(n);return}let r=p.highlight({code:e,language:t,themes:f},e=>{h(e)});r&&h(r)},[e,t,f,p,n]),(0,s.jsx)(a,{className:c,language:t,lineNumbers:u,result:m,startLine:l,...d})};export{c as HighlightedCodeBlockBody};
1
+ import{c as e,r as t,t as n}from"./jsx-runtime-C0W9Wf2W.js";import{n as r,r as i,t as a}from"./bloby-6SStf2s3.js";var o=e(t(),1),s=n(),c=({code:e,language:t,raw:n,className:c,startLine:l,lineNumbers:u,...d})=>{let{shikiTheme:f}=(0,o.useContext)(i),p=r(),[m,h]=(0,o.useState)(n);return(0,o.useEffect)(()=>{if(!p){h(n);return}let r=p.highlight({code:e,language:t,themes:f},e=>{h(e)});r&&h(r)},[e,t,f,p,n]),(0,s.jsx)(a,{className:c,language:t,lineNumbers:u,result:m,startLine:l,...d})};export{c as HighlightedCodeBlockBody};
@@ -0,0 +1 @@
1
+ import{i as e}from"./bloby-6SStf2s3.js";export{e as Mermaid};
@@ -1 +1 @@
1
- import{c as e,r as t,t as n}from"./jsx-runtime-C0W9Wf2W.js";import{p as r,t as i}from"./globals-Ci0CEj1X.js";var a=e(t(),1),o=e(r(),1),s=n();function c(){return(0,s.jsx)(i,{onComplete:()=>{window.parent?.postMessage({type:`bloby:onboard-complete`},`*`)},isInitialSetup:!0})}o.createRoot(document.getElementById(`root`)).render((0,s.jsx)(a.StrictMode,{children:(0,s.jsx)(c,{})}));
1
+ import{c as e,r as t,t as n}from"./jsx-runtime-C0W9Wf2W.js";import{p as r,t as i}from"./globals-Co_gc73k.js";var a=e(t(),1),o=e(r(),1),s=n();function c(){return(0,s.jsx)(i,{onComplete:()=>{window.parent?.postMessage({type:`bloby:onboard-complete`},`*`)},isInitialSetup:!0})}o.createRoot(document.getElementById(`root`)).render((0,s.jsx)(a.StrictMode,{children:(0,s.jsx)(c,{})}));
@@ -4,10 +4,10 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, interactive-widget=resizes-content" />
6
6
  <title>Bloby Chat</title>
7
- <script type="module" crossorigin src="/bloby/assets/bloby-E-QLmQDW.js"></script>
7
+ <script type="module" crossorigin src="/bloby/assets/bloby-6SStf2s3.js"></script>
8
8
  <link rel="modulepreload" crossorigin href="/bloby/assets/jsx-runtime-C0W9Wf2W.js">
9
- <link rel="modulepreload" crossorigin href="/bloby/assets/globals-Ci0CEj1X.js">
10
- <link rel="stylesheet" crossorigin href="/bloby/assets/globals-DriF_8Q_.css">
9
+ <link rel="modulepreload" crossorigin href="/bloby/assets/globals-Co_gc73k.js">
10
+ <link rel="stylesheet" crossorigin href="/bloby/assets/globals-BjVrJod9.css">
11
11
  <link rel="stylesheet" crossorigin href="/bloby/assets/bloby-DkK0ymA2.css">
12
12
  </head>
13
13
  <body class="bg-background text-foreground">
@@ -4,10 +4,10 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, interactive-widget=resizes-content" />
6
6
  <title>Bloby Setup</title>
7
- <script type="module" crossorigin src="/bloby/assets/onboard-C1uMxuk2.js"></script>
7
+ <script type="module" crossorigin src="/bloby/assets/onboard-BJOgPIim.js"></script>
8
8
  <link rel="modulepreload" crossorigin href="/bloby/assets/jsx-runtime-C0W9Wf2W.js">
9
- <link rel="modulepreload" crossorigin href="/bloby/assets/globals-Ci0CEj1X.js">
10
- <link rel="stylesheet" crossorigin href="/bloby/assets/globals-DriF_8Q_.css">
9
+ <link rel="modulepreload" crossorigin href="/bloby/assets/globals-Co_gc73k.js">
10
+ <link rel="stylesheet" crossorigin href="/bloby/assets/globals-BjVrJod9.css">
11
11
  </head>
12
12
  <body class="bg-background text-foreground">
13
13
  <div id="root"></div>
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "bloby-bot",
3
- "version": "0.47.8",
3
+ "version": "0.47.10",
4
4
  "releaseNotes": [
5
- "1. # voice note (PTT bubble)",
6
- "2. # audio file + caption",
7
- "3. # PDF",
5
+ "1. Something great..",
6
+ "2. ",
7
+ "3. ",
8
8
  "4. "
9
9
  ],
10
10
  "description": "Self-hosted, self-evolving AI agent with its own dashboard.",
@@ -46,9 +46,10 @@ const ACCESS_LABELS: Record<AccessMethod, string> = {
46
46
  /* ── Provider config ── */
47
47
 
48
48
  const PROVIDERS = [
49
- { id: 'pi', name: 'Bloby', subtitle: 'Built on top of Pi — bring your own model', icon: '/bloby-icon-192.png' },
50
- { id: 'anthropic', name: 'Claude', subtitle: 'by Anthropic', icon: '/icons/claude.png' },
51
- { id: 'openai', name: 'OpenAI Codex', subtitle: 'ChatGPT Plus / Pro', icon: '/icons/codex.png' },
49
+ { id: 'pi', name: 'Pi', subtitle: 'Bring your own\nmodel', icon: '/pi-logo.svg', comingSoon: false },
50
+ { id: 'bloby', name: 'Bloby', subtitle: 'Coming Soon..', icon: '/bloby.png', comingSoon: true },
51
+ { id: 'anthropic', name: 'Claude', subtitle: 'by\nAnthropic', icon: '/icons/claude.png', comingSoon: false },
52
+ { id: 'openai', name: 'OpenAI Codex', subtitle: 'ChatGPT\nPlus / Pro', icon: '/icons/codex.png', comingSoon: false },
52
53
  ] as const;
53
54
 
54
55
  const MODELS: Record<string, { id: string; label: string }[]> = {
@@ -112,17 +113,17 @@ function ModelDropdown({ models, value, onChange }: { models: { id: string; labe
112
113
  const selected = models.find((m) => m.id === value);
113
114
 
114
115
  return (
115
- <div className="relative">
116
+ <div className="relative min-w-0">
116
117
  <button
117
118
  ref={btnRef}
118
119
  type="button"
119
120
  onClick={() => setOpen((o) => !o)}
120
- className="w-full flex items-center justify-between bg-white/[0.03] border border-white/[0.08] text-white rounded-xl px-4 py-2.5 text-[13px] outline-none hover:border-white/15 focus:border-[#AF27E3]/30 transition-colors"
121
+ className="w-full flex items-center justify-between gap-2 bg-white/[0.03] border border-white/[0.08] text-white rounded-xl px-4 py-2.5 text-[13px] outline-none hover:border-white/15 focus:border-[#AF27E3]/30 transition-colors overflow-hidden"
121
122
  >
122
- <span className={selected ? 'text-white' : 'text-white/20'}>
123
+ <span className={`min-w-0 truncate text-left ${selected ? 'text-white' : 'text-white/20'}`}>
123
124
  {selected ? selected.label : 'Choose a model...'}
124
125
  </span>
125
- <ChevronDown className={`h-4 w-4 text-white/30 transition-transform ${open ? 'rotate-180' : ''}`} />
126
+ <ChevronDown className={`h-4 w-4 shrink-0 text-white/30 transition-transform ${open ? 'rotate-180' : ''}`} />
126
127
  </button>
127
128
  {open && pos && createPortal(
128
129
  <div
@@ -196,8 +197,6 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
196
197
  const [piShowKey, setPiShowKey] = useState(false);
197
198
  const [piConnecting, setPiConnecting] = useState(false);
198
199
  const [piError, setPiError] = useState<string | undefined>();
199
- const [piTestRunning, setPiTestRunning] = useState(false);
200
- const [piTestResult, setPiTestResult] = useState<{ ok: boolean; text?: string; error?: string } | null>(null);
201
200
  const [piSavedStatus, setPiSavedStatus] = useState<{ subProvider?: string; modelId?: string; baseUrl?: string } | null>(null);
202
201
 
203
202
  // Anthropic/Claude-specific
@@ -280,6 +279,10 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
280
279
  const [portalOldPassError, setPortalOldPassError] = useState('');
281
280
  const [portalOldPassVerified, setPortalOldPassVerified] = useState(false);
282
281
  const [portalVerifying, setPortalVerifying] = useState(false);
282
+ // When the portal already has credentials, password fields are hidden by
283
+ // default so the user doesn't think they must re-authenticate to proceed.
284
+ // Toggling this opens the change-password sub-form.
285
+ const [portalChangeMode, setPortalChangeMode] = useState(false);
283
286
 
284
287
  // Whisper (step 5)
285
288
  const [whisperEnabled, setWhisperEnabled] = useState(false);
@@ -601,7 +604,6 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
601
604
  setOpenaiError(undefined);
602
605
  // Pi flow cleanup is per-sub-provider; the load effect handles re-hydration.
603
606
  setPiError(undefined);
604
- setPiTestResult(null);
605
607
  };
606
608
 
607
609
  /* ── Bloby (pi) handlers ── */
@@ -648,7 +650,6 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
648
650
  setPiBaseUrl(next?.baseUrl || '');
649
651
  setPiModelId(next?.defaultModel || '');
650
652
  setPiError(undefined);
651
- setPiTestResult(null);
652
653
  // Picking a new sub-provider invalidates the previously saved auth.
653
654
  if (authState.pi === 'connected') {
654
655
  setAuthState((s) => ({ ...s, pi: 'idle' }));
@@ -659,7 +660,6 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
659
660
  const handlePiConnect = async () => {
660
661
  if (!piSubProvider) return;
661
662
  setPiError(undefined);
662
- setPiTestResult(null);
663
663
  setPiConnecting(true);
664
664
  try {
665
665
  const payload = {
@@ -703,28 +703,9 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
703
703
  try { await fetch('/api/auth/pi', { method: 'DELETE' }); } catch {}
704
704
  setAuthState((s) => ({ ...s, pi: 'idle' }));
705
705
  setPiSavedStatus(null);
706
- setPiTestResult(null);
707
706
  setModel('');
708
707
  };
709
708
 
710
- const handlePiTestCompletion = async () => {
711
- setPiTestRunning(true);
712
- setPiTestResult(null);
713
- try {
714
- const res = await fetch('/api/auth/pi/completion', {
715
- method: 'POST',
716
- headers: { 'Content-Type': 'application/json' },
717
- body: JSON.stringify({}),
718
- });
719
- const data = await res.json();
720
- setPiTestResult({ ok: !!data?.ok, text: data?.text, error: data?.error });
721
- } catch (err: any) {
722
- setPiTestResult({ ok: false, error: err?.message || 'Request failed' });
723
- } finally {
724
- setPiTestRunning(false);
725
- }
726
- };
727
-
728
709
  /* ── Auth handlers: Anthropic/Claude ── */
729
710
 
730
711
  const openExternal = (url: string) => {
@@ -1719,29 +1700,65 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
1719
1700
  transition={{ duration: 0.15 }}
1720
1701
  >
1721
1702
  <h1 className="text-xl font-bold text-white tracking-tight">
1722
- Set a password
1703
+ {portalExists ? 'Password & 2FA' : 'Set a password'}
1723
1704
  </h1>
1724
1705
  <p className="text-white/40 text-[13px] mt-1.5 leading-relaxed">
1725
- You'll need this password to access your agent's chat. Keep it safe — anyone with your URL will need it to log in.
1706
+ {portalExists
1707
+ ? "Your portal password is already set — you can keep it as-is or change it below."
1708
+ : "You'll need this password to access your agent's chat. Keep it safe — anyone with your URL will need it to log in."}
1726
1709
  </p>
1727
1710
 
1728
- {portalExists && (
1729
- <div className="mt-4 bg-white/[0.02] border border-white/[0.06] rounded-xl px-4 py-2.5">
1730
- <p className="text-white/40 text-[12px]">Password already set. Leave fields empty to keep your current password, or enter your current password to change it.</p>
1711
+ {/* ── Existing-password collapsed state: just a "Change password" pill ── */}
1712
+ {portalExists && !portalChangeMode && (
1713
+ <div className="mt-5 flex items-center justify-between bg-white/[0.02] border border-white/[0.06] rounded-xl px-4 py-3">
1714
+ <div>
1715
+ <p className="text-white/70 text-[13px] font-medium">Portal password</p>
1716
+ <p className="text-white/30 text-[11px] mt-0.5">Already configured — click to change it.</p>
1717
+ </div>
1718
+ <button
1719
+ type="button"
1720
+ onClick={() => {
1721
+ setPortalChangeMode(true);
1722
+ setPortalOldPass('');
1723
+ setPortalOldPassError('');
1724
+ setPortalOldPassVerified(false);
1725
+ setPortalPass('');
1726
+ setPortalPassConfirm('');
1727
+ }}
1728
+ className="shrink-0 px-3.5 py-2 bg-white/[0.04] hover:bg-white/[0.08] text-white/70 hover:text-white text-[12px] font-medium rounded-lg transition-colors"
1729
+ >
1730
+ Change password
1731
+ </button>
1731
1732
  </div>
1732
1733
  )}
1733
1734
 
1734
- {portalExists && (
1735
+ {/* ── Existing-password expanded: current password verify step ── */}
1736
+ {portalExists && portalChangeMode && (
1735
1737
  <div className="mt-5">
1736
- <label className="text-[12px] text-white/40 font-medium mb-1.5 block">Current password</label>
1738
+ <div className="flex items-center justify-between mb-1.5">
1739
+ <label className="text-[12px] text-white/40 font-medium">Current password</label>
1740
+ <button
1741
+ type="button"
1742
+ onClick={() => {
1743
+ setPortalChangeMode(false);
1744
+ setPortalOldPass('');
1745
+ setPortalOldPassError('');
1746
+ setPortalOldPassVerified(false);
1747
+ setPortalPass('');
1748
+ setPortalPassConfirm('');
1749
+ }}
1750
+ className="text-white/30 hover:text-white/60 text-[11px] transition-colors"
1751
+ >
1752
+ Cancel
1753
+ </button>
1754
+ </div>
1737
1755
  <div className="flex items-center gap-2">
1738
1756
  <input
1739
1757
  type="password"
1740
1758
  value={portalOldPass}
1741
1759
  onChange={(e) => { setPortalOldPass(e.target.value); setPortalOldPassError(''); setPortalOldPassVerified(false); }}
1742
- placeholder="Enter current password to change it"
1760
+ placeholder="Enter your current password"
1743
1761
  autoComplete="current-password"
1744
- autoFocus
1745
1762
  className={inputCls + ' flex-1'}
1746
1763
  />
1747
1764
  {portalOldPass.length > 0 && !portalOldPassVerified && (
@@ -1785,7 +1802,8 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
1785
1802
  </div>
1786
1803
  )}
1787
1804
 
1788
- {(!portalExists || portalOldPassVerified) && (
1805
+ {/* ── New-password fields: first-time onboard OR verified change-mode ── */}
1806
+ {(!portalExists || (portalChangeMode && portalOldPassVerified)) && (
1789
1807
  <>
1790
1808
  <div className={portalExists ? 'mt-3' : 'mt-5'}>
1791
1809
  <label className="text-[12px] text-white/40 font-medium mb-1.5 block">
@@ -1839,6 +1857,16 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
1839
1857
  setTotpEnabled(true);
1840
1858
  setTotpError('');
1841
1859
  setTotpCode('');
1860
+ // Enabling 2FA on an existing portal requires the current
1861
+ // password to authenticate the setup call. If the user hasn't
1862
+ // entered it yet, open change-mode so the current-password
1863
+ // field becomes visible and surface a clear error.
1864
+ if (portalExists && !portalOldPassVerified) {
1865
+ setPortalChangeMode(true);
1866
+ setTotpError('Enter your current portal password above first, then toggle 2FA again.');
1867
+ setTotpEnabled(false);
1868
+ return;
1869
+ }
1842
1870
  // Fetch QR if not already loaded
1843
1871
  if (!totpQrUri) {
1844
1872
  try {
@@ -2217,15 +2245,15 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
2217
2245
  >
2218
2246
  <div className="flex flex-col items-center gap-1.5 py-0.5">
2219
2247
  {p.icon ? (
2220
- <img src={p.icon} alt={p.name} className="w-8 h-8 rounded-lg" />
2248
+ <img src={p.icon} alt={p.name} className="w-9 h-9 object-contain" />
2221
2249
  ) : (
2222
- <div className="w-8 h-8 rounded-lg bg-white/[0.06] flex items-center justify-center text-white/50 text-sm font-bold">
2250
+ <div className="w-9 h-9 rounded-lg bg-white/[0.06] flex items-center justify-center text-white/50 text-sm font-bold">
2223
2251
  O
2224
2252
  </div>
2225
2253
  )}
2226
2254
  <div className="text-center">
2227
2255
  <div className="text-[13px] font-medium text-white">{p.name}</div>
2228
- <div className="text-[10px] text-white/30">{p.subtitle}</div>
2256
+ <div className="text-[10px] text-white/30 whitespace-pre-line leading-tight">{p.subtitle}</div>
2229
2257
  </div>
2230
2258
  </div>
2231
2259
  {authState[p.id] === 'connected' ? (
@@ -2241,180 +2269,146 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
2241
2269
 
2242
2270
  <div className="border-t border-white/[0.06] mt-4 mb-3" />
2243
2271
 
2244
- {/* ── Auth flow: Bloby (pi) ── */}
2272
+ {/* ── Auth flow: Pi (bring your own model) ── */}
2245
2273
  {provider === 'pi' && (
2246
- <div className="space-y-3">
2274
+ <div className="space-y-2">
2247
2275
  {authState.pi === 'connected' && piSavedStatus ? (
2248
- <>
2249
- <div className="bg-emerald-500/8 border border-emerald-500/15 rounded-lg px-3.5 py-2.5">
2250
- <p className="text-emerald-400/90 text-[12px]">
2251
- Connected — {piSubProviders.find((p) => p.id === piSavedStatus.subProvider)?.name || piSavedStatus.subProvider}
2252
- {piSavedStatus.modelId ? <> · <span className="font-mono">{piSavedStatus.modelId}</span></> : null}
2253
- </p>
2254
- </div>
2255
-
2256
- <div className="bg-white/[0.03] border border-white/[0.06] rounded-xl p-3.5 space-y-2.5">
2257
- <p className="text-[12px] text-white/55">
2258
- Send a quick test request to confirm the model is reachable.
2259
- </p>
2260
- <button
2261
- onClick={handlePiTestCompletion}
2262
- disabled={piTestRunning}
2263
- className="w-full py-2 px-4 bg-white/[0.06] hover:bg-white/[0.1] text-white text-[12px] font-medium rounded-xl transition-colors flex items-center justify-center gap-2 disabled:opacity-60"
2264
- >
2265
- {piTestRunning ? (
2266
- <><LoaderCircle className="h-3.5 w-3.5 animate-spin" />Sending test request…</>
2267
- ) : (
2268
- <>Send test request<ArrowRight className="h-3.5 w-3.5 opacity-60" /></>
2269
- )}
2270
- </button>
2271
- {piTestResult && piTestResult.ok && (
2272
- <div className="bg-emerald-500/5 border border-emerald-500/15 rounded-lg px-3 py-2">
2273
- <p className="text-[11px] text-emerald-400/70 mb-1">Model reply</p>
2274
- <p className="text-[12px] text-white/85 whitespace-pre-wrap">{piTestResult.text}</p>
2275
- </div>
2276
- )}
2277
- {piTestResult && !piTestResult.ok && (
2278
- <div className="bg-red-500/8 border border-red-500/15 rounded-lg px-3 py-2">
2279
- <p className="text-[12px] text-red-400/90">{piTestResult.error}</p>
2280
- </div>
2281
- )}
2282
- </div>
2283
-
2276
+ <div className="flex items-center justify-between bg-emerald-500/8 border border-emerald-500/15 rounded-lg px-3 py-2">
2277
+ <p className="text-emerald-400/90 text-[12px] truncate">
2278
+ Connected — {piSubProviders.find((p) => p.id === piSavedStatus.subProvider)?.name || piSavedStatus.subProvider}
2279
+ {piSavedStatus.modelId ? <> · <span className="font-mono">{piSavedStatus.modelId}</span></> : null}
2280
+ </p>
2284
2281
  <button
2285
2282
  onClick={handlePiDisconnect}
2286
- className="w-full py-1.5 text-white/25 text-[11px] hover:text-white/40 transition-colors flex items-center justify-center gap-1.5"
2283
+ className="text-white/30 hover:text-white/60 text-[11px] flex items-center gap-1 shrink-0 ml-2"
2287
2284
  >
2288
2285
  <RefreshCw className="h-3 w-3" />
2289
- Use a different model
2286
+ Change
2290
2287
  </button>
2291
- </>
2288
+ </div>
2292
2289
  ) : (
2293
2290
  <>
2294
- <div>
2295
- <p className="text-[12px] text-white/40 mb-2">Pick an LLM provider — Bloby brings your own model.</p>
2296
- <div className="grid grid-cols-2 gap-2 max-h-[320px] overflow-y-auto pr-1">
2297
- {piSubProviders.map((sp) => (
2298
- <button
2299
- key={sp.id}
2300
- type="button"
2301
- onClick={() => choosePiSubProvider(sp.id)}
2302
- className={`relative text-left rounded-xl border px-3 py-2.5 transition-colors ${
2303
- piSubProvider === sp.id
2304
- ? 'border-[#AF27E3]/50 bg-[#AF27E3]/10'
2305
- : 'border-white/[0.08] bg-white/[0.02] hover:bg-white/[0.04]'
2306
- }`}
2307
- >
2308
- <p className="text-[12.5px] text-white font-medium leading-tight">{sp.name}</p>
2309
- <p className="text-[10.5px] text-white/40 mt-0.5 leading-tight">{sp.subtitle}</p>
2310
- {piSubProvider === sp.id && (
2311
- <div className="absolute top-1.5 right-1.5 w-1.5 h-1.5 rounded-full bg-gradient-brand" />
2312
- )}
2313
- </button>
2314
- ))}
2291
+ {/* Two-column compact row: provider dropdown + model picker */}
2292
+ <div className="grid grid-cols-2 gap-2">
2293
+ <div>
2294
+ <label className="text-[11px] text-white/40 font-medium mb-1 block">Provider</label>
2295
+ <ModelDropdown
2296
+ models={piSubProviders.map((sp) => ({ id: sp.id, label: sp.name }))}
2297
+ value={piSubProvider}
2298
+ onChange={choosePiSubProvider}
2299
+ />
2315
2300
  </div>
2316
- </div>
2317
-
2318
- {selectedPiSub && (
2319
- <div className="space-y-2 pt-1">
2320
- {(selectedPiSub.needsBaseUrl || selectedPiSub.flavor === 'openai-completions') && (
2321
- <div>
2322
- <label className="text-[11px] text-white/40 font-medium mb-1 block">
2323
- Base URL{selectedPiSub.needsBaseUrl ? '' : ' (override — optional)'}
2324
- </label>
2325
- <input
2326
- type="text"
2327
- value={piBaseUrl}
2328
- onChange={(e) => setPiBaseUrl(e.target.value)}
2329
- placeholder={selectedPiSub.baseUrl || 'https://example.com/v1'}
2330
- className={inputSmCls + ' font-mono'}
2331
- />
2332
- </div>
2301
+ <div>
2302
+ <label className="text-[11px] text-white/40 font-medium mb-1 block">Model</label>
2303
+ {selectedPiSub && Array.isArray(selectedPiSub.models) ? (
2304
+ <ModelDropdown
2305
+ models={selectedPiSub.models}
2306
+ value={piModelId}
2307
+ onChange={setPiModelId}
2308
+ />
2309
+ ) : (
2310
+ <input
2311
+ type="text"
2312
+ value={piModelId}
2313
+ onChange={(e) => setPiModelId(e.target.value)}
2314
+ placeholder={selectedPiSub?.defaultModel || 'model-id'}
2315
+ className={inputSmCls + ' font-mono'}
2316
+ />
2333
2317
  )}
2318
+ </div>
2319
+ </div>
2334
2320
 
2335
- {selectedPiSub.needsApiKey && (
2336
- <div>
2337
- <label className="text-[11px] text-white/40 font-medium mb-1 block flex items-center justify-between">
2338
- <span>API key</span>
2339
- {selectedPiSub.apiKeyUrl && (
2340
- <button
2341
- type="button"
2342
- onClick={() => openExternal(selectedPiSub.apiKeyUrl!)}
2343
- className="text-white/30 hover:text-white/60 text-[10.5px] flex items-center gap-1"
2344
- >
2345
- Get a key <ExternalLink className="h-3 w-3" />
2346
- </button>
2347
- )}
2348
- </label>
2349
- <div className="relative">
2350
- <input
2351
- type={piShowKey ? 'text' : 'password'}
2352
- value={piApiKey}
2353
- onChange={(e) => setPiApiKey(e.target.value)}
2354
- onKeyDown={(e) => e.key === 'Enter' && handlePiConnect()}
2355
- placeholder="sk-..."
2356
- className={inputSmCls + ' pr-10 font-mono'}
2357
- />
2358
- <button
2359
- type="button"
2360
- onClick={() => setPiShowKey((v) => !v)}
2361
- className="absolute right-3 top-1/2 -translate-y-1/2 text-white/20 hover:text-white/50 transition-colors"
2362
- >
2363
- {piShowKey ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
2364
- </button>
2365
- </div>
2366
- </div>
2367
- )}
2321
+ {selectedPiSub?.needsBaseUrl && (
2322
+ <input
2323
+ type="text"
2324
+ value={piBaseUrl}
2325
+ onChange={(e) => setPiBaseUrl(e.target.value)}
2326
+ placeholder={selectedPiSub.baseUrl || 'https://example.com/v1'}
2327
+ className={inputSmCls + ' font-mono'}
2328
+ />
2329
+ )}
2368
2330
 
2369
- <div>
2370
- <label className="text-[11px] text-white/40 font-medium mb-1 block">Model</label>
2371
- {Array.isArray(selectedPiSub.models) ? (
2372
- <ModelDropdown
2373
- models={selectedPiSub.models}
2374
- value={piModelId}
2375
- onChange={setPiModelId}
2376
- />
2377
- ) : (
2378
- <input
2379
- type="text"
2380
- value={piModelId}
2381
- onChange={(e) => setPiModelId(e.target.value)}
2382
- placeholder={selectedPiSub.defaultModel || 'model-id'}
2383
- className={inputSmCls + ' font-mono'}
2384
- />
2331
+ {selectedPiSub?.needsApiKey && (
2332
+ <div className="relative">
2333
+ <input
2334
+ type={piShowKey ? 'text' : 'password'}
2335
+ value={piApiKey}
2336
+ onChange={(e) => setPiApiKey(e.target.value)}
2337
+ onKeyDown={(e) => e.key === 'Enter' && handlePiConnect()}
2338
+ placeholder="API key…"
2339
+ className={inputSmCls + ' pr-16 font-mono'}
2340
+ />
2341
+ <div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1.5">
2342
+ {selectedPiSub.apiKeyUrl && (
2343
+ <button
2344
+ type="button"
2345
+ onClick={() => openExternal(selectedPiSub.apiKeyUrl!)}
2346
+ className="text-white/25 hover:text-white/55 transition-colors"
2347
+ title="Get a key"
2348
+ >
2349
+ <ExternalLink className="h-3.5 w-3.5" />
2350
+ </button>
2385
2351
  )}
2352
+ <button
2353
+ type="button"
2354
+ onClick={() => setPiShowKey((v) => !v)}
2355
+ className="text-white/25 hover:text-white/55 transition-colors"
2356
+ >
2357
+ {piShowKey ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
2358
+ </button>
2386
2359
  </div>
2360
+ </div>
2361
+ )}
2387
2362
 
2388
- {piError && (
2389
- <div className="bg-red-500/8 border border-red-500/15 rounded-lg px-3 py-2">
2390
- <p className="text-[12px] text-red-400/90 break-words">{piError}</p>
2391
- </div>
2392
- )}
2393
-
2394
- <button
2395
- onClick={handlePiConnect}
2396
- disabled={
2397
- piConnecting ||
2398
- !piSubProvider ||
2399
- (selectedPiSub.needsApiKey && !piApiKey.trim()) ||
2400
- (selectedPiSub.needsBaseUrl && !piBaseUrl.trim()) ||
2401
- !piModelId.trim()
2402
- }
2403
- className="w-full py-2.5 px-4 bg-gradient-brand hover:opacity-90 text-white text-[13px] font-medium rounded-xl transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
2404
- >
2405
- {piConnecting ? (
2406
- <><LoaderCircle className="h-3.5 w-3.5 animate-spin" />Connecting…</>
2407
- ) : (
2408
- <>Test &amp; connect<ArrowRight className="h-3.5 w-3.5 opacity-60" /></>
2409
- )}
2410
- </button>
2363
+ {piError && (
2364
+ <div className="bg-red-500/8 border border-red-500/15 rounded-lg px-3 py-2">
2365
+ <p className="text-[12px] text-red-400/90 break-words">{piError}</p>
2411
2366
  </div>
2412
2367
  )}
2368
+
2369
+ <button
2370
+ onClick={handlePiConnect}
2371
+ disabled={
2372
+ piConnecting ||
2373
+ !piSubProvider ||
2374
+ (!!selectedPiSub?.needsApiKey && !piApiKey.trim()) ||
2375
+ (!!selectedPiSub?.needsBaseUrl && !piBaseUrl.trim()) ||
2376
+ !piModelId.trim()
2377
+ }
2378
+ className="w-full py-2.5 px-4 bg-gradient-brand hover:opacity-90 text-white text-[13px] font-medium rounded-xl transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
2379
+ >
2380
+ {piConnecting ? (
2381
+ <><LoaderCircle className="h-3.5 w-3.5 animate-spin" />Connecting…</>
2382
+ ) : (
2383
+ <>Test &amp; connect<ArrowRight className="h-3.5 w-3.5 opacity-60" /></>
2384
+ )}
2385
+ </button>
2413
2386
  </>
2414
2387
  )}
2415
2388
  </div>
2416
2389
  )}
2417
2390
 
2391
+ {/* ── Auth flow: Bloby (coming soon placeholder) ── */}
2392
+ {provider === 'bloby' && (
2393
+ <div className="space-y-3">
2394
+ <p className="text-[12px] text-white/40 leading-relaxed">
2395
+ Bloby (managed) is on the way. Sign in with Google once it ships and your bot will be hosted, updated, and billed by us — no API keys to manage.
2396
+ </p>
2397
+ <button
2398
+ type="button"
2399
+ disabled
2400
+ title="Coming soon"
2401
+ className="w-full py-2.5 px-4 bg-white/[0.04] border border-white/[0.06] text-white/35 text-[13px] font-medium rounded-xl flex items-center justify-center gap-2 cursor-not-allowed"
2402
+ >
2403
+ <svg viewBox="0 0 24 24" className="h-4 w-4 opacity-50" fill="currentColor">
2404
+ <path d="M21.35 11.1h-9.17v2.96h5.27c-.23 1.39-1.62 4.08-5.27 4.08-3.17 0-5.76-2.62-5.76-5.85s2.59-5.85 5.76-5.85c1.81 0 3.02.77 3.71 1.43l2.53-2.43C16.85 3.91 14.74 3 12.18 3 7.03 3 2.86 7.17 2.86 12.29s4.17 9.29 9.32 9.29c5.38 0 8.94-3.79 8.94-9.12 0-.61-.07-1.08-.17-1.55Z"/>
2405
+ </svg>
2406
+ Login with Google
2407
+ </button>
2408
+ <p className="text-[10.5px] text-white/25 text-center">Not available yet.</p>
2409
+ </div>
2410
+ )}
2411
+
2418
2412
  {/* ── Auth flow: Anthropic ── */}
2419
2413
  {provider === 'anthropic' && (
2420
2414
  <div className="space-y-2.5">