fix(web): restore chat composer disabled-state messaging and clean up code (#2526)

This commit is contained in:
wenjie 2026-04-15 11:24:27 +08:00 committed by GitHub
parent 773a94c414
commit 51ab3b1385
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 184 additions and 161 deletions

View file

@ -41,9 +41,7 @@ export async function postLauncherDashboardLogout(): Promise<boolean> {
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"

View file

@ -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() {
<IconPower className="h-4 w-4 opacity-80" />
</Button>
</TooltipTrigger>
<TooltipContent>{gwError ?? t("header.gateway.action.stop")}</TooltipContent>
<TooltipContent>
{gwError ?? t("header.gateway.action.stop")}
</TooltipContent>
</Tooltip>
) : (
<Tooltip delayDuration={(gwError || (!canStart && startReason)) ? 0 : 700}>
<Tooltip
delayDuration={gwError || (!canStart && startReason) ? 0 : 700}
>
<TooltipTrigger asChild>
{/* Wrap in span so the tooltip still fires when the button is disabled */}
<span
className={!canStart && startReason ? "cursor-not-allowed" : undefined}
className={
!canStart && startReason ? "cursor-not-allowed" : undefined
}
tabIndex={!canStart && startReason ? 0 : undefined}
>
<Button
variant={
isStarting || isRestarting || isStopping ? "secondary" : "default"
isStarting || isRestarting || isStopping
? "secondary"
: "default"
}
size="sm"
data-tour="gateway-button"
className={`h-8 gap-2 px-3 ${isStopped ? "bg-green-500 text-white hover:bg-green-600" : ""
className={`h-8 gap-2 px-3 ${
isStopped
? "bg-green-500 text-white hover:bg-green-600"
: ""
} ${!canStart ? "pointer-events-none" : ""}`}
onClick={handleGatewayToggle}
disabled={
gwLoading || isStarting || isRestarting || isStopping || !canStart
gwLoading ||
isStarting ||
isRestarting ||
isStopping ||
!canStart
}
>
{gwLoading || isStarting || isRestarting || isStopping ? (
@ -238,7 +253,7 @@ export function AppHeader() {
</Button>
</span>
</TooltipTrigger>
{(gwError || (!canStart && startReason)) ? (
{gwError || (!canStart && startReason) ? (
<TooltipContent>{gwError ?? startReason}</TooltipContent>
) : null}
</Tooltip>

View file

@ -42,15 +42,11 @@ export function ChatComposer({
}: ChatComposerProps) {
const { t } = useTranslation()
const canInput = inputDisabledReason === null
const placeholder = canInput
? t("chat.placeholder")
const disabledMessage =
inputDisabledReason === null
? null
: t(`chat.disabledPlaceholder.${inputDisabledReason}`)
const inputDisabledReason = (() => {
if (!isConnected) return t("chat.inputDisabled.notConnected")
if (!hasDefaultModel) return t("chat.inputDisabled.noModel")
return null
})()
const placeholder = disabledMessage ?? t("chat.placeholder")
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
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 && (
<div className="px-3 py-1 text-xs text-muted-foreground">
{inputDisabledReason}
{!canInput && disabledMessage && (
<div className="text-muted-foreground px-3 py-1 text-xs">
{disabledMessage}
</div>
)}

View file

@ -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"

View file

@ -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,

View file

@ -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

View file

@ -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,
}
}

View file

@ -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.",
)
}
})

View file

@ -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(