v0.1.2
parent
a2ac5c4fb5
commit
301a6a4a2d
|
|
@ -29,6 +29,20 @@ function normalizeChannelName(raw: unknown): string {
|
||||||
return channel;
|
return channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isLikelyEchoOfUserInput(progressText: string, userText: string): boolean {
|
||||||
|
const progress = normalizeAssistantMessageText(progressText).replace(/\s+/g, ' ').trim().toLowerCase();
|
||||||
|
const user = normalizeUserMessageText(userText).replace(/\s+/g, ' ').trim().toLowerCase();
|
||||||
|
if (!progress || !user) return false;
|
||||||
|
if (progress === user) return true;
|
||||||
|
if (user.length < 8) return false;
|
||||||
|
const hasProcessingPrefix =
|
||||||
|
/processing message|message from|received message|收到消息|处理消息|用户输入|command/i.test(progress);
|
||||||
|
if (progress.includes(user) && (hasProcessingPrefix || progress.length <= user.length + 40)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export function useBotsSync() {
|
export function useBotsSync() {
|
||||||
const { activeBots, setBots, updateBotState, addBotLog, addBotMessage, addBotEvent, setBotMessages } = useAppStore();
|
const { activeBots, setBots, updateBotState, addBotLog, addBotMessage, addBotEvent, setBotMessages } = useAppStore();
|
||||||
const socketsRef = useRef<Record<string, WebSocket>>({});
|
const socketsRef = useRef<Record<string, WebSocket>>({});
|
||||||
|
|
@ -157,8 +171,8 @@ export function useBotsSync() {
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
channel: sourceChannel || undefined,
|
channel: sourceChannel || undefined,
|
||||||
});
|
});
|
||||||
if (isDashboardChannel && fullMessage && (normalizedState === 'THINKING' || normalizedState === 'TOOL_CALL')) {
|
if (isDashboardChannel && fullMessage && normalizedState === 'TOOL_CALL') {
|
||||||
const chatText = normalizedState === 'TOOL_CALL' ? `${isZh ? '工具调用' : 'Tool Call'}\n${fullMessage}` : fullMessage;
|
const chatText = `${isZh ? '工具调用' : 'Tool Call'}\n${fullMessage}`;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const prev = lastProgressRef.current[bot.id];
|
const prev = lastProgressRef.current[bot.id];
|
||||||
if (!prev || prev.text !== chatText || now - prev.ts > 1200) {
|
if (!prev || prev.text !== chatText || now - prev.ts > 1200) {
|
||||||
|
|
@ -198,6 +212,10 @@ export function useBotsSync() {
|
||||||
updateBotState(bot.id, state, fullProgress);
|
updateBotState(bot.id, state, fullProgress);
|
||||||
addBotEvent(bot.id, { state, text: fullProgress || t.progress, ts: Date.now(), channel: sourceChannel || undefined });
|
addBotEvent(bot.id, { state, text: fullProgress || t.progress, ts: Date.now(), channel: sourceChannel || undefined });
|
||||||
if (isDashboardChannel) {
|
if (isDashboardChannel) {
|
||||||
|
const lastUserText = lastUserEchoRef.current[bot.id]?.text || '';
|
||||||
|
if (!isTool && isLikelyEchoOfUserInput(fullProgress, lastUserText)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const chatText = isTool ? `${isZh ? '工具调用' : 'Tool Call'}\n${fullProgress}` : fullProgress;
|
const chatText = isTool ? `${isZh ? '工具调用' : 'Tool Call'}\n${fullProgress}` : fullProgress;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const prev = lastProgressRef.current[bot.id];
|
const prev = lastProgressRef.current[bot.id];
|
||||||
|
|
|
||||||
|
|
@ -366,6 +366,29 @@
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ops-chat-meta-right {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-chat-expand-icon-btn {
|
||||||
|
border: 1px solid color-mix(in oklab, var(--brand) 56%, var(--line) 44%);
|
||||||
|
background: color-mix(in oklab, var(--panel) 76%, var(--brand-soft) 24%);
|
||||||
|
color: var(--text);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.ops-chat-text {
|
.ops-chat-text {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
|
@ -392,20 +415,6 @@
|
||||||
background: linear-gradient(to bottom, transparent, color-mix(in oklab, var(--panel-soft) 88%, var(--panel) 12%));
|
background: linear-gradient(to bottom, transparent, color-mix(in oklab, var(--panel-soft) 88%, var(--panel) 12%));
|
||||||
}
|
}
|
||||||
|
|
||||||
.ops-chat-more-btn {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
margin-top: 8px;
|
|
||||||
border: 1px solid color-mix(in oklab, var(--brand) 56%, var(--line) 44%);
|
|
||||||
background: color-mix(in oklab, var(--panel) 72%, var(--brand-soft) 28%);
|
|
||||||
color: var(--text);
|
|
||||||
border-radius: 999px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
padding: 4px 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ops-chat-text > *:first-child {
|
.ops-chat-text > *:first-child {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -1074,24 +1083,42 @@
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ops-runtime-action {
|
.ops-runtime-action-inline {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: grid;
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ops-runtime-action-text {
|
.ops-runtime-action-text {
|
||||||
display: block;
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
line-height: 1.54;
|
line-height: 1.54;
|
||||||
max-height: 82px;
|
overflow: hidden;
|
||||||
overflow: auto;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ops-runtime-action-text.expanded {
|
.ops-runtime-expand-btn {
|
||||||
max-height: 180px;
|
border: 1px solid color-mix(in oklab, var(--brand) 56%, var(--line) 44%);
|
||||||
|
background: color-mix(in oklab, var(--panel) 76%, var(--brand-soft) 24%);
|
||||||
|
color: var(--text);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ops-preview {
|
.ops-preview {
|
||||||
|
|
|
||||||
|
|
@ -249,7 +249,8 @@ function decorateWorkspacePathsForMarkdown(text: string) {
|
||||||
/\[(\/root\/\.nanobot\/workspace\/[^\]]+)\]\s*\n?\s*\((https:\/\/workspace\.local\/open\/[^)\s]+)\)/g,
|
/\[(\/root\/\.nanobot\/workspace\/[^\]]+)\]\s*\n?\s*\((https:\/\/workspace\.local\/open\/[^)\s]+)\)/g,
|
||||||
'[$1]($2)',
|
'[$1]($2)',
|
||||||
);
|
);
|
||||||
const workspacePathPattern = /\/root\/\.nanobot\/workspace\/[^\s<>"'`)\]},。!?;:、]+/g;
|
const workspacePathPattern =
|
||||||
|
/\/root\/\.nanobot\/workspace\/[^\n\r<>"'`]+?\.(?:md|json|log|txt|csv|pdf|png|jpg|jpeg|webp)\b/gi;
|
||||||
return normalizedExistingLinks.replace(workspacePathPattern, (fullPath) => {
|
return normalizedExistingLinks.replace(workspacePathPattern, (fullPath) => {
|
||||||
const normalized = normalizeDashboardAttachmentPath(fullPath);
|
const normalized = normalizeDashboardAttachmentPath(fullPath);
|
||||||
if (!normalized) return fullPath;
|
if (!normalized) return fullPath;
|
||||||
|
|
@ -391,7 +392,7 @@ export function BotDashboardModule({
|
||||||
const [compactPanelTab, setCompactPanelTab] = useState<CompactPanelTab>('chat');
|
const [compactPanelTab, setCompactPanelTab] = useState<CompactPanelTab>('chat');
|
||||||
const [isCompactMobile, setIsCompactMobile] = useState(false);
|
const [isCompactMobile, setIsCompactMobile] = useState(false);
|
||||||
const [expandedProgressByKey, setExpandedProgressByKey] = useState<Record<string, boolean>>({});
|
const [expandedProgressByKey, setExpandedProgressByKey] = useState<Record<string, boolean>>({});
|
||||||
const [runtimeActionExpanded, setRuntimeActionExpanded] = useState(false);
|
const [showRuntimeActionModal, setShowRuntimeActionModal] = useState(false);
|
||||||
const runtimeMenuRef = useRef<HTMLDivElement | null>(null);
|
const runtimeMenuRef = useRef<HTMLDivElement | null>(null);
|
||||||
const openWorkspacePathFromChat = (path: string) => {
|
const openWorkspacePathFromChat = (path: string) => {
|
||||||
const normalized = String(path || '').trim();
|
const normalized = String(path || '').trim();
|
||||||
|
|
@ -406,7 +407,7 @@ export function BotDashboardModule({
|
||||||
const source = String(text || '');
|
const source = String(text || '');
|
||||||
if (!source) return [source];
|
if (!source) return [source];
|
||||||
const pattern =
|
const pattern =
|
||||||
/\[(\/root\/\.nanobot\/workspace\/[^\]]+)\]\((https:\/\/workspace\.local\/open\/[^)\s]+)\)|\/root\/\.nanobot\/workspace\/[^\s<>"'`)\]},。!?;:、]+|https:\/\/workspace\.local\/open\/[^\s)]+/g;
|
/\[(\/root\/\.nanobot\/workspace\/[^\]]+?\.(?:md|json|log|txt|csv|pdf|png|jpg|jpeg|webp))\]\((https:\/\/workspace\.local\/open\/[^)\s]+)\)|\/root\/\.nanobot\/workspace\/[^\n\r<>"'`]+?\.(?:md|json|log|txt|csv|pdf|png|jpg|jpeg|webp)\b|https:\/\/workspace\.local\/open\/[^\s)]+/gi;
|
||||||
const nodes: ReactNode[] = [];
|
const nodes: ReactNode[] = [];
|
||||||
let lastIndex = 0;
|
let lastIndex = 0;
|
||||||
let matchIndex = 0;
|
let matchIndex = 0;
|
||||||
|
|
@ -624,7 +625,7 @@ export function BotDashboardModule({
|
||||||
const summary = String(runtimeActionSummary || '').trim();
|
const summary = String(runtimeActionSummary || '').trim();
|
||||||
return Boolean(full && full !== '-' && summary && full !== summary);
|
return Boolean(full && full !== '-' && summary && full !== summary);
|
||||||
}, [runtimeAction, runtimeActionSummary]);
|
}, [runtimeAction, runtimeActionSummary]);
|
||||||
const runtimeActionDisplay = runtimeActionExpanded || !runtimeActionHasMore ? runtimeAction : runtimeActionSummary;
|
const runtimeActionDisplay = runtimeActionHasMore ? runtimeActionSummary : runtimeAction;
|
||||||
|
|
||||||
const shouldCollapseProgress = (text: string) => {
|
const shouldCollapseProgress = (text: string) => {
|
||||||
const normalized = String(text || '').trim();
|
const normalized = String(text || '').trim();
|
||||||
|
|
@ -656,7 +657,24 @@ export function BotDashboardModule({
|
||||||
<div className={`ops-chat-bubble ${item.role === 'user' ? 'user' : 'assistant'} ${(item.kind || 'final') === 'progress' ? 'progress' : ''}`}>
|
<div className={`ops-chat-bubble ${item.role === 'user' ? 'user' : 'assistant'} ${(item.kind || 'final') === 'progress' ? 'progress' : ''}`}>
|
||||||
<div className="ops-chat-meta">
|
<div className="ops-chat-meta">
|
||||||
<span>{item.role === 'user' ? t.you : 'Nanobot'}</span>
|
<span>{item.role === 'user' ? t.you : 'Nanobot'}</span>
|
||||||
|
<div className="ops-chat-meta-right">
|
||||||
<span className="mono">{formatClock(item.ts)}</span>
|
<span className="mono">{formatClock(item.ts)}</span>
|
||||||
|
{collapsible ? (
|
||||||
|
<button
|
||||||
|
className="ops-chat-expand-icon-btn"
|
||||||
|
onClick={() =>
|
||||||
|
setExpandedProgressByKey((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[itemKey]: !prev[itemKey],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
title={expanded ? (isZh ? '收起' : 'Collapse') : (isZh ? '展开' : 'Expand')}
|
||||||
|
aria-label={expanded ? (isZh ? '收起' : 'Collapse') : (isZh ? '展开' : 'Expand')}
|
||||||
|
>
|
||||||
|
{expanded ? '×' : '…'}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`ops-chat-text ${collapsible && !expanded ? 'is-collapsed' : ''}`}>
|
<div className={`ops-chat-text ${collapsible && !expanded ? 'is-collapsed' : ''}`}>
|
||||||
{item.text ? (
|
{item.text ? (
|
||||||
|
|
@ -668,19 +686,6 @@ export function BotDashboardModule({
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
{collapsible ? (
|
|
||||||
<button
|
|
||||||
className="ops-chat-more-btn"
|
|
||||||
onClick={() =>
|
|
||||||
setExpandedProgressByKey((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[itemKey]: !prev[itemKey],
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{expanded ? (isZh ? '收起' : 'Less') : (isZh ? '更多' : 'More')}
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
{(item.attachments || []).length > 0 ? (
|
{(item.attachments || []).length > 0 ? (
|
||||||
<div className="ops-chat-attachments">
|
<div className="ops-chat-attachments">
|
||||||
{(item.attachments || []).map((rawPath) => {
|
{(item.attachments || []).map((rawPath) => {
|
||||||
|
|
@ -752,7 +757,7 @@ export function BotDashboardModule({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setExpandedProgressByKey({});
|
setExpandedProgressByKey({});
|
||||||
setRuntimeActionExpanded(false);
|
setShowRuntimeActionModal(false);
|
||||||
}, [selectedBotId]);
|
}, [selectedBotId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -1951,14 +1956,16 @@ export function BotDashboardModule({
|
||||||
<div className="ops-runtime-row"><span>{t.current}</span><strong className="mono">{displayState}</strong></div>
|
<div className="ops-runtime-row"><span>{t.current}</span><strong className="mono">{displayState}</strong></div>
|
||||||
<div className="ops-runtime-row">
|
<div className="ops-runtime-row">
|
||||||
<span>{t.lastAction}</span>
|
<span>{t.lastAction}</span>
|
||||||
<div className="ops-runtime-action">
|
<div className="ops-runtime-action-inline">
|
||||||
<strong className={`ops-runtime-action-text ${runtimeActionExpanded ? 'expanded' : ''}`}>{runtimeActionDisplay}</strong>
|
<strong className="ops-runtime-action-text">{runtimeActionDisplay}</strong>
|
||||||
{runtimeActionHasMore ? (
|
{runtimeActionHasMore ? (
|
||||||
<button
|
<button
|
||||||
className="ops-chat-more-btn"
|
className="ops-runtime-expand-btn"
|
||||||
onClick={() => setRuntimeActionExpanded((prev) => !prev)}
|
onClick={() => setShowRuntimeActionModal(true)}
|
||||||
|
title={isZh ? '查看完整内容' : 'Show full content'}
|
||||||
|
aria-label={isZh ? '查看完整内容' : 'Show full content'}
|
||||||
>
|
>
|
||||||
{runtimeActionExpanded ? (isZh ? '收起' : 'Less') : (isZh ? '更多' : 'More')}
|
…
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2468,6 +2475,23 @@ export function BotDashboardModule({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showRuntimeActionModal && (
|
||||||
|
<div className="modal-mask" onClick={() => setShowRuntimeActionModal(false)}>
|
||||||
|
<div className="modal-card modal-preview" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div className="modal-title-row">
|
||||||
|
<h3>{t.lastAction}</h3>
|
||||||
|
</div>
|
||||||
|
<div className="workspace-preview-body">
|
||||||
|
<pre>{runtimeAction}</pre>
|
||||||
|
</div>
|
||||||
|
<div className="row-between">
|
||||||
|
<span />
|
||||||
|
<button className="btn btn-primary" onClick={() => setShowRuntimeActionModal(false)}>{t.close}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{workspacePreview && (
|
{workspacePreview && (
|
||||||
<div className="modal-mask" onClick={() => setWorkspacePreview(null)}>
|
<div className="modal-mask" onClick={() => setWorkspacePreview(null)}>
|
||||||
<div className="modal-card modal-preview" onClick={(e) => e.stopPropagation()}>
|
<div className="modal-card modal-preview" onClick={(e) => e.stopPropagation()}>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue