feat(web): 通道/会话切换移至设置页 ConnectionTab,修复通道失效回退;连接状态上移 Logo 行并支持窄侧栏 compact 降级
This commit is contained in:
parent
eaee29841d
commit
f705536fb1
@ -28,8 +28,6 @@ import {
|
||||
} from './components/Settings/SettingsModal';
|
||||
import { ConfigPage } from './components/Settings/ConfigPage';
|
||||
import { ConnectionStatus } from './components/ConnectionStatus';
|
||||
import { ChannelSelector } from './components/Header/ChannelSelector';
|
||||
import { SessionSelector } from './components/Header/SessionSelector';
|
||||
import { getVersion } from './api/config';
|
||||
import { useWebSocket } from './hooks/useWebSocket';
|
||||
import { useChat } from './hooks/useChat';
|
||||
@ -719,6 +717,8 @@ function App() {
|
||||
v{appVersion}
|
||||
</span>
|
||||
)}
|
||||
{/* 窄侧栏时降级为 compact 图标,避免 Logo 行溢出裁剪折叠按钮 */}
|
||||
<ConnectionStatus status={status} compact={sidebarWidth < 264} />
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg text-[var(--text-muted)] hover:bg-[var(--overlay-hover)] hover:text-[var(--text-primary)] transition-colors"
|
||||
@ -788,22 +788,6 @@ function App() {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="shrink-0 px-3 pb-2">
|
||||
<ConnectionStatus status={status} />
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-col gap-1.5 px-3 pb-2 text-sm text-[var(--text-secondary)]">
|
||||
<ChannelSelector
|
||||
channels={channels}
|
||||
selectedChannel={selectedChannel}
|
||||
onSelectChannel={handleSwitchChannel}
|
||||
/>
|
||||
<SessionSelector
|
||||
sessions={sessions}
|
||||
selectedSessionId={selectedSessionId}
|
||||
onSelectSession={handleSelectSession}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tab 栏 */}
|
||||
<div className="flex shrink-0 border-b border-[var(--border-color)] px-3">
|
||||
<button
|
||||
@ -1148,6 +1132,12 @@ function App() {
|
||||
}}
|
||||
onSaveConnection={handleSaveConnection}
|
||||
initialTab={configInitialTab}
|
||||
channels={channels}
|
||||
selectedChannel={selectedChannel}
|
||||
onSelectChannel={handleSwitchChannel}
|
||||
sessions={sessions}
|
||||
selectedSessionId={selectedSessionId}
|
||||
onSelectSession={handleSelectSession}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -42,7 +42,17 @@ function TabLoading() {
|
||||
);
|
||||
}
|
||||
|
||||
export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPageProps) {
|
||||
export function ConfigPage({
|
||||
onClose,
|
||||
onSaveConnection,
|
||||
initialTab,
|
||||
channels,
|
||||
selectedChannel,
|
||||
onSelectChannel,
|
||||
sessions,
|
||||
selectedSessionId,
|
||||
onSelectSession,
|
||||
}: ConfigPageProps) {
|
||||
const [config, setConfig] = useState<AppConfig | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<TabId>(initialTab ?? 'providers');
|
||||
const [loading, setLoading] = useState(true);
|
||||
@ -192,7 +202,18 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
const commonProps = { config, update };
|
||||
switch (activeTab) {
|
||||
case 'connection':
|
||||
return <ConnectionTab onSaveConnection={onSaveConnection} setToast={showToast} />;
|
||||
return (
|
||||
<ConnectionTab
|
||||
onSaveConnection={onSaveConnection}
|
||||
setToast={showToast}
|
||||
channels={channels}
|
||||
selectedChannel={selectedChannel}
|
||||
onSelectChannel={onSelectChannel}
|
||||
sessions={sessions}
|
||||
selectedSessionId={selectedSessionId}
|
||||
onSelectSession={onSelectSession}
|
||||
/>
|
||||
);
|
||||
case 'gateway':
|
||||
return <GatewayTab {...commonProps} />;
|
||||
case 'providers':
|
||||
|
||||
@ -1,17 +1,34 @@
|
||||
// ConnectionTab - WebSocket 连接设置(localStorage 持久化,不走后端 config)
|
||||
// 同时承载通道/会话切换(立即生效,无需保存)
|
||||
import { useState } from 'react';
|
||||
import { Wifi, KeyRound } from 'lucide-react';
|
||||
import { Field, SectionCard } from '../ui';
|
||||
import { inputCls } from '../constants';
|
||||
import { inputCls, selectCls } from '../constants';
|
||||
import { ErrorBanner } from '../shared';
|
||||
import { getAuthToken, setAuthToken } from '../../../api/client';
|
||||
import type { Channel, SessionSummary } from '../../../types/protocol';
|
||||
|
||||
interface ConnectionTabProps {
|
||||
onSaveConnection?: (host: string, port: number) => void;
|
||||
setToast: (msg: string) => void;
|
||||
channels?: Channel[];
|
||||
selectedChannel?: string | null;
|
||||
onSelectChannel?: (id: string) => void;
|
||||
sessions?: SessionSummary[];
|
||||
selectedSessionId?: string | null;
|
||||
onSelectSession?: (id: string) => void;
|
||||
}
|
||||
|
||||
export function ConnectionTab({ onSaveConnection, setToast }: ConnectionTabProps) {
|
||||
export function ConnectionTab({
|
||||
onSaveConnection,
|
||||
setToast,
|
||||
channels,
|
||||
selectedChannel,
|
||||
onSelectChannel,
|
||||
sessions,
|
||||
selectedSessionId,
|
||||
onSelectSession,
|
||||
}: ConnectionTabProps) {
|
||||
const [connHost, setConnHost] = useState(() => {
|
||||
try {
|
||||
return localStorage.getItem('picobot-gateway-host') || '127.0.0.1';
|
||||
@ -55,6 +72,48 @@ export function ConnectionTab({ onSaveConnection, setToast }: ConnectionTabProps
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<SectionCard title="通道与会话">
|
||||
<Field label="当前通道">
|
||||
<select
|
||||
value={selectedChannel ?? ''}
|
||||
onChange={(e) => onSelectChannel?.(e.target.value)}
|
||||
disabled={!channels || channels.length === 0}
|
||||
className={selectCls}
|
||||
>
|
||||
{channels && channels.length > 0 ? (
|
||||
channels.map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.name}
|
||||
{c.isWritable ? '' : '(只读)'}
|
||||
</option>
|
||||
))
|
||||
) : (
|
||||
<option value="">暂无可用通道</option>
|
||||
)}
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="当前会话">
|
||||
<select
|
||||
value={selectedSessionId ?? ''}
|
||||
onChange={(e) => onSelectSession?.(e.target.value)}
|
||||
disabled={!sessions || sessions.length <= 1}
|
||||
className={selectCls}
|
||||
>
|
||||
{sessions && sessions.length > 0 ? (
|
||||
sessions.map((s) => (
|
||||
<option key={s.session_id} value={s.session_id}>
|
||||
{s.title}
|
||||
</option>
|
||||
))
|
||||
) : (
|
||||
<option value="">暂无会话</option>
|
||||
)}
|
||||
</select>
|
||||
</Field>
|
||||
<div className="text-xs text-[var(--text-muted)] bg-[var(--overlay-dim)] rounded-lg px-3 py-2">
|
||||
通道与会话切换立即生效,无需保存;通道决定当前视图的消息来源与读写权限。
|
||||
</div>
|
||||
</SectionCard>
|
||||
<SectionCard title="WebSocket 连接">
|
||||
<Field label="主机地址">
|
||||
<input
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
// Config-related type definitions extracted from ConfigPage.tsx
|
||||
|
||||
import type { Channel, SessionSummary } from '../../types/protocol';
|
||||
|
||||
export interface ProviderConfig {
|
||||
type: string;
|
||||
base_url: string;
|
||||
@ -282,6 +284,12 @@ export interface ConfigPageProps {
|
||||
onClose: () => void;
|
||||
onSaveConnection?: (host: string, port: number) => void;
|
||||
initialTab?: TabId;
|
||||
channels?: Channel[];
|
||||
selectedChannel?: string | null;
|
||||
onSelectChannel?: (id: string) => void;
|
||||
sessions?: SessionSummary[];
|
||||
selectedSessionId?: string | null;
|
||||
onSelectSession?: (id: string) => void;
|
||||
}
|
||||
|
||||
export interface KnownSource {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, type Dispatch, type SetStateAction } from 'react';
|
||||
import { useCallback, useMemo, useRef, type Dispatch, type SetStateAction } from 'react';
|
||||
import type {
|
||||
Command,
|
||||
ChatMessage,
|
||||
@ -166,6 +166,10 @@ export function useChat(): UseChatReturn {
|
||||
});
|
||||
const scheduler = useSchedulerView();
|
||||
|
||||
// selectedChannel 的 ref 镜像:handleServerMessage 空依赖闭包中读取当前值,
|
||||
// 用于 channel_list 到达时校正失效的选中通道(如默认 'websocket' 在后端不存在)
|
||||
const selectedChannelRef = useRef(sideData.selectedChannel);
|
||||
|
||||
// ---- handleServerMessage: 纯路由分发(Tier 1 → Tier 2 → Tier 3) ----
|
||||
// 所有被调用的方法都是稳定引用(useState setter 或 useCallback([])),
|
||||
// 因此空依赖数组安全,不会产生过期闭包。
|
||||
@ -252,9 +256,20 @@ export function useChat(): UseChatReturn {
|
||||
sideData.setTodos(message.todos);
|
||||
return;
|
||||
|
||||
case 'channel_list':
|
||||
case 'channel_list': {
|
||||
sideData.setChannels(message.channels);
|
||||
// 当前选中通道不在列表中(默认 'websocket' 在后端不存在、或通道被移除)时,
|
||||
// 回退到第一个通道,避免 ConnectionTab 通道下拉空白及 session 请求携带无效通道
|
||||
if (
|
||||
message.channels.length > 0 &&
|
||||
!message.channels.some((c) => c.id === selectedChannelRef.current)
|
||||
) {
|
||||
const fallback = message.channels[0].id;
|
||||
selectedChannelRef.current = fallback;
|
||||
sideData.setSelectedChannel(fallback);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
case 'pong':
|
||||
return;
|
||||
@ -300,6 +315,7 @@ export function useChat(): UseChatReturn {
|
||||
const selectChannel = useCallback(
|
||||
(channelId: string) => {
|
||||
if (channelId === sideData.selectedChannel) return;
|
||||
selectedChannelRef.current = channelId;
|
||||
sideData.setSelectedChannel(channelId);
|
||||
sessions.setSessions([]);
|
||||
sessions.setSelectedSessionId(null);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user