fix(web): refactor pico chat flow and fix proxied websocket URLs (#1639)

- move chat controller, state, protocol, history, and websocket logic into a dedicated chat feature module
- improve chat reconnection, session hydration, and send gating based on actual websocket state
- preserve gateway status during transient SSE disconnects and update stop state immediately
- generate wss websocket URLs behind HTTPS proxies and add backend tests for forwarded proto handling
This commit is contained in:
wenjie
2026-03-16 16:25:16 +08:00
committed by GitHub
parent 0c94e6f7b3
commit c513ad22d7
16 changed files with 509 additions and 215 deletions
@@ -42,7 +42,7 @@ export function ChatComposer({
placeholder={t("chat.placeholder")}
disabled={!canInput}
className={cn(
"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 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",
)}
minRows={1}
@@ -56,7 +56,7 @@ export function ChatComposer({
size="icon"
className="size-8 rounded-full bg-violet-500 text-white transition-transform hover:bg-violet-600 active:scale-95"
onClick={onSend}
disabled={!input.trim() || !isConnected}
disabled={!input.trim() || !canInput}
>
<IconArrowUp className="size-4" />
</Button>
@@ -34,7 +34,7 @@ export function ChatEmptyState({
<p className="text-muted-foreground mb-4 text-center text-sm">
{t("chat.empty.noConfiguredModelDescription")}
</p>
<Button asChild variant="secondary" size="sm" className="px-4">
<Button asChild variant="outline" size="sm" className="px-4">
<Link to="/models">{t("chat.empty.goToModels")}</Link>
</Button>
</div>
+11 -12
View File
@@ -15,7 +15,6 @@ 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 { hydrateActiveSession } from "@/lib/pico-chat-controller"
export function ChatPage() {
const { t } = useTranslation()
@@ -26,6 +25,7 @@ export function ChatPage() {
const {
messages,
connectionState,
isTyping,
activeSessionId,
sendMessage,
@@ -34,7 +34,8 @@ export function ChatPage() {
} = usePicoChat()
const { state: gwState } = useGateway()
const isConnected = gwState === "running"
const isGatewayRunning = gwState === "running"
const isChatConnected = connectionState === "connected"
const {
defaultModelName,
@@ -43,7 +44,8 @@ export function ChatPage() {
oauthModels,
localModels,
handleSetDefault,
} = useChatModels({ isConnected })
} = useChatModels({ isConnected: isGatewayRunning })
const canSend = isChatConnected && Boolean(defaultModelName)
const {
sessions,
@@ -68,10 +70,6 @@ export function ChatPage() {
syncScrollState(e.currentTarget)
}
useEffect(() => {
void hydrateActiveSession()
}, [])
useEffect(() => {
if (scrollRef.current) {
if (isAtBottom) {
@@ -82,9 +80,10 @@ export function ChatPage() {
}, [messages, isTyping, isAtBottom])
const handleSend = () => {
if (!input.trim() || !isConnected) return
sendMessage(input.trim())
setInput("")
if (!input.trim() || !canSend) return
if (sendMessage(input.trim())) {
setInput("")
}
}
return (
@@ -143,7 +142,7 @@ export function ChatPage() {
<ChatEmptyState
hasConfiguredModels={hasConfiguredModels}
defaultModelName={defaultModelName}
isConnected={isConnected}
isConnected={isGatewayRunning}
/>
)}
@@ -168,7 +167,7 @@ export function ChatPage() {
input={input}
onInputChange={setInput}
onSend={handleSend}
isConnected={isConnected}
isConnected={isChatConnected}
hasDefaultModel={Boolean(defaultModelName)}
/>
</div>