diff --git a/apps/web/src/pages/collaborators.tsx b/apps/web/src/pages/collaborators.tsx index de77cf2c..e0dd83ee 100644 --- a/apps/web/src/pages/collaborators.tsx +++ b/apps/web/src/pages/collaborators.tsx @@ -9,6 +9,7 @@ */ import { wording } from "../config.ts"; import { useCallback, useEffect, useState } from "react"; +import { toast } from "sonner"; import { Banner, Button, QuietSelect, StateLabel } from "../components.jsx"; import { productClient } from "../api/transport.ts"; import { workspaceTenantId, type HubUser } from "../api/auth.ts"; @@ -108,7 +109,7 @@ export function Collaborators({ user }: { user: HubUser | null }) { setEmail(""); await read(); } catch (cause) { - setError(await refusal(cause)); + toast.error(await refusal(cause)); } finally { setBusy(false); } @@ -122,7 +123,7 @@ export function Collaborators({ user }: { user: HubUser | null }) { await client.revokeInvitation(await workspaceTenantId(), invitationId); await read(); } catch (cause) { - setError(await refusal(cause)); + toast.error(await refusal(cause)); } finally { setBusy(false); } diff --git a/apps/web/src/pages/custom-providers.tsx b/apps/web/src/pages/custom-providers.tsx index 14721ee1..8c9bc213 100644 --- a/apps/web/src/pages/custom-providers.tsx +++ b/apps/web/src/pages/custom-providers.tsx @@ -39,7 +39,7 @@ import type { ModelCatalog, ModelProvider, } from "@corbits-builder/schemas/models"; -import { Banner, Button, QuietState } from "../components.jsx"; +import { Button, QuietState } from "../components.jsx"; import { keys } from "../queries/keys.ts"; import { productClient } from "../api/transport.ts"; import { workspaceTenantId } from "../api/auth.ts"; @@ -128,13 +128,12 @@ export function message(cause: unknown): string { return cause instanceof Error ? cause.message : String(cause); } -/** Title, lead, the fields, the last error, and Cancel or Back beside the dialog's action. */ +/** Title, lead, the fields, and Cancel or Back beside the dialog's action. */ export function ProviderDialog(props: { open: boolean; onOpenChange: (open: boolean) => void; title: string; description: string; - error: string | null; busy: boolean; ready: boolean; /** Whether submitting tests a connection first. */ @@ -148,7 +147,7 @@ export function ProviderDialog(props: { focusDialog?: boolean; children: ReactNode; }) { - const { busy, error, onBack, onOpenChange } = props; + const { busy, onBack, onOpenChange } = props; return ( {props.children} - {error ? : null} - {error ? : null} ); } @@ -871,12 +863,10 @@ function CustomProviderActions({ provider, apis, onChanged, - onError, }: { provider: ModelProvider; apis: Apis; onChanged: () => void; - onError: (message: string | null) => void; }) { const queryClient = useQueryClient(); const [editing, setEditing] = useState(false); @@ -886,7 +876,6 @@ function CustomProviderActions({ const remove = async () => { setRemoving(true); - onError(null); try { await productClient().removeCustomProvider( await workspaceTenantId(), @@ -896,7 +885,7 @@ function CustomProviderActions({ await queryClient.invalidateQueries({ queryKey: keys.modelCatalog }); onChanged(); } catch (cause) { - onError(message(cause)); + toast.error(message(cause)); } finally { setRemoving(false); } @@ -971,12 +960,10 @@ function EditCustomProviderDialog({ const queryClient = useQueryClient(); const form = useCustomProviderForm(apis, provider); const [busy, setBusy] = useState(false); - const [error, setError] = useState(null); const save = async () => { if (!form.ready || busy) return; setBusy(true); - setError(null); try { await form.save(); toast.success(copy.custom.saved(form.name)); @@ -984,7 +971,7 @@ function EditCustomProviderDialog({ onSaved(); onClose(); } catch (cause) { - setError(message(cause)); + toast.error(message(cause)); } finally { setBusy(false); } @@ -996,7 +983,6 @@ function EditCustomProviderDialog({ onOpenChange={(open) => (open || busy ? undefined : onClose())} title={copy.custom.editTitle(provider.label)} description={copy.custom.editLead} - error={error} busy={busy} ready={form.ready} tests={form.tests} diff --git a/apps/web/src/pages/invite-accept.tsx b/apps/web/src/pages/invite-accept.tsx index 3653f5a6..c13117ae 100644 --- a/apps/web/src/pages/invite-accept.tsx +++ b/apps/web/src/pages/invite-accept.tsx @@ -9,6 +9,7 @@ */ import { wording } from "../config.ts"; import { useEffect, useState } from "react"; +import { toast } from "sonner"; import { Banner, Button, Mark, StateLabel } from "../components.jsx"; import { productClient } from "../api/transport.ts"; import { getHubSession, type HubUser } from "../api/auth.ts"; @@ -58,12 +59,11 @@ export function InvitationAccept({ invitationId }: { invitationId: string }) { const accept = async () => { setBusy(true); - setFailed(null); try { const result = await productClient().acceptInvitation(invitationId); setAccepted(result); } catch (cause) { - setFailed(cause instanceof Error ? cause.message : String(cause)); + toast.error(cause instanceof Error ? cause.message : String(cause)); } finally { setBusy(false); } diff --git a/apps/web/src/pages/model-setup.tsx b/apps/web/src/pages/model-setup.tsx index 8899bbc9..fb20c9e5 100644 --- a/apps/web/src/pages/model-setup.tsx +++ b/apps/web/src/pages/model-setup.tsx @@ -61,7 +61,7 @@ export function ModelSetup({ onConnected(); }; const catalog = useModelCatalog(); - const { logins, signingIn, error, signIn } = useSubscriptionLogins(connected); + const { logins, signingIn, signIn } = useSubscriptionLogins(connected); const apiKeyProviders = (catalog.data?.providers ?? []).filter( (provider) => provider.kind === "api_key", ); @@ -85,7 +85,6 @@ export function ModelSetup({ {catalog.error ? ( ) : null} - {error ? : null} (null); const act = async (work: () => Promise) => { try { @@ -366,7 +365,6 @@ export function ProjectInfoDialog({ {project.title} - {error ? : null}
diff --git a/apps/web/src/pages/projects.tsx b/apps/web/src/pages/projects.tsx index 6e3b8d31..a3032f9c 100644 --- a/apps/web/src/pages/projects.tsx +++ b/apps/web/src/pages/projects.tsx @@ -44,7 +44,6 @@ export function Projects({ const [problem, setProblem] = useState(""); const [profile, setProfile] = useState(); const [busy, setBusy] = useState<"start" | "import" | null>(null); - const [error, setError] = useState(null); const starting = busy === "start"; const shell = useShellReply(useRef(null)); @@ -52,11 +51,10 @@ export function Projects({ if (busy !== null) return; if (shell.answers(problem)) return setProblem(""); if (!canStartProject(problem)) { - setError(wording.message.projectTooShort); + toast.error(wording.message.projectTooShort); return; } setBusy("start"); - setError(null); try { const created = await createProject({ problemStatement: problem.trim(), @@ -69,7 +67,7 @@ export function Projects({ // shows it until the conversation records it. onOpen(created.projectId, opening); } catch (cause) { - setError( + toast.error( cause instanceof ApiFailure ? cause.detail.message : String(cause), ); } finally { @@ -143,7 +141,6 @@ export function Projects({

{wording.newProject.title}

{wording.newProject.lead}

- {error ? : null} {needsProvider ? (

{wording.providers.noneHint}

diff --git a/apps/web/src/pages/providers.tsx b/apps/web/src/pages/providers.tsx index bc64e9d8..2ab0aaaa 100644 --- a/apps/web/src/pages/providers.tsx +++ b/apps/web/src/pages/providers.tsx @@ -88,26 +88,24 @@ export function useSubscriptionLogins(onConnected: () => void) { const catalog = useModelCatalog(); const queryClient = useQueryClient(); const [signingIn, setSigningIn] = useState(null); - const [error, setError] = useState(null); const logins = (catalog.data?.providers ?? []).filter( (provider) => provider.kind === "login", ); const signIn = async (provider: ModelProvider) => { setSigningIn(provider.id); - setError(null); try { await logIn(provider); await queryClient.invalidateQueries({ queryKey: keys.modelCatalog }); onConnected(); } catch (cause) { - setError(message(cause)); + toast.error(message(cause)); } finally { setSigningIn(null); } }; - return { logins, signingIn, error, signIn }; + return { logins, signingIn, signIn }; } /** One row per subscription login, each running the real sign-in through the hub. */ @@ -116,12 +114,10 @@ export function SubscriptionLogins({ }: { onConnected: () => void; }) { - const { logins, signingIn, error, signIn } = - useSubscriptionLogins(onConnected); + const { logins, signingIn, signIn } = useSubscriptionLogins(onConnected); return ( <> - {error ? : null} {logins.map((provider) => { const live = provider.connections.find( (connection) => connection.status === "connected", @@ -154,7 +150,6 @@ export function SubscriptionLogins({ export function ProvidersSection({ onChanged }: { onChanged: () => void }) { const catalog = useModelCatalog(); const [adding, setAdding] = useState(false); - const [error, setError] = useState(null); const providers = catalog.data?.providers ?? []; const customApis = catalog.data?.customApis; const hasLogins = providers.some((provider) => provider.kind === "login"); @@ -186,7 +181,6 @@ export function ProvidersSection({ onChanged }: { onChanged: () => void }) { provider={provider} apis={customApis} onChanged={onChanged} - onError={setError} /> ); } @@ -207,7 +201,6 @@ export function ProvidersSection({ onChanged }: { onChanged: () => void }) {
); })} - {error ? : null} ); const addAction = @@ -288,7 +281,6 @@ export function AddProviderDialog({ const [secret, setSecret] = useState(""); const customForm = useCustomProviderForm(apis); const [busy, setBusy] = useState(false); - const [error, setError] = useState(null); const custom = providerId === undefined && chosenId === CUSTOM_ENDPOINT; const chosen: ModelProvider | undefined = providers.find((provider) => provider.id === (providerId ?? chosenId)) ?? @@ -313,7 +305,6 @@ export function AddProviderDialog({ if (!next) { setSecret(""); customForm.reset(); - setError(null); } onOpenChange(next); }; @@ -330,7 +321,6 @@ export function AddProviderDialog({ const testAndSave = async () => { if (!ready || busy) return; setBusy(true); - setError(null); try { if (custom) { await customForm.save(); @@ -345,7 +335,7 @@ export function AddProviderDialog({ secret: secret.trim(), }); if (status !== "connected") { - setError(copy.failed[status]); + toast.error(copy.failed[status]); return; } await setCredential({ @@ -356,7 +346,7 @@ export function AddProviderDialog({ }); await saved(chosen.label); } catch (cause) { - setError(message(cause)); + toast.error(message(cause)); } finally { setBusy(false); } @@ -378,7 +368,6 @@ export function AddProviderDialog({ onOpenChange={close} title={dialogTitle} description={dialogDescription} - error={error} busy={busy} ready={ready} tests @@ -387,7 +376,6 @@ export function AddProviderDialog({ custom ? () => { setChosenId(null); - setError(null); } : undefined } @@ -408,7 +396,6 @@ export function AddProviderDialog({ labelledBy="provider-choice-label" onChoose={(id) => { setChosenId(id); - setError(null); }} /> diff --git a/apps/web/src/pages/settings.tsx b/apps/web/src/pages/settings.tsx index 8f4aa17d..98934c62 100644 --- a/apps/web/src/pages/settings.tsx +++ b/apps/web/src/pages/settings.tsx @@ -18,6 +18,7 @@ import { type ThemeMode, } from "@corbits/react-ui"; import { useEffect, useState, type ReactNode } from "react"; +import { toast } from "sonner"; import { useQueryClient } from "@tanstack/react-query"; import { keys } from "../queries/keys.ts"; import { ProvidersSection } from "./providers.jsx"; @@ -264,10 +265,10 @@ function WorkingPreferences() { void queryClient.invalidateQueries({ queryKey: keys.preferences }); const conflicted = receipt.conflicts.length > 0; setSave({ id, state: conflicted ? "failed" : "saved" }); - setError(conflicted ? wording.message.stepFailed : null); + if (conflicted) toast.error(wording.message.stepFailed); } catch (cause) { setSave({ id, state: "failed" }); - setError(cause instanceof Error ? cause.message : String(cause)); + toast.error(cause instanceof Error ? cause.message : String(cause)); } }; const copy = wording.settings.preferences; diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 67ad9338..1a2a2094 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -727,9 +727,6 @@ h3 { } /* Severity reads from the words and the state label, not a coloured edge. */ -.banner.is-error { - border-color: var(--wb-destructive); -} .banner.is-okay { border-color: var(--wb-okay); } diff --git a/apps/web/src/workspace-menu.tsx b/apps/web/src/workspace-menu.tsx index d79bd9a4..83e0bcf9 100644 --- a/apps/web/src/workspace-menu.tsx +++ b/apps/web/src/workspace-menu.tsx @@ -31,7 +31,8 @@ import { } from "@corbits/react-ui/ui/menu"; import { Check, ChevronDown, Plus } from "lucide-react"; import { useState } from "react"; -import { Banner, Button } from "./components.jsx"; +import { toast } from "sonner"; +import { Button } from "./components.jsx"; import { LogoMark } from "./flourish.tsx"; import { createWorkspace, @@ -125,7 +126,6 @@ function CreateWorkspaceDialog({ }) { const [name, setName] = useState(""); const [busy, setBusy] = useState(false); - const [error, setError] = useState(null); const [created, setCreated] = useState(null); const copy = wording.workspace; @@ -133,12 +133,11 @@ function CreateWorkspaceDialog({ const trimmed = name.trim(); if (busy || trimmed.length === 0) return; setBusy(true); - setError(null); try { setCreated(await createWorkspace(trimmed)); void queryClient.invalidateQueries({ queryKey: keys.workspaces }); } catch (cause) { - setError(humanFault(cause, "The workspace could not be created.")); + toast.error(humanFault(cause, "The workspace could not be created.")); } finally { setBusy(false); } @@ -162,7 +161,6 @@ function CreateWorkspaceDialog({ {copy.createLead} - {error ? : null}
{copy.nameLabel}