From ec52c728914bb86a58ccf4273da7e1a91e90ac1a Mon Sep 17 00:00:00 2001 From: Sawyer Date: Tue, 3 Nov 2026 21:31:03 -0800 Subject: [PATCH] feat(web): connect a model provider from the composer with Enable AI --- apps/web/src/App.tsx | 1 - apps/web/src/config.ts | 1 + apps/web/src/pages/model-setup.tsx | 144 ++++++++++++++++--------- apps/web/src/pages/workspace/index.tsx | 49 ++++----- 4 files changed, 119 insertions(+), 76 deletions(-) diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 52367833..96682c6b 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -869,7 +869,6 @@ export function App() { listedArchived: openProjectSummary.archivedAt !== null, })} onChanged={refresh} - onOpenSettings={openSettings} {...(openingMessage !== undefined ? { openingMessage } : {})} /> ) : ( diff --git a/apps/web/src/config.ts b/apps/web/src/config.ts index 310a7180..461119d4 100644 --- a/apps/web/src/config.ts +++ b/apps/web/src/config.ts @@ -84,6 +84,7 @@ export const wording = { signOut: "Sign out", tryAgain: "Try again", openSettings: "Open Settings", + enableAi: "Enable AI", unarchive: "Unarchive", markShipped: "Mark as shipped", reopen: "Reopen", diff --git a/apps/web/src/pages/model-setup.tsx b/apps/web/src/pages/model-setup.tsx index fb20c9e5..bf8165f2 100644 --- a/apps/web/src/pages/model-setup.tsx +++ b/apps/web/src/pages/model-setup.tsx @@ -6,6 +6,14 @@ import { useState } from "react"; import { productName, wording } from "../config.ts"; import { AuthLayout, DitherCanvas, KindCardGrid } from "@corbits/react-ui"; +import { + Dialog, + DialogBody, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@corbits/react-ui/ui/dialog"; import { Banner, Mark, QUIET_CONTROL } from "../components.jsx"; import { AddProviderDialog, @@ -49,13 +57,8 @@ export function needsModelSetup( ); } -export function ModelSetup({ - onConnected, - onLater, -}: { - onConnected: () => void; - onLater: () => void; -}) { +/** A tile per subscription login, then a tile per API-key provider opening the Add provider dialog. */ +function ProviderTiles({ onConnected }: { onConnected: () => void }) { const connected = () => { clearModelSetupDeferral(); onConnected(); @@ -67,6 +70,60 @@ export function ModelSetup({ ); const [keyOpen, setKeyOpen] = useState(false); const [keyProviderId, setKeyProviderId] = useState(); + return ( + <> + {catalog.error ? ( + + ) : null} + ({ + id: provider.id, + title: provider.label, + description: signingIn === provider.id ? copy.waiting : provider.hint, + icon: , + disabled: signingIn !== null, + }))} + onChange={(id) => { + const provider = logins.find((login) => login.id === id); + if (provider) void signIn(provider); + }} + /> +
+ ({ + id: provider.id, + title: provider.label, + description: provider.hint, + icon: , + disabled: signingIn !== null, + }))} + onChange={(id) => { + setKeyProviderId(id); + setKeyOpen(true); + }} + /> + + + ); +} + +export function ModelSetup({ + onConnected, + onLater, +}: { + onConnected: () => void; + onLater: () => void; +}) { return ( {copy.connect}

{copy.noneHint}

- {catalog.error ? ( - - ) : null} - ({ - id: provider.id, - title: provider.label, - description: - signingIn === provider.id ? copy.waiting : provider.hint, - icon: , - disabled: signingIn !== null, - }))} - onChange={(id) => { - const provider = logins.find((login) => login.id === id); - if (provider) void signIn(provider); - }} - /> -
- ({ - id: provider.id, - title: provider.label, - description: provider.hint, - icon: , - disabled: signingIn !== null, - }))} - onChange={(id) => { - setKeyProviderId(id); - setKeyOpen(true); - }} - /> - +
); - } else if (modelNeeded !== null) { - // A coordinator with no model to run on cannot answer, even one still bound from before. - dock = ( -
-

{modelNeeded}

-
- - {wording.action.openSettings} - - void startCoordinator()} - > - {wording.action.retry} - -
-
- ); - } else if (unbound) { + } else if (unbound && modelNeeded === null) { // The person does not start the coordinator by typing: an unbound room waits for the // start that opening it asked for, and a refused start says why and offers it again. if (startFault === null) { @@ -1393,7 +1373,7 @@ export function StageWorkspace({ onSubmit={send} canSubmit={value.trim() !== "" || files.length > 0} status={sending || approvalBusy ? "submitted" : "ready"} - disabled={sending || approvalBusy} + disabled={sending || approvalBusy || modelNeeded !== null} > {files.length === 0 ? null : ( @@ -1452,7 +1432,19 @@ export function StageWorkspace({ )} - + {modelNeeded === null ? ( + + ) : ( + setEnablingAi(true)} + > + {wording.action.enableAi} + + )} )} @@ -1556,6 +1548,11 @@ export function StageWorkspace({ {dock} + void startCoordinator()} + /> {chatFirst ? null : (