diff --git a/packages/web/src/components/layout/Sidebar.tsx b/packages/web/src/components/layout/Sidebar.tsx index 9305546..5f9a7b3 100644 --- a/packages/web/src/components/layout/Sidebar.tsx +++ b/packages/web/src/components/layout/Sidebar.tsx @@ -13,7 +13,9 @@ import { Sun, } from "lucide-react"; import { useEffect, useRef, useState } from "react"; +import { HealthDot } from "@/components/shared/HealthDot"; import { useDemo } from "@/hooks/useDemo"; +import { useHealthStatus } from "@/hooks/useHealthStatus"; import { useInstances } from "@/hooks/useInstances"; import { useTheme } from "@/hooks/useTheme"; import { COLOR } from "@/lib/constants"; @@ -29,6 +31,7 @@ export function Sidebar() { const { instances, active, activate } = useInstances(); const { theme, toggle } = useTheme(); const { demo, toggle: toggleDemo, mask } = useDemo(); + const { data: health } = useHealthStatus(); const [switcherOpen, setSwitcherOpen] = useState(false); const switcherRef = useRef(null); @@ -87,8 +90,12 @@ export function Sidebar() { title={mask(active.baseUrl)} >
-

- {active.name} +

+ + {active.name}

{mask(active.baseUrl.replace(/^https?:\/\//, ""))} diff --git a/packages/web/src/components/settings/InstancesManager.tsx b/packages/web/src/components/settings/InstancesManager.tsx index 4ff3af8..c707694 100644 --- a/packages/web/src/components/settings/InstancesManager.tsx +++ b/packages/web/src/components/settings/InstancesManager.tsx @@ -1,14 +1,20 @@ -import { motion } from "framer-motion"; -import { Check, Pencil, Plus, Server, Trash2 } from "lucide-react"; -import { useState } from "react"; -import { SettingsForm } from "@/components/settings/SettingsForm"; +import { AnimatePresence, motion } from "framer-motion"; +import { Check, ChevronRight, Cloud, Pencil, Plus, Server, Sparkles, Trash2 } from "lucide-react"; +import { useEffect, useState } from "react"; +import { type ConnectionPreset, SettingsForm } from "@/components/settings/SettingsForm"; import { Button } from "@/components/ui/button"; import { Muted } from "@/components/ui/typography"; import { useInstances } from "@/hooks/useInstances"; -import type { Instance } from "@/lib/config"; +import { checkConnection, HONCHO_CLOUD_URL, type Instance, isCloudInstance } from "@/lib/config"; import { COLOR } from "@/lib/constants"; -type Mode = { kind: "list" } | { kind: "create" } | { kind: "edit"; id: string }; +const LOCALHOST_PROBE_URL = "http://localhost:8000"; + +type Mode = + | { kind: "list" } + | { kind: "choose-type" } + | { kind: "create"; preset: ConnectionPreset } + | { kind: "edit"; id: string }; interface InstancesManagerProps { onActivated?: () => void; @@ -16,18 +22,32 @@ interface InstancesManagerProps { export function InstancesManager({ onActivated }: InstancesManagerProps) { const { instances, activeId, activate, remove } = useInstances(); - const [mode, setMode] = useState({ kind: "list" }); + const isFirstRun = instances.length === 0; + const [mode, setMode] = useState(isFirstRun ? { kind: "choose-type" } : { kind: "list" }); + + const backFromCreate = () => setMode(isFirstRun ? { kind: "choose-type" } : { kind: "list" }); + + if (mode.kind === "choose-type") { + return ( + setMode({ kind: "create", preset })} + onCancel={isFirstRun ? undefined : () => setMode({ kind: "list" })} + /> + ); + } if (mode.kind === "create") { return ( { setMode({ kind: "list" }); onActivated?.(); }} - onCancel={instances.length > 0 ? () => setMode({ kind: "list" }) : undefined} - hideCancel={instances.length === 0} + onCancel={backFromCreate} + hideCancel={false} + submitLabel={isFirstRun ? "Save Connection" : undefined} /> ); } @@ -44,17 +64,6 @@ export function InstancesManager({ onActivated }: InstancesManagerProps) { ); } - if (instances.length === 0) { - return ( - onActivated?.()} - hideCancel - submitLabel="Save Connection" - /> - ); - } - return (

@@ -76,7 +85,7 @@ export function InstancesManager({ onActivated }: InstancesManagerProps) { +
+ )} +
+ ); +} + +interface ConnectionTypeButtonProps { + icon: typeof Cloud; + title: string; + description: string; + accent?: boolean; + onClick: () => void; +} + +function ConnectionTypeButton({ + icon: Icon, + title, + description, + accent, + onClick, +}: ConnectionTypeButtonProps) { + return ( + + ); +} + interface InstanceRowProps { instance: Instance; active: boolean; @@ -96,6 +252,7 @@ interface InstanceRowProps { function InstanceRow({ instance, active, onActivate, onEdit, onDelete }: InstanceRowProps) { const [confirmingDelete, setConfirmingDelete] = useState(false); + const cloud = isCloudInstance(instance); return ( {active ? ( + ) : cloud ? ( + ) : ( )} @@ -134,7 +293,7 @@ function InstanceRow({ instance, active, onActivate, onEdit, onDelete }: Instanc {instance.name}

- {instance.baseUrl.replace(/^https?:\/\//, "")} + {cloud ? "Honcho Cloud" : instance.baseUrl.replace(/^https?:\/\//, "")}
diff --git a/packages/web/src/components/settings/SettingsForm.tsx b/packages/web/src/components/settings/SettingsForm.tsx index 236bdf0..1a767a5 100644 --- a/packages/web/src/components/settings/SettingsForm.tsx +++ b/packages/web/src/components/settings/SettingsForm.tsx @@ -1,17 +1,37 @@ import { AnimatePresence, motion } from "framer-motion"; -import { AlertCircle, CheckCircle, Loader, Lock, LockOpen, Wifi, WifiOff } from "lucide-react"; +import { + AlertCircle, + CheckCircle, + Cloud, + Loader, + Lock, + LockOpen, + Wifi, + WifiOff, +} from "lucide-react"; import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Input, Textarea } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Muted } from "@/components/ui/typography"; import { useInstances } from "@/hooks/useInstances"; -import { checkConnection, type HealthStatus, type Instance, instanceSchema } from "@/lib/config"; +import { + checkConnection, + type HealthStatus, + HONCHO_CLOUD_URL, + type Instance, + instanceSchema, + isCloudInstance, +} from "@/lib/config"; import { COLOR } from "@/lib/constants"; +export type ConnectionPreset = "cloud" | "self-hosted"; + interface SettingsFormProps { /** Instance to edit; pass `null` to create a new one. */ instance: Instance | null; + /** Whether this form is for a Cloud or Self-Hosted connection. Inferred from `instance` in edit mode. */ + preset?: ConnectionPreset; /** Called after a successful save. Receives the saved instance id. */ onSaved?: (id: string) => void; /** Called when the user cancels (only meaningful when there's something to cancel back to). */ @@ -31,6 +51,7 @@ const statusConfig = { export function SettingsForm({ instance, + preset, onSaved, onCancel, hideCancel, @@ -38,8 +59,17 @@ export function SettingsForm({ }: SettingsFormProps) { const { add, update, activate } = useInstances(); - const [name, setName] = useState(instance?.name ?? ""); - const [baseUrl, setBaseUrl] = useState(instance?.baseUrl ?? "http://localhost:8000"); + const resolvedPreset: ConnectionPreset = + preset ?? (instance && isCloudInstance(instance) ? "cloud" : "self-hosted"); + const isCloud = resolvedPreset === "cloud"; + + const initialName = instance?.name ?? (isCloud ? "Honcho Cloud" : ""); + const initialBaseUrl = isCloud + ? HONCHO_CLOUD_URL + : (instance?.baseUrl ?? "http://localhost:8000"); + + const [name, setName] = useState(initialName); + const [baseUrl, setBaseUrl] = useState(initialBaseUrl); const [token, setToken] = useState(instance?.token ?? ""); const [errors, setErrors] = useState>>({}); const [saved, setSaved] = useState(false); @@ -64,8 +94,8 @@ export function SettingsForm({ e.preventDefault(); const candidate = { id: instance?.id ?? "placeholder", - name: name.trim() || "Default", - baseUrl, + name: name.trim() || (isCloud ? "Honcho Cloud" : "Default"), + baseUrl: isCloud ? HONCHO_CLOUD_URL : baseUrl, token, }; const result = instanceSchema.safeParse(candidate); @@ -78,6 +108,10 @@ export function SettingsForm({ setErrors(fieldErrors); return; } + if (isCloud && !token.trim()) { + setErrors({ token: "API key is required for Honcho Cloud" }); + return; + } setErrors({}); let id: string; @@ -136,18 +170,36 @@ export function SettingsForm({ {/* Base URL */}
- +
- { - setBaseUrl(e.target.value); - setHealth(null); - }} - placeholder="http://localhost:8000" - className="flex-1 font-mono rounded-xl" - /> + {isCloud ? ( +
+ + {HONCHO_CLOUD_URL} +
+ ) : ( + { + setBaseUrl(e.target.value); + setHealth(null); + }} + placeholder="http://localhost:8000" + className="flex-1 font-mono rounded-xl" + /> + )}
- {errors.baseUrl && ( + {errors.baseUrl && !isCloud && (

{errors.baseUrl}

)} - URL of your self-hosted Honcho instance + + {isCloud + ? "Hosted Honcho service — endpoint is fixed" + : "URL of your self-hosted Honcho instance"} +
{/* Health status */} @@ -225,27 +281,39 @@ export function SettingsForm({ strokeWidth={1.5} /> )} - API Token + {isCloud ? "API key" : "API Token"} - optional + {isCloud ? "required" : "optional"}