From 51ab3b13854ce2770495143998fcc3dceed0b1b8 Mon Sep 17 00:00:00 2001 From: wenjie Date: Wed, 15 Apr 2026 11:24:27 +0800 Subject: [PATCH] fix(web): restore chat composer disabled-state messaging and clean up code (#2526) --- web/frontend/src/api/launcher-auth.ts | 9 +- web/frontend/src/components/app-header.tsx | 33 ++- .../src/components/chat/chat-composer.tsx | 22 +- .../src/components/chat/chat-page.tsx | 2 +- web/frontend/src/features/chat/controller.ts | 5 +- web/frontend/src/features/chat/protocol.ts | 5 +- web/frontend/src/hooks/use-gateway.ts | 12 +- web/frontend/src/routes/__root.tsx | 4 +- web/frontend/src/routes/launcher-login.tsx | 9 +- web/frontend/src/routes/launcher-setup.tsx | 244 +++++++++--------- 10 files changed, 184 insertions(+), 161 deletions(-) diff --git a/web/frontend/src/api/launcher-auth.ts b/web/frontend/src/api/launcher-auth.ts index ed2e30687..d6bd93c4d 100644 --- a/web/frontend/src/api/launcher-auth.ts +++ b/web/frontend/src/api/launcher-auth.ts @@ -41,9 +41,7 @@ export async function postLauncherDashboardLogout(): Promise { return res.ok } -export type SetupResult = - | { ok: true } - | { ok: false; error: string } +export type SetupResult = { ok: true } | { ok: false; error: string } export async function postLauncherDashboardSetup( password: string, @@ -53,7 +51,10 @@ export async function postLauncherDashboardSetup( method: "POST", headers: { "Content-Type": "application/json" }, credentials: "same-origin", - body: JSON.stringify({ password: password.trim(), confirm: confirm.trim() }), + body: JSON.stringify({ + password: password.trim(), + confirm: confirm.trim(), + }), }) if (res.ok) return { ok: true } let msg = "Unknown error" diff --git a/web/frontend/src/components/app-header.tsx b/web/frontend/src/components/app-header.tsx index 798ac8ad5..e94975075 100644 --- a/web/frontend/src/components/app-header.tsx +++ b/web/frontend/src/components/app-header.tsx @@ -14,6 +14,7 @@ import { Link } from "@tanstack/react-router" import * as React from "react" import { useTranslation } from "react-i18next" +import { postLauncherDashboardLogout } from "@/api/launcher-auth" import { AlertDialog, AlertDialogAction, @@ -40,7 +41,6 @@ import { } from "@/components/ui/tooltip" import { useGateway } from "@/hooks/use-gateway.ts" import { useTheme } from "@/hooks/use-theme.ts" -import { postLauncherDashboardLogout } from "@/api/launcher-auth" export function AppHeader() { const { i18n, t } = useTranslation() @@ -198,27 +198,42 @@ export function AppHeader() { - {gwError ?? t("header.gateway.action.stop")} + + {gwError ?? t("header.gateway.action.stop")} + ) : ( - + {/* Wrap in span so the tooltip still fires when the button is disabled */} - {(gwError || (!canStart && startReason)) ? ( + {gwError || (!canStart && startReason) ? ( {gwError ?? startReason} ) : null} diff --git a/web/frontend/src/components/chat/chat-composer.tsx b/web/frontend/src/components/chat/chat-composer.tsx index 53465a788..58612d846 100644 --- a/web/frontend/src/components/chat/chat-composer.tsx +++ b/web/frontend/src/components/chat/chat-composer.tsx @@ -42,15 +42,11 @@ export function ChatComposer({ }: ChatComposerProps) { const { t } = useTranslation() const canInput = inputDisabledReason === null - const placeholder = canInput - ? t("chat.placeholder") - : t(`chat.disabledPlaceholder.${inputDisabledReason}`) - - const inputDisabledReason = (() => { - if (!isConnected) return t("chat.inputDisabled.notConnected") - if (!hasDefaultModel) return t("chat.inputDisabled.noModel") - return null - })() + const disabledMessage = + inputDisabledReason === null + ? null + : t(`chat.disabledPlaceholder.${inputDisabledReason}`) + const placeholder = disabledMessage ?? t("chat.placeholder") const handleKeyDown = (e: KeyboardEvent) => { if (e.nativeEvent.isComposing) return @@ -95,7 +91,7 @@ export function ChatComposer({ onKeyDown={handleKeyDown} placeholder={placeholder} disabled={!canInput} - title={inputDisabledReason || undefined} + title={disabledMessage || undefined} className={cn( "placeholder:text-muted-foreground/50 max-h-[200px] min-h-[60px] resize-none border-0 bg-transparent px-2 py-1 text-[15px] shadow-none transition-colors focus-visible:ring-0 focus-visible:outline-none dark:bg-transparent", !canInput && "cursor-not-allowed", @@ -103,9 +99,9 @@ export function ChatComposer({ minRows={1} maxRows={8} /> - {!canInput && inputDisabledReason && ( -
- {inputDisabledReason} + {!canInput && disabledMessage && ( +
+ {disabledMessage}
)} diff --git a/web/frontend/src/components/chat/chat-page.tsx b/web/frontend/src/components/chat/chat-page.tsx index 30be8d581..4129d812a 100644 --- a/web/frontend/src/components/chat/chat-page.tsx +++ b/web/frontend/src/components/chat/chat-page.tsx @@ -5,8 +5,8 @@ import { toast } from "sonner" import { AssistantMessage } from "@/components/chat/assistant-message" import { - type ChatInputDisabledReason, ChatComposer, + type ChatInputDisabledReason, } from "@/components/chat/chat-composer" import { ChatEmptyState } from "@/components/chat/chat-empty-state" import { ModelSelector } from "@/components/chat/model-selector" diff --git a/web/frontend/src/features/chat/controller.ts b/web/frontend/src/features/chat/controller.ts index c5c93d2e8..28ef491fa 100644 --- a/web/frontend/src/features/chat/controller.ts +++ b/web/frontend/src/features/chat/controller.ts @@ -12,10 +12,7 @@ import { generateSessionId, readStoredSessionId, } from "@/features/chat/state" -import { - invalidateSocket, - isCurrentSocket, -} from "@/features/chat/websocket" +import { invalidateSocket, isCurrentSocket } from "@/features/chat/websocket" import i18n from "@/i18n" import { type ChatAttachment, diff --git a/web/frontend/src/features/chat/protocol.ts b/web/frontend/src/features/chat/protocol.ts index a7edfc21b..717b42f84 100644 --- a/web/frontend/src/features/chat/protocol.ts +++ b/web/frontend/src/features/chat/protocol.ts @@ -1,10 +1,7 @@ import { toast } from "sonner" import { normalizeUnixTimestamp } from "@/features/chat/state" -import { - type AssistantMessageKind, - updateChatStore, -} from "@/store/chat" +import { type AssistantMessageKind, updateChatStore } from "@/store/chat" export interface PicoMessage { type: string diff --git a/web/frontend/src/hooks/use-gateway.ts b/web/frontend/src/hooks/use-gateway.ts index 31bee0e91..cbf132941 100644 --- a/web/frontend/src/hooks/use-gateway.ts +++ b/web/frontend/src/hooks/use-gateway.ts @@ -77,5 +77,15 @@ export function useGateway() { } }, [state]) - return { state, loading, canStart, startReason, restartRequired, start, stop, restart, error } + return { + state, + loading, + canStart, + startReason, + restartRequired, + start, + stop, + restart, + error, + } } diff --git a/web/frontend/src/routes/__root.tsx b/web/frontend/src/routes/__root.tsx index b5af5de45..60d45ef84 100644 --- a/web/frontend/src/routes/__root.tsx +++ b/web/frontend/src/routes/__root.tsx @@ -53,7 +53,9 @@ const RootLayout = () => { globalThis.location.assign("/launcher-login") } else { setAuthError( - err instanceof Error ? err.message : "Auth service unavailable, please try to delete the launcher-auth.db at picoclaw home directory and restart the application.", + err instanceof Error + ? err.message + : "Auth service unavailable, please try to delete the launcher-auth.db at picoclaw home directory and restart the application.", ) } }) diff --git a/web/frontend/src/routes/launcher-login.tsx b/web/frontend/src/routes/launcher-login.tsx index c5626fbb0..caa548c79 100644 --- a/web/frontend/src/routes/launcher-login.tsx +++ b/web/frontend/src/routes/launcher-login.tsx @@ -3,7 +3,10 @@ import { createFileRoute } from "@tanstack/react-router" import * as React from "react" import { useTranslation } from "react-i18next" -import { postLauncherDashboardLogin, getLauncherAuthStatus } from "@/api/launcher-auth" +import { + getLauncherAuthStatus, + postLauncherDashboardLogin, +} from "@/api/launcher-auth" import { Button } from "@/components/ui/button" import { Card, @@ -37,7 +40,9 @@ function LauncherLoginPage() { globalThis.location.assign("/launcher-setup") } }) - .catch(() => { /* network error — stay on login page */ }) + .catch(() => { + /* network error — stay on login page */ + }) }, []) const loginWithToken = React.useCallback( diff --git a/web/frontend/src/routes/launcher-setup.tsx b/web/frontend/src/routes/launcher-setup.tsx index 876af94fb..87c934a09 100644 --- a/web/frontend/src/routes/launcher-setup.tsx +++ b/web/frontend/src/routes/launcher-setup.tsx @@ -6,141 +6,141 @@ import { useTranslation } from "react-i18next" import { postLauncherDashboardSetup } from "@/api/launcher-auth" import { Button } from "@/components/ui/button" import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, } from "@/components/ui/card" import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { useTheme } from "@/hooks/use-theme" function LauncherSetupPage() { - const { t, i18n } = useTranslation() - const { theme, toggleTheme } = useTheme() - const [password, setPassword] = React.useState("") - const [confirm, setConfirm] = React.useState("") - const [submitting, setSubmitting] = React.useState(false) - const [error, setError] = React.useState("") + const { t, i18n } = useTranslation() + const { theme, toggleTheme } = useTheme() + const [password, setPassword] = React.useState("") + const [confirm, setConfirm] = React.useState("") + const [submitting, setSubmitting] = React.useState(false) + const [error, setError] = React.useState("") - const onSubmit = async (e: React.FormEvent) => { - e.preventDefault() - setError("") - if (password !== confirm) { - setError(t("launcherSetup.errorMismatch")) - return - } - setSubmitting(true) - try { - const result = await postLauncherDashboardSetup(password, confirm) - if (result.ok) { - globalThis.location.assign("/launcher-login") - return - } - setError(result.error) - } catch { - setError(t("launcherSetup.errorNetwork")) - } finally { - setSubmitting(false) - } + const onSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError("") + if (password !== confirm) { + setError(t("launcherSetup.errorMismatch")) + return } + setSubmitting(true) + try { + const result = await postLauncherDashboardSetup(password, confirm) + if (result.ok) { + globalThis.location.assign("/launcher-login") + return + } + setError(result.error) + } catch { + setError(t("launcherSetup.errorNetwork")) + } finally { + setSubmitting(false) + } + } - return ( -
-
- - - - - - i18n.changeLanguage("en")}> - English - - i18n.changeLanguage("zh")}> - 简体中文 - - - - -
+ return ( +
+
+ + + + + + i18n.changeLanguage("en")}> + English + + i18n.changeLanguage("zh")}> + 简体中文 + + + + +
-
- - - {t("launcherSetup.title")} - {t("launcherSetup.description")} - - -
-
- - setPassword(e.target.value)} - placeholder={t("launcherSetup.passwordPlaceholder")} - /> -
-
- - setConfirm(e.target.value)} - placeholder={t("launcherSetup.confirmPlaceholder")} - /> -
- - {error ? ( -

- {error} -

- ) : null} -
-
-
-
-
- ) +
+ + + {t("launcherSetup.title")} + {t("launcherSetup.description")} + + +
+
+ + setPassword(e.target.value)} + placeholder={t("launcherSetup.passwordPlaceholder")} + /> +
+
+ + setConfirm(e.target.value)} + placeholder={t("launcherSetup.confirmPlaceholder")} + /> +
+ + {error ? ( +

+ {error} +

+ ) : null} +
+
+
+
+
+ ) } export const Route = createFileRoute("/launcher-setup")({ - component: LauncherSetupPage, + component: LauncherSetupPage, })