v0.1.4-p2
parent
38bdcdfd63
commit
86370d5824
|
|
@ -1062,6 +1062,14 @@ def _probe_mcp_server(cfg: Dict[str, Any], bot_id: Optional[str] = None) -> Dict
|
|||
},
|
||||
}
|
||||
|
||||
def _with_body_preview(message: str, preview: Any) -> str:
|
||||
text = str(message or "").strip()
|
||||
body = " ".join(str(preview or "").strip().split())
|
||||
if not body:
|
||||
return text
|
||||
body = body[:240]
|
||||
return f"{text}: {body}" if text else body
|
||||
|
||||
if bot_id:
|
||||
if transport_type == "sse":
|
||||
probe_headers = dict(headers)
|
||||
|
|
@ -1077,16 +1085,17 @@ def _probe_mcp_server(cfg: Dict[str, Any], bot_id: Optional[str] = None) -> Dict
|
|||
status_code = probe.get("status_code")
|
||||
content_type = str(probe.get("content_type") or "")
|
||||
message = str(probe.get("message") or "").strip()
|
||||
body_preview = probe.get("body_preview")
|
||||
if status_code in {401, 403}:
|
||||
return {"ok": False, "transport": transport_type, "status_code": status_code, "message": "Auth failed for MCP SSE endpoint", "content_type": content_type, "probe_from": "bot-container"}
|
||||
if status_code == 404:
|
||||
return {"ok": False, "transport": transport_type, "status_code": status_code, "message": "MCP SSE endpoint not found", "content_type": content_type, "probe_from": "bot-container"}
|
||||
if isinstance(status_code, int) and status_code >= 500:
|
||||
return {"ok": False, "transport": transport_type, "status_code": status_code, "message": "MCP SSE endpoint server error", "content_type": content_type, "probe_from": "bot-container"}
|
||||
return {"ok": False, "transport": transport_type, "status_code": status_code, "message": _with_body_preview("MCP SSE endpoint server error", body_preview), "content_type": content_type, "probe_from": "bot-container"}
|
||||
if not probe.get("ok"):
|
||||
return {"ok": False, "transport": transport_type, "status_code": status_code, "message": message or "Failed to connect MCP SSE endpoint from bot container", "content_type": content_type, "probe_from": "bot-container"}
|
||||
return {"ok": False, "transport": transport_type, "status_code": status_code, "message": _with_body_preview(message or "Failed to connect MCP SSE endpoint from bot container", body_preview), "content_type": content_type, "probe_from": "bot-container"}
|
||||
if "text/event-stream" not in content_type.lower():
|
||||
return {"ok": False, "transport": transport_type, "status_code": status_code, "message": "Endpoint reachable, but content-type is not text/event-stream", "content_type": content_type, "probe_from": "bot-container"}
|
||||
return {"ok": False, "transport": transport_type, "status_code": status_code, "message": _with_body_preview("Endpoint reachable, but content-type is not text/event-stream", body_preview), "content_type": content_type, "probe_from": "bot-container"}
|
||||
return {"ok": True, "transport": transport_type, "status_code": status_code, "message": "MCP SSE endpoint is reachable", "content_type": content_type, "probe_from": "bot-container"}
|
||||
|
||||
probe_headers = dict(headers)
|
||||
|
|
@ -1102,16 +1111,17 @@ def _probe_mcp_server(cfg: Dict[str, Any], bot_id: Optional[str] = None) -> Dict
|
|||
)
|
||||
status_code = probe.get("status_code")
|
||||
message = str(probe.get("message") or "").strip()
|
||||
body_preview = probe.get("body_preview")
|
||||
if status_code in {401, 403}:
|
||||
return {"ok": False, "transport": transport_type, "status_code": status_code, "message": "Auth failed for MCP endpoint", "probe_from": "bot-container"}
|
||||
if status_code == 404:
|
||||
return {"ok": False, "transport": transport_type, "status_code": status_code, "message": "MCP endpoint not found", "probe_from": "bot-container"}
|
||||
if isinstance(status_code, int) and status_code >= 500:
|
||||
return {"ok": False, "transport": transport_type, "status_code": status_code, "message": "MCP endpoint server error", "probe_from": "bot-container"}
|
||||
return {"ok": False, "transport": transport_type, "status_code": status_code, "message": _with_body_preview("MCP endpoint server error", body_preview), "probe_from": "bot-container"}
|
||||
if probe.get("ok") and status_code in {200, 201, 202, 204, 400, 405, 415, 422}:
|
||||
reachability_msg = "MCP endpoint is reachable" if status_code in {200, 201, 202, 204} else "MCP endpoint is reachable (request format not fully accepted by probe)"
|
||||
return {"ok": True, "transport": transport_type, "status_code": status_code, "message": reachability_msg, "probe_from": "bot-container"}
|
||||
return {"ok": False, "transport": transport_type, "status_code": status_code, "message": message or "Unexpected response from MCP endpoint", "probe_from": "bot-container"}
|
||||
return {"ok": False, "transport": transport_type, "status_code": status_code, "message": _with_body_preview(message or "Unexpected response from MCP endpoint", body_preview), "probe_from": "bot-container"}
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=httpx.Timeout(timeout_s), follow_redirects=True) as client:
|
||||
|
|
@ -1120,6 +1130,7 @@ def _probe_mcp_server(cfg: Dict[str, Any], bot_id: Optional[str] = None) -> Dict
|
|||
req_headers.setdefault("Accept", "text/event-stream")
|
||||
resp = client.get(url, headers=req_headers)
|
||||
content_type = str(resp.headers.get("content-type") or "")
|
||||
body_preview = resp.text[:512]
|
||||
if resp.status_code in {401, 403}:
|
||||
return {
|
||||
"ok": False,
|
||||
|
|
@ -1143,7 +1154,7 @@ def _probe_mcp_server(cfg: Dict[str, Any], bot_id: Optional[str] = None) -> Dict
|
|||
"ok": False,
|
||||
"transport": transport_type,
|
||||
"status_code": resp.status_code,
|
||||
"message": "MCP SSE endpoint server error",
|
||||
"message": _with_body_preview("MCP SSE endpoint server error", body_preview),
|
||||
"content_type": content_type,
|
||||
"probe_from": "backend-host",
|
||||
}
|
||||
|
|
@ -1152,7 +1163,7 @@ def _probe_mcp_server(cfg: Dict[str, Any], bot_id: Optional[str] = None) -> Dict
|
|||
"ok": False,
|
||||
"transport": transport_type,
|
||||
"status_code": resp.status_code,
|
||||
"message": "Endpoint reachable, but content-type is not text/event-stream",
|
||||
"message": _with_body_preview("Endpoint reachable, but content-type is not text/event-stream", body_preview),
|
||||
"content_type": content_type,
|
||||
"probe_from": "backend-host",
|
||||
}
|
||||
|
|
@ -1169,6 +1180,7 @@ def _probe_mcp_server(cfg: Dict[str, Any], bot_id: Optional[str] = None) -> Dict
|
|||
req_headers.setdefault("Content-Type", "application/json")
|
||||
req_headers.setdefault("Accept", "application/json, text/event-stream")
|
||||
resp = client.post(url, headers=req_headers, json=probe_payload)
|
||||
body_preview = resp.text[:512]
|
||||
if resp.status_code in {401, 403}:
|
||||
return {
|
||||
"ok": False,
|
||||
|
|
@ -1190,7 +1202,7 @@ def _probe_mcp_server(cfg: Dict[str, Any], bot_id: Optional[str] = None) -> Dict
|
|||
"ok": False,
|
||||
"transport": transport_type,
|
||||
"status_code": resp.status_code,
|
||||
"message": "MCP endpoint server error",
|
||||
"message": _with_body_preview("MCP endpoint server error", body_preview),
|
||||
"probe_from": "backend-host",
|
||||
}
|
||||
if resp.status_code in {200, 201, 202, 204}:
|
||||
|
|
@ -1213,7 +1225,7 @@ def _probe_mcp_server(cfg: Dict[str, Any], bot_id: Optional[str] = None) -> Dict
|
|||
"ok": False,
|
||||
"transport": transport_type,
|
||||
"status_code": resp.status_code,
|
||||
"message": "Unexpected response from MCP endpoint",
|
||||
"message": _with_body_preview("Unexpected response from MCP endpoint", body_preview),
|
||||
"probe_from": "backend-host",
|
||||
}
|
||||
except httpx.TimeoutException:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from typing import Any, Dict, Optional
|
|||
|
||||
from sqlmodel import Session
|
||||
|
||||
from services.topic_service import _topic_publish_internal
|
||||
from services.topic_service import _has_topic_mcp_server, _topic_publish_internal
|
||||
|
||||
from .publisher import build_topic_publish_payload
|
||||
|
||||
|
|
@ -19,6 +19,8 @@ def publish_runtime_topic_packet(
|
|||
packet_type = str(packet.get("type") or "").strip().upper()
|
||||
if packet_type not in {"ASSISTANT_MESSAGE", "BUS_EVENT"} or not persisted_message_id:
|
||||
return
|
||||
if not _has_topic_mcp_server(bot_id):
|
||||
return
|
||||
|
||||
topic_payload = build_topic_publish_payload(
|
||||
bot_id,
|
||||
|
|
|
|||
|
|
@ -151,6 +151,18 @@ def _resolve_topic_mcp_bot_id_by_token(session: Session, token: str) -> Optional
|
|||
return None
|
||||
|
||||
|
||||
def _has_topic_mcp_server(bot_id: str) -> bool:
|
||||
config_data = _read_bot_config(bot_id)
|
||||
tools_cfg = config_data.get("tools")
|
||||
if not isinstance(tools_cfg, dict):
|
||||
return False
|
||||
mcp_servers = tools_cfg.get("mcpServers")
|
||||
if not isinstance(mcp_servers, dict):
|
||||
return False
|
||||
token = _extract_topic_mcp_token(mcp_servers.get(TOPIC_MCP_SERVER_NAME))
|
||||
return bool(token)
|
||||
|
||||
|
||||
def _normalize_topic_key(raw: Any) -> str:
|
||||
value = str(raw or "").strip().lower()
|
||||
if not value:
|
||||
|
|
|
|||
|
|
@ -332,7 +332,8 @@ body {
|
|||
}
|
||||
|
||||
.grid-ops.grid-ops-compact {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(280px, 420px);
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.stack {
|
||||
|
|
@ -1197,12 +1198,12 @@ body {
|
|||
@media (max-width: 980px) {
|
||||
.grid-ops.grid-ops-compact {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.app-shell-compact .grid-ops.grid-ops-compact {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: minmax(0, 1fr) auto;
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ function AuthenticatedApp({
|
|||
const [singleBotSubmitting, setSingleBotSubmitting] = useState(false);
|
||||
useBotsSync(forcedBotId);
|
||||
const t = pickLocale(locale, { 'zh-cn': appZhCn, en: appEn });
|
||||
const isSingleBotCompactView = compactMode && Boolean(String(forcedBotId || '').trim());
|
||||
const [headerCollapsed, setHeaderCollapsed] = useState(isSingleBotCompactView);
|
||||
const isCompactShell = compactMode;
|
||||
const [headerCollapsed, setHeaderCollapsed] = useState(isCompactShell);
|
||||
const forced = String(forcedBotId || '').trim();
|
||||
const forcedBot = forced ? activeBots[forced] : undefined;
|
||||
const shouldPromptSingleBotPassword = Boolean(forced && forcedBot?.has_access_password && !singleBotUnlocked);
|
||||
|
|
@ -51,8 +51,8 @@ function AuthenticatedApp({
|
|||
}, [forced, forcedBot?.name, t.title]);
|
||||
|
||||
useEffect(() => {
|
||||
setHeaderCollapsed(isSingleBotCompactView);
|
||||
}, [isSingleBotCompactView, forcedBotId]);
|
||||
setHeaderCollapsed(isCompactShell);
|
||||
}, [isCompactShell, forcedBotId]);
|
||||
|
||||
useEffect(() => {
|
||||
setSingleBotUnlocked(false);
|
||||
|
|
@ -114,9 +114,9 @@ function AuthenticatedApp({
|
|||
<div className={`app-shell ${compactMode ? 'app-shell-compact' : ''}`} data-theme={theme}>
|
||||
<div className="app-frame">
|
||||
<header
|
||||
className={`app-header ${isSingleBotCompactView ? 'app-header-collapsible' : ''} ${isSingleBotCompactView && headerCollapsed ? 'is-collapsed' : ''}`}
|
||||
className={`app-header ${isCompactShell ? 'app-header-collapsible' : ''} ${isCompactShell && headerCollapsed ? 'is-collapsed' : ''}`}
|
||||
onClick={() => {
|
||||
if (isSingleBotCompactView && headerCollapsed) setHeaderCollapsed(false);
|
||||
if (isCompactShell && headerCollapsed) setHeaderCollapsed(false);
|
||||
}}
|
||||
>
|
||||
<div className="row-between app-header-top">
|
||||
|
|
@ -124,7 +124,7 @@ function AuthenticatedApp({
|
|||
<img src="/app-bot-icon.svg" alt="Nanobot" className="app-title-icon" />
|
||||
<div className="app-title-main">
|
||||
<h1>{t.title}</h1>
|
||||
{isSingleBotCompactView ? (
|
||||
{isCompactShell ? (
|
||||
<button
|
||||
type="button"
|
||||
className="app-header-toggle-inline"
|
||||
|
|
@ -305,6 +305,19 @@ function PanelLoginGate({
|
|||
const compactMode = compactByFlag || forcedBotId.length > 0;
|
||||
return { forcedBotId, compactMode };
|
||||
}, []);
|
||||
const [viewportCompact, setViewportCompact] = useState(() => {
|
||||
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;
|
||||
return window.matchMedia('(max-width: 980px)').matches;
|
||||
});
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
|
||||
const media = window.matchMedia('(max-width: 980px)');
|
||||
const apply = () => setViewportCompact(media.matches);
|
||||
apply();
|
||||
media.addEventListener('change', apply);
|
||||
return () => media.removeEventListener('change', apply);
|
||||
}, []);
|
||||
const compactMode = urlView.compactMode || viewportCompact;
|
||||
|
||||
const [checking, setChecking] = useState(true);
|
||||
const [required, setRequired] = useState(false);
|
||||
|
|
@ -427,7 +440,7 @@ function PanelLoginGate({
|
|||
);
|
||||
}
|
||||
|
||||
return children(urlView);
|
||||
return children({ forcedBotId: urlView.forcedBotId || undefined, compactMode });
|
||||
}
|
||||
|
||||
function App() {
|
||||
|
|
|
|||
|
|
@ -13,14 +13,30 @@
|
|||
max-height: 72vh;
|
||||
}
|
||||
|
||||
.app-shell-compact .ops-bot-list {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.app-shell-compact .ops-bot-list .list-scroll {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.ops-compact-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ops-compact-fab-switch {
|
||||
.ops-compact-fab-stack {
|
||||
position: fixed;
|
||||
right: 14px;
|
||||
bottom: 14px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
z-index: 85;
|
||||
}
|
||||
|
||||
.ops-compact-fab-switch {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 999px;
|
||||
|
|
@ -33,7 +49,6 @@
|
|||
box-shadow:
|
||||
0 10px 24px rgba(9, 15, 28, 0.42),
|
||||
0 0 0 2px color-mix(in oklab, var(--brand) 22%, transparent);
|
||||
z-index: 85;
|
||||
cursor: pointer;
|
||||
overflow: visible;
|
||||
transform: translateY(0);
|
||||
|
|
@ -423,6 +438,46 @@
|
|||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.ops-compact-bot-surface {
|
||||
position: fixed;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
top: 58px;
|
||||
bottom: 12px;
|
||||
z-index: 72;
|
||||
animation: ops-compact-sheet-in 220ms ease;
|
||||
}
|
||||
|
||||
.ops-compact-close-btn {
|
||||
position: fixed;
|
||||
top: 18px;
|
||||
right: 14px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid color-mix(in oklab, var(--brand) 58%, var(--line) 42%);
|
||||
background: color-mix(in oklab, var(--panel) 78%, var(--brand-soft) 22%);
|
||||
color: var(--icon);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 90;
|
||||
box-shadow:
|
||||
0 8px 20px rgba(9, 15, 28, 0.38),
|
||||
0 0 0 2px color-mix(in oklab, var(--brand) 22%, transparent);
|
||||
}
|
||||
|
||||
@keyframes ops-compact-sheet-in {
|
||||
from {
|
||||
transform: translateY(22px);
|
||||
opacity: 0.82;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.ops-main-content-shell {
|
||||
display: block;
|
||||
min-height: 0;
|
||||
|
|
|
|||
|
|
@ -1402,6 +1402,10 @@ export function BotDashboardModule({
|
|||
}),
|
||||
[activeBots],
|
||||
);
|
||||
const hasForcedBot = Boolean(String(forcedBotId || '').trim());
|
||||
const compactListFirstMode = compactMode && !hasForcedBot;
|
||||
const isCompactListPage = compactListFirstMode && !selectedBotId;
|
||||
const showCompactBotPageClose = compactListFirstMode && Boolean(selectedBotId);
|
||||
const normalizedBotListQuery = botListQuery.trim().toLowerCase();
|
||||
const filteredBots = useMemo(() => {
|
||||
if (!normalizedBotListQuery) return bots;
|
||||
|
|
@ -1858,9 +1862,15 @@ export function BotDashboardModule({
|
|||
}
|
||||
return;
|
||||
}
|
||||
if (compactListFirstMode) {
|
||||
if (selectedBotId && !activeBots[selectedBotId]) {
|
||||
setSelectedBotId('');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!selectedBotId && bots.length > 0) setSelectedBotId(bots[0].id);
|
||||
if (selectedBotId && !activeBots[selectedBotId] && bots.length > 0) setSelectedBotId(bots[0].id);
|
||||
}, [bots, selectedBotId, activeBots, forcedBotId]);
|
||||
}, [bots, selectedBotId, activeBots, forcedBotId, compactListFirstMode]);
|
||||
|
||||
useEffect(() => {
|
||||
setComposerDraftHydrated(false);
|
||||
|
|
@ -4361,7 +4371,7 @@ export function BotDashboardModule({
|
|||
return (
|
||||
<>
|
||||
<div className={`grid-ops ${compactMode ? 'grid-ops-compact' : ''}`}>
|
||||
{!compactMode ? (
|
||||
{!compactMode || isCompactListPage ? (
|
||||
<section className="panel stack ops-bot-list">
|
||||
<div className="row-between">
|
||||
<h2 style={{ fontSize: 18 }}>
|
||||
|
|
@ -4481,7 +4491,14 @@ export function BotDashboardModule({
|
|||
const isStarting = controlState === 'starting';
|
||||
const isStopping = controlState === 'stopping';
|
||||
return (
|
||||
<div key={bot.id} className={`ops-bot-card ${selected ? 'is-active' : ''}`} onClick={() => setSelectedBotId(bot.id)}>
|
||||
<div
|
||||
key={bot.id}
|
||||
className={`ops-bot-card ${selected ? 'is-active' : ''}`}
|
||||
onClick={() => {
|
||||
setSelectedBotId(bot.id);
|
||||
if (compactMode) setCompactPanelTab('chat');
|
||||
}}
|
||||
>
|
||||
<span className={`ops-bot-strip ${bot.docker_status === 'RUNNING' ? 'is-running' : 'is-stopped'}`} aria-hidden="true" />
|
||||
<div className="row-between ops-bot-top">
|
||||
<div className="ops-bot-name-wrap">
|
||||
|
|
@ -4610,7 +4627,7 @@ export function BotDashboardModule({
|
|||
</section>
|
||||
) : null}
|
||||
|
||||
<section className={`panel ops-chat-panel ${compactMode && isCompactMobile && compactPanelTab !== 'chat' ? 'ops-compact-hidden' : ''}`}>
|
||||
<section className={`panel ops-chat-panel ${compactMode && (isCompactListPage || compactPanelTab !== 'chat') ? 'ops-compact-hidden' : ''} ${showCompactBotPageClose ? 'ops-compact-bot-surface' : ''}`}>
|
||||
{selectedBot ? (
|
||||
<div className="ops-chat-shell">
|
||||
<div className="ops-main-content-shell">
|
||||
|
|
@ -4906,7 +4923,7 @@ export function BotDashboardModule({
|
|||
)}
|
||||
</section>
|
||||
|
||||
<section className={`panel stack ops-runtime-panel ${compactMode && isCompactMobile && compactPanelTab !== 'runtime' ? 'ops-compact-hidden' : ''}`}>
|
||||
<section className={`panel stack ops-runtime-panel ${compactMode && (isCompactListPage || compactPanelTab !== 'runtime') ? 'ops-compact-hidden' : ''} ${showCompactBotPageClose ? 'ops-compact-bot-surface' : ''}`}>
|
||||
{selectedBot ? (
|
||||
<div className="ops-runtime-shell">
|
||||
<div className="row-between ops-runtime-head">
|
||||
|
|
@ -5186,16 +5203,31 @@ export function BotDashboardModule({
|
|||
)}
|
||||
</section>
|
||||
</div>
|
||||
{compactMode && isCompactMobile ? (
|
||||
{showCompactBotPageClose ? (
|
||||
<LucentIconButton
|
||||
className={`ops-compact-fab-switch ${compactPanelTab === 'chat' ? 'is-chat' : 'is-runtime'}`}
|
||||
onClick={() => setCompactPanelTab((v) => (v === 'chat' ? 'runtime' : 'chat'))}
|
||||
tooltip={compactPanelTab === 'chat' ? (isZh ? '切换到运行面板' : 'Switch to runtime') : (isZh ? '切换到对话面板' : 'Switch to chat')}
|
||||
aria-label={compactPanelTab === 'chat' ? (isZh ? '切换到运行面板' : 'Switch to runtime') : (isZh ? '切换到对话面板' : 'Switch to chat')}
|
||||
className="ops-compact-close-btn"
|
||||
onClick={() => {
|
||||
setSelectedBotId('');
|
||||
setCompactPanelTab('chat');
|
||||
}}
|
||||
tooltip={isZh ? '关闭并返回 Bot 列表' : 'Close and back to bot list'}
|
||||
aria-label={isZh ? '关闭并返回 Bot 列表' : 'Close and back to bot list'}
|
||||
>
|
||||
{compactPanelTab === 'chat' ? <Activity size={18} /> : <MessageSquareText size={18} />}
|
||||
<X size={16} />
|
||||
</LucentIconButton>
|
||||
) : null}
|
||||
{compactMode && !isCompactListPage ? (
|
||||
<div className="ops-compact-fab-stack">
|
||||
<LucentIconButton
|
||||
className={`ops-compact-fab-switch ${compactPanelTab === 'chat' ? 'is-chat' : 'is-runtime'}`}
|
||||
onClick={() => setCompactPanelTab((v) => (v === 'runtime' ? 'chat' : 'runtime'))}
|
||||
tooltip={compactPanelTab === 'runtime' ? (isZh ? '切换到对话面板' : 'Switch to chat') : (isZh ? '切换到运行面板' : 'Switch to runtime')}
|
||||
aria-label={compactPanelTab === 'runtime' ? (isZh ? '切换到对话面板' : 'Switch to chat') : (isZh ? '切换到运行面板' : 'Switch to runtime')}
|
||||
>
|
||||
{compactPanelTab === 'runtime' ? <MessageSquareText size={18} /> : <Activity size={18} />}
|
||||
</LucentIconButton>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showResourceModal && (
|
||||
<div className="modal-mask" onClick={() => setShowResourceModal(false)}>
|
||||
|
|
|
|||
Loading…
Reference in New Issue