v0.1.2
parent
a2ac5c4fb5
commit
301a6a4a2d
|
|
@ -29,6 +29,20 @@ function normalizeChannelName(raw: unknown): string {
|
|||
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() {
|
||||
const { activeBots, setBots, updateBotState, addBotLog, addBotMessage, addBotEvent, setBotMessages } = useAppStore();
|
||||
const socketsRef = useRef<Record<string, WebSocket>>({});
|
||||
|
|
@ -157,8 +171,8 @@ export function useBotsSync() {
|
|||
ts: Date.now(),
|
||||
channel: sourceChannel || undefined,
|
||||
});
|
||||
if (isDashboardChannel && fullMessage && (normalizedState === 'THINKING' || normalizedState === 'TOOL_CALL')) {
|
||||
const chatText = normalizedState === 'TOOL_CALL' ? `${isZh ? '工具调用' : 'Tool Call'}\n${fullMessage}` : fullMessage;
|
||||
if (isDashboardChannel && fullMessage && normalizedState === 'TOOL_CALL') {
|
||||
const chatText = `${isZh ? '工具调用' : 'Tool Call'}\n${fullMessage}`;
|
||||
const now = Date.now();
|
||||
const prev = lastProgressRef.current[bot.id];
|
||||
if (!prev || prev.text !== chatText || now - prev.ts > 1200) {
|
||||
|
|
@ -198,6 +212,10 @@ export function useBotsSync() {
|
|||
updateBotState(bot.id, state, fullProgress);
|
||||
addBotEvent(bot.id, { state, text: fullProgress || t.progress, ts: Date.now(), channel: sourceChannel || undefined });
|
||||
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 now = Date.now();
|
||||
const prev = lastProgressRef.current[bot.id];
|
||||
|
|
|
|||
|
|
@ -366,6 +366,29 @@
|
|||
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 {
|
||||
white-space: pre-wrap;
|
||||
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%));
|
||||
}
|
||||
|
||||
.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 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
|
@ -1074,24 +1083,42 @@
|
|||
font-weight: 700;
|
||||
}
|
||||
|
||||
.ops-runtime-action {
|
||||
.ops-runtime-action-inline {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.ops-runtime-action-text {
|
||||
display: block;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
line-height: 1.54;
|
||||
max-height: 82px;
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ops-runtime-action-text.expanded {
|
||||
max-height: 180px;
|
||||
.ops-runtime-expand-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: 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 {
|
||||
|
|
|
|||
|
|
@ -249,7 +249,8 @@ function decorateWorkspacePathsForMarkdown(text: string) {
|
|||
/\[(\/root\/\.nanobot\/workspace\/[^\]]+)\]\s*\n?\s*\((https:\/\/workspace\.local\/open\/[^)\s]+)\)/g,
|
||||
'[$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) => {
|
||||
const normalized = normalizeDashboardAttachmentPath(fullPath);
|
||||
if (!normalized) return fullPath;
|
||||
|
|
@ -391,7 +392,7 @@ export function BotDashboardModule({
|
|||
const [compactPanelTab, setCompactPanelTab] = useState<CompactPanelTab>('chat');
|
||||
const [isCompactMobile, setIsCompactMobile] = useState(false);
|
||||
const [expandedProgressByKey, setExpandedProgressByKey] = useState<Record<string, boolean>>({});
|
||||
const [runtimeActionExpanded, setRuntimeActionExpanded] = useState(false);
|
||||
const [showRuntimeActionModal, setShowRuntimeActionModal] = useState(false);
|
||||
const runtimeMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const openWorkspacePathFromChat = (path: string) => {
|
||||
const normalized = String(path || '').trim();
|
||||
|
|
@ -406,7 +407,7 @@ export function BotDashboardModule({
|
|||
const source = String(text || '');
|
||||
if (!source) return [source];
|
||||
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[] = [];
|
||||
let lastIndex = 0;
|
||||
let matchIndex = 0;
|
||||
|
|
@ -624,7 +625,7 @@ export function BotDashboardModule({
|
|||
const summary = String(runtimeActionSummary || '').trim();
|
||||
return Boolean(full && full !== '-' && summary && full !== summary);
|
||||
}, [runtimeAction, runtimeActionSummary]);
|
||||
const runtimeActionDisplay = runtimeActionExpanded || !runtimeActionHasMore ? runtimeAction : runtimeActionSummary;
|
||||
const runtimeActionDisplay = runtimeActionHasMore ? runtimeActionSummary : runtimeAction;
|
||||
|
||||
const shouldCollapseProgress = (text: string) => {
|
||||
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-meta">
|
||||
<span>{item.role === 'user' ? t.you : 'Nanobot'}</span>
|
||||
<div className="ops-chat-meta-right">
|
||||
<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 className={`ops-chat-text ${collapsible && !expanded ? 'is-collapsed' : ''}`}>
|
||||
{item.text ? (
|
||||
|
|
@ -668,19 +686,6 @@ export function BotDashboardModule({
|
|||
</ReactMarkdown>
|
||||
)
|
||||
) : 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 ? (
|
||||
<div className="ops-chat-attachments">
|
||||
{(item.attachments || []).map((rawPath) => {
|
||||
|
|
@ -752,7 +757,7 @@ export function BotDashboardModule({
|
|||
|
||||
useEffect(() => {
|
||||
setExpandedProgressByKey({});
|
||||
setRuntimeActionExpanded(false);
|
||||
setShowRuntimeActionModal(false);
|
||||
}, [selectedBotId]);
|
||||
|
||||
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.lastAction}</span>
|
||||
<div className="ops-runtime-action">
|
||||
<strong className={`ops-runtime-action-text ${runtimeActionExpanded ? 'expanded' : ''}`}>{runtimeActionDisplay}</strong>
|
||||
<div className="ops-runtime-action-inline">
|
||||
<strong className="ops-runtime-action-text">{runtimeActionDisplay}</strong>
|
||||
{runtimeActionHasMore ? (
|
||||
<button
|
||||
className="ops-chat-more-btn"
|
||||
onClick={() => setRuntimeActionExpanded((prev) => !prev)}
|
||||
className="ops-runtime-expand-btn"
|
||||
onClick={() => setShowRuntimeActionModal(true)}
|
||||
title={isZh ? '查看完整内容' : 'Show full content'}
|
||||
aria-label={isZh ? '查看完整内容' : 'Show full content'}
|
||||
>
|
||||
{runtimeActionExpanded ? (isZh ? '收起' : 'Less') : (isZh ? '更多' : 'More')}
|
||||
…
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
@ -2468,6 +2475,23 @@ export function BotDashboardModule({
|
|||
</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 && (
|
||||
<div className="modal-mask" onClick={() => setWorkspacePreview(null)}>
|
||||
<div className="modal-card modal-preview" onClick={(e) => e.stopPropagation()}>
|
||||
|
|
|
|||
Loading…
Reference in New Issue