mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
feat(web): show disabled chat reasons in composer
This commit is contained in:
@@ -7,6 +7,18 @@ import { Button } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils"
|
||||
import type { ChatAttachment } from "@/store/chat"
|
||||
|
||||
export type ChatInputDisabledReason =
|
||||
| "gatewayUnknown"
|
||||
| "gatewayStarting"
|
||||
| "gatewayRestarting"
|
||||
| "gatewayStopping"
|
||||
| "gatewayStopped"
|
||||
| "gatewayError"
|
||||
| "websocketConnecting"
|
||||
| "websocketDisconnected"
|
||||
| "websocketError"
|
||||
| "noDefaultModel"
|
||||
|
||||
interface ChatComposerProps {
|
||||
input: string
|
||||
attachments: ChatAttachment[]
|
||||
@@ -14,8 +26,7 @@ interface ChatComposerProps {
|
||||
onAddImages: () => void
|
||||
onRemoveAttachment: (index: number) => void
|
||||
onSend: () => void
|
||||
isConnected: boolean
|
||||
hasDefaultModel: boolean
|
||||
inputDisabledReason: ChatInputDisabledReason | null
|
||||
canSend: boolean
|
||||
}
|
||||
|
||||
@@ -26,12 +37,14 @@ export function ChatComposer({
|
||||
onAddImages,
|
||||
onRemoveAttachment,
|
||||
onSend,
|
||||
isConnected,
|
||||
hasDefaultModel,
|
||||
inputDisabledReason,
|
||||
canSend,
|
||||
}: ChatComposerProps) {
|
||||
const { t } = useTranslation()
|
||||
const canInput = isConnected && hasDefaultModel
|
||||
const canInput = inputDisabledReason === null
|
||||
const placeholder = canInput
|
||||
? t("chat.placeholder")
|
||||
: t(`chat.disabledPlaceholder.${inputDisabledReason}`)
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.nativeEvent.isComposing) return
|
||||
@@ -74,7 +87,7 @@ export function ChatComposer({
|
||||
value={input}
|
||||
onChange={(e) => onInputChange(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={t("chat.placeholder")}
|
||||
placeholder={placeholder}
|
||||
disabled={!canInput}
|
||||
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",
|
||||
@@ -100,15 +113,17 @@ export function ChatComposer({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
className="size-8 rounded-full bg-violet-500 text-white transition-transform hover:bg-violet-600 active:scale-95"
|
||||
onClick={onSend}
|
||||
disabled={!canSend}
|
||||
>
|
||||
<IconArrowUp className="size-4" />
|
||||
</Button>
|
||||
{canInput ? (
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
className="size-8 rounded-full bg-violet-500 text-white transition-transform hover:bg-violet-600 active:scale-95"
|
||||
onClick={onSend}
|
||||
disabled={!canSend}
|
||||
>
|
||||
<IconArrowUp className="size-4" />
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,10 @@ import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { AssistantMessage } from "@/components/chat/assistant-message"
|
||||
import { ChatComposer } from "@/components/chat/chat-composer"
|
||||
import {
|
||||
type ChatInputDisabledReason,
|
||||
ChatComposer,
|
||||
} from "@/components/chat/chat-composer"
|
||||
import { ChatEmptyState } from "@/components/chat/chat-empty-state"
|
||||
import { ModelSelector } from "@/components/chat/model-selector"
|
||||
import { SessionHistoryMenu } from "@/components/chat/session-history-menu"
|
||||
@@ -16,7 +19,9 @@ import { useChatModels } from "@/hooks/use-chat-models"
|
||||
import { useGateway } from "@/hooks/use-gateway"
|
||||
import { usePicoChat } from "@/hooks/use-pico-chat"
|
||||
import { useSessionHistory } from "@/hooks/use-session-history"
|
||||
import type { ConnectionState } from "@/store/chat"
|
||||
import type { ChatAttachment } from "@/store/chat"
|
||||
import type { GatewayState } from "@/store/gateway"
|
||||
|
||||
const MAX_IMAGE_SIZE_BYTES = 7 * 1024 * 1024
|
||||
const MAX_IMAGE_SIZE_LABEL = "7 MB"
|
||||
@@ -44,6 +49,58 @@ function readFileAsDataUrl(file: File): Promise<string> {
|
||||
})
|
||||
}
|
||||
|
||||
function resolveChatInputDisabledReason({
|
||||
hasDefaultModel,
|
||||
connectionState,
|
||||
gatewayState,
|
||||
}: {
|
||||
hasDefaultModel: boolean
|
||||
connectionState: ConnectionState
|
||||
gatewayState: GatewayState
|
||||
}): ChatInputDisabledReason | null {
|
||||
if (gatewayState === "unknown") {
|
||||
return "gatewayUnknown"
|
||||
}
|
||||
|
||||
if (gatewayState === "starting") {
|
||||
return "gatewayStarting"
|
||||
}
|
||||
|
||||
if (gatewayState === "restarting") {
|
||||
return "gatewayRestarting"
|
||||
}
|
||||
|
||||
if (gatewayState === "stopping") {
|
||||
return "gatewayStopping"
|
||||
}
|
||||
|
||||
if (gatewayState === "stopped") {
|
||||
return "gatewayStopped"
|
||||
}
|
||||
|
||||
if (gatewayState === "error") {
|
||||
return "gatewayError"
|
||||
}
|
||||
|
||||
if (connectionState === "connecting") {
|
||||
return "websocketConnecting"
|
||||
}
|
||||
|
||||
if (connectionState === "error") {
|
||||
return "websocketError"
|
||||
}
|
||||
|
||||
if (connectionState === "disconnected") {
|
||||
return "websocketDisconnected"
|
||||
}
|
||||
|
||||
if (!hasDefaultModel) {
|
||||
return "noDefaultModel"
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function ChatPage() {
|
||||
const { t } = useTranslation()
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
@@ -65,7 +122,6 @@ export function ChatPage() {
|
||||
|
||||
const { state: gwState } = useGateway()
|
||||
const isGatewayRunning = gwState === "running"
|
||||
const isChatConnected = connectionState === "connected"
|
||||
|
||||
const {
|
||||
defaultModelName,
|
||||
@@ -75,7 +131,13 @@ export function ChatPage() {
|
||||
localModels,
|
||||
handleSetDefault,
|
||||
} = useChatModels({ isConnected: isGatewayRunning })
|
||||
const canSend = isChatConnected && Boolean(defaultModelName)
|
||||
const hasDefaultModel = Boolean(defaultModelName)
|
||||
const inputDisabledReason = resolveChatInputDisabledReason({
|
||||
hasDefaultModel,
|
||||
connectionState,
|
||||
gatewayState: gwState,
|
||||
})
|
||||
const canInput = inputDisabledReason === null
|
||||
|
||||
const {
|
||||
sessions,
|
||||
@@ -110,7 +172,7 @@ export function ChatPage() {
|
||||
}, [messages, isTyping, isAtBottom])
|
||||
|
||||
const handleSend = () => {
|
||||
if ((!input.trim() && attachments.length === 0) || !canSend) return
|
||||
if ((!input.trim() && attachments.length === 0) || !canInput) return
|
||||
if (
|
||||
sendMessage({
|
||||
content: input,
|
||||
@@ -123,7 +185,7 @@ export function ChatPage() {
|
||||
}
|
||||
|
||||
const handleAddImages = () => {
|
||||
if (!canSend) return
|
||||
if (!canInput) return
|
||||
fileInputRef.current?.click()
|
||||
}
|
||||
|
||||
@@ -180,7 +242,8 @@ export function ChatPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const canSubmit = canSend && (Boolean(input.trim()) || attachments.length > 0)
|
||||
const canSubmit =
|
||||
canInput && (Boolean(input.trim()) || attachments.length > 0)
|
||||
|
||||
return (
|
||||
<div className="bg-background/95 flex h-full flex-col">
|
||||
@@ -278,8 +341,7 @@ export function ChatPage() {
|
||||
onAddImages={handleAddImages}
|
||||
onRemoveAttachment={handleRemoveAttachment}
|
||||
onSend={handleSend}
|
||||
isConnected={isChatConnected}
|
||||
hasDefaultModel={Boolean(defaultModelName)}
|
||||
inputDisabledReason={inputDisabledReason}
|
||||
canSend={canSubmit}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -39,6 +39,18 @@
|
||||
"welcome": "How can I help you today?",
|
||||
"welcomeDesc": "Ask me about weather, settings, or any other tasks. I'm here to assist you.",
|
||||
"placeholder": "Start a new message...\nPress Enter to send, Shift + Enter for a new line",
|
||||
"disabledPlaceholder": {
|
||||
"gatewayUnknown": "Unable to chat: Gateway status is still being checked. Please wait, then refresh the page or restart Launcher if needed.",
|
||||
"gatewayStarting": "Unable to chat: Gateway is starting. Wait for startup to complete, then try again.",
|
||||
"gatewayRestarting": "Unable to chat: Gateway is restarting. Please wait for restart to finish.",
|
||||
"gatewayStopping": "Unable to chat: Gateway is stopping. Wait for it to stop, then start Gateway again.",
|
||||
"gatewayStopped": "Unable to chat: Gateway is not started. Click Start Gateway in the top bar, then retry.",
|
||||
"gatewayError": "Unable to chat: Gateway is in an error state. Check logs, then restart Gateway or Launcher.",
|
||||
"websocketConnecting": "Connecting to chat service... Please wait.",
|
||||
"websocketDisconnected": "Unable to chat: WebSocket connection is disconnected. Check network and gateway status, then refresh the page or restart Launcher.",
|
||||
"websocketError": "Unable to chat: WebSocket connection failed. Check network and gateway status, then retry.",
|
||||
"noDefaultModel": "Unable to chat: No default model is selected. Set a default model on the Models page."
|
||||
},
|
||||
"newChat": "New Chat",
|
||||
"notConnected": "Gateway is not running. Start it to chat.",
|
||||
"thinking": {
|
||||
|
||||
@@ -39,6 +39,18 @@
|
||||
"welcome": "今天我能为您做些什么?",
|
||||
"welcomeDesc": "您可以询问我天气、设置或其他任何任务,我随时为您效劳。",
|
||||
"placeholder": "输入新消息...\n按 Enter 发送,Shift + Enter 换行",
|
||||
"disabledPlaceholder": {
|
||||
"gatewayUnknown": "无法对话:网关状态仍在检测中。请稍候重试,如仍无效请刷新页面或重启 Launcher。",
|
||||
"gatewayStarting": "无法对话:网关正在启动。请等待启动完成后重试。",
|
||||
"gatewayRestarting": "无法对话:网关正在重启。请等待重启完成。",
|
||||
"gatewayStopping": "无法对话:网关正在停止。请等待停止完成后重新启动服务。",
|
||||
"gatewayStopped": "无法对话:网关服务未启动。请点击顶部栏的“启动服务”后重试。",
|
||||
"gatewayError": "无法对话:网关处于错误状态。请检查日志后重启网关或 Launcher。",
|
||||
"websocketConnecting": "正在连接聊天服务,请稍候。",
|
||||
"websocketDisconnected": "无法对话:WebSocket 连接已断开。请检查网络与服务状态,然后刷新页面或重启 Launcher。",
|
||||
"websocketError": "无法对话:WebSocket 连接失败。请检查网络与服务状态后重试。",
|
||||
"noDefaultModel": "无法对话:尚未设置默认模型。请前往模型页面设置默认模型。"
|
||||
},
|
||||
"newChat": "新建对话",
|
||||
"notConnected": "服务未运行,请先启动以进行对话。",
|
||||
"thinking": {
|
||||
|
||||
Reference in New Issue
Block a user