fix(web): restore chat composer disabled-state messaging and clean up code (#2526)
This commit is contained in:
parent
773a94c414
commit
51ab3b1385
10 changed files with 184 additions and 161 deletions
|
|
@ -41,9 +41,7 @@ export async function postLauncherDashboardLogout(): Promise<boolean> {
|
||||||
return res.ok
|
return res.ok
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SetupResult =
|
export type SetupResult = { ok: true } | { ok: false; error: string }
|
||||||
| { ok: true }
|
|
||||||
| { ok: false; error: string }
|
|
||||||
|
|
||||||
export async function postLauncherDashboardSetup(
|
export async function postLauncherDashboardSetup(
|
||||||
password: string,
|
password: string,
|
||||||
|
|
@ -53,7 +51,10 @@ export async function postLauncherDashboardSetup(
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
credentials: "same-origin",
|
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 }
|
if (res.ok) return { ok: true }
|
||||||
let msg = "Unknown error"
|
let msg = "Unknown error"
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { Link } from "@tanstack/react-router"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { postLauncherDashboardLogout } from "@/api/launcher-auth"
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
|
|
@ -40,7 +41,6 @@ import {
|
||||||
} from "@/components/ui/tooltip"
|
} from "@/components/ui/tooltip"
|
||||||
import { useGateway } from "@/hooks/use-gateway.ts"
|
import { useGateway } from "@/hooks/use-gateway.ts"
|
||||||
import { useTheme } from "@/hooks/use-theme.ts"
|
import { useTheme } from "@/hooks/use-theme.ts"
|
||||||
import { postLauncherDashboardLogout } from "@/api/launcher-auth"
|
|
||||||
|
|
||||||
export function AppHeader() {
|
export function AppHeader() {
|
||||||
const { i18n, t } = useTranslation()
|
const { i18n, t } = useTranslation()
|
||||||
|
|
@ -198,27 +198,42 @@ export function AppHeader() {
|
||||||
<IconPower className="h-4 w-4 opacity-80" />
|
<IconPower className="h-4 w-4 opacity-80" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>{gwError ?? t("header.gateway.action.stop")}</TooltipContent>
|
<TooltipContent>
|
||||||
|
{gwError ?? t("header.gateway.action.stop")}
|
||||||
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
<Tooltip delayDuration={(gwError || (!canStart && startReason)) ? 0 : 700}>
|
<Tooltip
|
||||||
|
delayDuration={gwError || (!canStart && startReason) ? 0 : 700}
|
||||||
|
>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
{/* Wrap in span so the tooltip still fires when the button is disabled */}
|
{/* Wrap in span so the tooltip still fires when the button is disabled */}
|
||||||
<span
|
<span
|
||||||
className={!canStart && startReason ? "cursor-not-allowed" : undefined}
|
className={
|
||||||
|
!canStart && startReason ? "cursor-not-allowed" : undefined
|
||||||
|
}
|
||||||
tabIndex={!canStart && startReason ? 0 : undefined}
|
tabIndex={!canStart && startReason ? 0 : undefined}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant={
|
variant={
|
||||||
isStarting || isRestarting || isStopping ? "secondary" : "default"
|
isStarting || isRestarting || isStopping
|
||||||
|
? "secondary"
|
||||||
|
: "default"
|
||||||
}
|
}
|
||||||
size="sm"
|
size="sm"
|
||||||
data-tour="gateway-button"
|
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 ${
|
||||||
} ${!canStart ? "pointer-events-none" : ""}`}
|
isStopped
|
||||||
|
? "bg-green-500 text-white hover:bg-green-600"
|
||||||
|
: ""
|
||||||
|
} ${!canStart ? "pointer-events-none" : ""}`}
|
||||||
onClick={handleGatewayToggle}
|
onClick={handleGatewayToggle}
|
||||||
disabled={
|
disabled={
|
||||||
gwLoading || isStarting || isRestarting || isStopping || !canStart
|
gwLoading ||
|
||||||
|
isStarting ||
|
||||||
|
isRestarting ||
|
||||||
|
isStopping ||
|
||||||
|
!canStart
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{gwLoading || isStarting || isRestarting || isStopping ? (
|
{gwLoading || isStarting || isRestarting || isStopping ? (
|
||||||
|
|
@ -238,7 +253,7 @@ export function AppHeader() {
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
{(gwError || (!canStart && startReason)) ? (
|
{gwError || (!canStart && startReason) ? (
|
||||||
<TooltipContent>{gwError ?? startReason}</TooltipContent>
|
<TooltipContent>{gwError ?? startReason}</TooltipContent>
|
||||||
) : null}
|
) : null}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
||||||
|
|
@ -42,15 +42,11 @@ export function ChatComposer({
|
||||||
}: ChatComposerProps) {
|
}: ChatComposerProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const canInput = inputDisabledReason === null
|
const canInput = inputDisabledReason === null
|
||||||
const placeholder = canInput
|
const disabledMessage =
|
||||||
? t("chat.placeholder")
|
inputDisabledReason === null
|
||||||
: t(`chat.disabledPlaceholder.${inputDisabledReason}`)
|
? null
|
||||||
|
: t(`chat.disabledPlaceholder.${inputDisabledReason}`)
|
||||||
const inputDisabledReason = (() => {
|
const placeholder = disabledMessage ?? t("chat.placeholder")
|
||||||
if (!isConnected) return t("chat.inputDisabled.notConnected")
|
|
||||||
if (!hasDefaultModel) return t("chat.inputDisabled.noModel")
|
|
||||||
return null
|
|
||||||
})()
|
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (e.nativeEvent.isComposing) return
|
if (e.nativeEvent.isComposing) return
|
||||||
|
|
@ -95,7 +91,7 @@ export function ChatComposer({
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
disabled={!canInput}
|
disabled={!canInput}
|
||||||
title={inputDisabledReason || undefined}
|
title={disabledMessage || undefined}
|
||||||
className={cn(
|
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",
|
"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",
|
!canInput && "cursor-not-allowed",
|
||||||
|
|
@ -103,9 +99,9 @@ export function ChatComposer({
|
||||||
minRows={1}
|
minRows={1}
|
||||||
maxRows={8}
|
maxRows={8}
|
||||||
/>
|
/>
|
||||||
{!canInput && inputDisabledReason && (
|
{!canInput && disabledMessage && (
|
||||||
<div className="px-3 py-1 text-xs text-muted-foreground">
|
<div className="text-muted-foreground px-3 py-1 text-xs">
|
||||||
{inputDisabledReason}
|
{disabledMessage}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import { toast } from "sonner"
|
||||||
|
|
||||||
import { AssistantMessage } from "@/components/chat/assistant-message"
|
import { AssistantMessage } from "@/components/chat/assistant-message"
|
||||||
import {
|
import {
|
||||||
type ChatInputDisabledReason,
|
|
||||||
ChatComposer,
|
ChatComposer,
|
||||||
|
type ChatInputDisabledReason,
|
||||||
} from "@/components/chat/chat-composer"
|
} from "@/components/chat/chat-composer"
|
||||||
import { ChatEmptyState } from "@/components/chat/chat-empty-state"
|
import { ChatEmptyState } from "@/components/chat/chat-empty-state"
|
||||||
import { ModelSelector } from "@/components/chat/model-selector"
|
import { ModelSelector } from "@/components/chat/model-selector"
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,7 @@ import {
|
||||||
generateSessionId,
|
generateSessionId,
|
||||||
readStoredSessionId,
|
readStoredSessionId,
|
||||||
} from "@/features/chat/state"
|
} from "@/features/chat/state"
|
||||||
import {
|
import { invalidateSocket, isCurrentSocket } from "@/features/chat/websocket"
|
||||||
invalidateSocket,
|
|
||||||
isCurrentSocket,
|
|
||||||
} from "@/features/chat/websocket"
|
|
||||||
import i18n from "@/i18n"
|
import i18n from "@/i18n"
|
||||||
import {
|
import {
|
||||||
type ChatAttachment,
|
type ChatAttachment,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
import { normalizeUnixTimestamp } from "@/features/chat/state"
|
import { normalizeUnixTimestamp } from "@/features/chat/state"
|
||||||
import {
|
import { type AssistantMessageKind, updateChatStore } from "@/store/chat"
|
||||||
type AssistantMessageKind,
|
|
||||||
updateChatStore,
|
|
||||||
} from "@/store/chat"
|
|
||||||
|
|
||||||
export interface PicoMessage {
|
export interface PicoMessage {
|
||||||
type: string
|
type: string
|
||||||
|
|
|
||||||
|
|
@ -77,5 +77,15 @@ export function useGateway() {
|
||||||
}
|
}
|
||||||
}, [state])
|
}, [state])
|
||||||
|
|
||||||
return { state, loading, canStart, startReason, restartRequired, start, stop, restart, error }
|
return {
|
||||||
|
state,
|
||||||
|
loading,
|
||||||
|
canStart,
|
||||||
|
startReason,
|
||||||
|
restartRequired,
|
||||||
|
start,
|
||||||
|
stop,
|
||||||
|
restart,
|
||||||
|
error,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,9 @@ const RootLayout = () => {
|
||||||
globalThis.location.assign("/launcher-login")
|
globalThis.location.assign("/launcher-login")
|
||||||
} else {
|
} else {
|
||||||
setAuthError(
|
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.",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,10 @@ import { createFileRoute } from "@tanstack/react-router"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
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 { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
|
|
@ -37,7 +40,9 @@ function LauncherLoginPage() {
|
||||||
globalThis.location.assign("/launcher-setup")
|
globalThis.location.assign("/launcher-setup")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => { /* network error — stay on login page */ })
|
.catch(() => {
|
||||||
|
/* network error — stay on login page */
|
||||||
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const loginWithToken = React.useCallback(
|
const loginWithToken = React.useCallback(
|
||||||
|
|
|
||||||
|
|
@ -6,141 +6,141 @@ import { useTranslation } from "react-i18next"
|
||||||
import { postLauncherDashboardSetup } from "@/api/launcher-auth"
|
import { postLauncherDashboardSetup } from "@/api/launcher-auth"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card"
|
} from "@/components/ui/card"
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { useTheme } from "@/hooks/use-theme"
|
import { useTheme } from "@/hooks/use-theme"
|
||||||
|
|
||||||
function LauncherSetupPage() {
|
function LauncherSetupPage() {
|
||||||
const { t, i18n } = useTranslation()
|
const { t, i18n } = useTranslation()
|
||||||
const { theme, toggleTheme } = useTheme()
|
const { theme, toggleTheme } = useTheme()
|
||||||
const [password, setPassword] = React.useState("")
|
const [password, setPassword] = React.useState("")
|
||||||
const [confirm, setConfirm] = React.useState("")
|
const [confirm, setConfirm] = React.useState("")
|
||||||
const [submitting, setSubmitting] = React.useState(false)
|
const [submitting, setSubmitting] = React.useState(false)
|
||||||
const [error, setError] = React.useState("")
|
const [error, setError] = React.useState("")
|
||||||
|
|
||||||
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setError("")
|
setError("")
|
||||||
if (password !== confirm) {
|
if (password !== confirm) {
|
||||||
setError(t("launcherSetup.errorMismatch"))
|
setError(t("launcherSetup.errorMismatch"))
|
||||||
return
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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 (
|
return (
|
||||||
<div className="bg-background text-foreground flex min-h-dvh flex-col">
|
<div className="bg-background text-foreground flex min-h-dvh flex-col">
|
||||||
<header className="border-border/50 flex h-14 shrink-0 items-center justify-end gap-2 border-b px-4">
|
<header className="border-border/50 flex h-14 shrink-0 items-center justify-end gap-2 border-b px-4">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="outline" size="icon" aria-label="Language">
|
<Button variant="outline" size="icon" aria-label="Language">
|
||||||
<IconLanguage className="size-4" />
|
<IconLanguage className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem onClick={() => i18n.changeLanguage("en")}>
|
<DropdownMenuItem onClick={() => i18n.changeLanguage("en")}>
|
||||||
English
|
English
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => i18n.changeLanguage("zh")}>
|
<DropdownMenuItem onClick={() => i18n.changeLanguage("zh")}>
|
||||||
简体中文
|
简体中文
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => toggleTheme()}
|
onClick={() => toggleTheme()}
|
||||||
aria-label={theme === "dark" ? "Light mode" : "Dark mode"}
|
aria-label={theme === "dark" ? "Light mode" : "Dark mode"}
|
||||||
>
|
>
|
||||||
{theme === "dark" ? (
|
{theme === "dark" ? (
|
||||||
<IconSun className="size-4" />
|
<IconSun className="size-4" />
|
||||||
) : (
|
) : (
|
||||||
<IconMoon className="size-4" />
|
<IconMoon className="size-4" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="flex flex-1 items-center justify-center p-4">
|
<div className="flex flex-1 items-center justify-center p-4">
|
||||||
<Card className="w-full max-w-md" size="sm">
|
<Card className="w-full max-w-md" size="sm">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t("launcherSetup.title")}</CardTitle>
|
<CardTitle>{t("launcherSetup.title")}</CardTitle>
|
||||||
<CardDescription>{t("launcherSetup.description")}</CardDescription>
|
<CardDescription>{t("launcherSetup.description")}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form className="flex flex-col gap-4" onSubmit={onSubmit}>
|
<form className="flex flex-col gap-4" onSubmit={onSubmit}>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label htmlFor="setup-password">
|
<Label htmlFor="setup-password">
|
||||||
{t("launcherSetup.passwordLabel")}
|
{t("launcherSetup.passwordLabel")}
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="setup-password"
|
id="setup-password"
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
required
|
required
|
||||||
minLength={8}
|
minLength={8}
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
placeholder={t("launcherSetup.passwordPlaceholder")}
|
placeholder={t("launcherSetup.passwordPlaceholder")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label htmlFor="setup-confirm">
|
<Label htmlFor="setup-confirm">
|
||||||
{t("launcherSetup.confirmLabel")}
|
{t("launcherSetup.confirmLabel")}
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="setup-confirm"
|
id="setup-confirm"
|
||||||
name="confirm"
|
name="confirm"
|
||||||
type="password"
|
type="password"
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
required
|
required
|
||||||
minLength={8}
|
minLength={8}
|
||||||
value={confirm}
|
value={confirm}
|
||||||
onChange={(e) => setConfirm(e.target.value)}
|
onChange={(e) => setConfirm(e.target.value)}
|
||||||
placeholder={t("launcherSetup.confirmPlaceholder")}
|
placeholder={t("launcherSetup.confirmPlaceholder")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit" disabled={submitting}>
|
<Button type="submit" disabled={submitting}>
|
||||||
{submitting ? t("labels.loading") : t("launcherSetup.submit")}
|
{submitting ? t("labels.loading") : t("launcherSetup.submit")}
|
||||||
</Button>
|
</Button>
|
||||||
{error ? (
|
{error ? (
|
||||||
<p className="text-destructive text-sm" role="alert">
|
<p className="text-destructive text-sm" role="alert">
|
||||||
{error}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Route = createFileRoute("/launcher-setup")({
|
export const Route = createFileRoute("/launcher-setup")({
|
||||||
component: LauncherSetupPage,
|
component: LauncherSetupPage,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue