@sleep2agi/agent-network-dashboard 0.6.3-preview.16 → 0.6.3-preview.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +3 -3
- package/.next/diagnostics/route-bundle-stats.json +10 -10
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/prerender-manifest.json +3 -3
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +2 -2
- package/.next/server/app/_not-found.rsc +2 -2
- package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
- package/.next/server/app/admin.html +2 -2
- package/.next/server/app/admin.rsc +2 -2
- package/.next/server/app/admin.segments/_full.segment.rsc +2 -2
- package/.next/server/app/admin.segments/_head.segment.rsc +1 -1
- package/.next/server/app/admin.segments/_index.segment.rsc +2 -2
- package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/admin.segments/admin.segment.rsc +1 -1
- package/.next/server/app/index.html +2 -2
- package/.next/server/app/index.rsc +3 -3
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login.html +2 -2
- package/.next/server/app/login.rsc +3 -3
- package/.next/server/app/login.segments/_full.segment.rsc +3 -3
- package/.next/server/app/login.segments/_head.segment.rsc +1 -1
- package/.next/server/app/login.segments/_index.segment.rsc +2 -2
- package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/login.segments/login.segment.rsc +1 -1
- package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/logs.html +2 -2
- package/.next/server/app/logs.rsc +2 -2
- package/.next/server/app/logs.segments/_full.segment.rsc +2 -2
- package/.next/server/app/logs.segments/_head.segment.rsc +1 -1
- package/.next/server/app/logs.segments/_index.segment.rsc +2 -2
- package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/logs.segments/logs.segment.rsc +1 -1
- package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
- package/.next/server/app/messages.html +2 -2
- package/.next/server/app/messages.rsc +2 -2
- package/.next/server/app/messages.segments/_full.segment.rsc +2 -2
- package/.next/server/app/messages.segments/_head.segment.rsc +1 -1
- package/.next/server/app/messages.segments/_index.segment.rsc +2 -2
- package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/messages.segments/messages.segment.rsc +1 -1
- package/.next/server/app/node/page_client-reference-manifest.js +1 -1
- package/.next/server/app/node.html +2 -2
- package/.next/server/app/node.rsc +3 -3
- package/.next/server/app/node.segments/_full.segment.rsc +3 -3
- package/.next/server/app/node.segments/_head.segment.rsc +1 -1
- package/.next/server/app/node.segments/_index.segment.rsc +2 -2
- package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/node.segments/node.segment.rsc +1 -1
- package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
- package/.next/server/app/nodes.html +2 -2
- package/.next/server/app/nodes.rsc +3 -3
- package/.next/server/app/nodes.segments/_full.segment.rsc +3 -3
- package/.next/server/app/nodes.segments/_head.segment.rsc +1 -1
- package/.next/server/app/nodes.segments/_index.segment.rsc +2 -2
- package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/nodes.segments/nodes.segment.rsc +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/providers/page_client-reference-manifest.js +1 -1
- package/.next/server/app/providers.html +2 -2
- package/.next/server/app/providers.rsc +2 -2
- package/.next/server/app/providers.segments/_full.segment.rsc +2 -2
- package/.next/server/app/providers.segments/_head.segment.rsc +1 -1
- package/.next/server/app/providers.segments/_index.segment.rsc +2 -2
- package/.next/server/app/providers.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/providers.segments/providers/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/providers.segments/providers.segment.rsc +1 -1
- package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/server-logs.html +2 -2
- package/.next/server/app/server-logs.rsc +2 -2
- package/.next/server/app/server-logs.segments/_full.segment.rsc +2 -2
- package/.next/server/app/server-logs.segments/_head.segment.rsc +1 -1
- package/.next/server/app/server-logs.segments/_index.segment.rsc +2 -2
- package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/server-logs.segments/server-logs.segment.rsc +1 -1
- package/.next/server/app/servers/page_client-reference-manifest.js +1 -1
- package/.next/server/app/servers.html +2 -2
- package/.next/server/app/servers.rsc +2 -2
- package/.next/server/app/servers.segments/_full.segment.rsc +2 -2
- package/.next/server/app/servers.segments/_head.segment.rsc +1 -1
- package/.next/server/app/servers.segments/_index.segment.rsc +2 -2
- package/.next/server/app/servers.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/servers.segments/servers/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/servers.segments/servers.segment.rsc +1 -1
- package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/networks.html +2 -2
- package/.next/server/app/settings/networks.rsc +2 -2
- package/.next/server/app/settings/networks.segments/_full.segment.rsc +2 -2
- package/.next/server/app/settings/networks.segments/_head.segment.rsc +1 -1
- package/.next/server/app/settings/networks.segments/_index.segment.rsc +2 -2
- package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +1 -1
- package/.next/server/app/settings/networks.segments/settings.segment.rsc +1 -1
- package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens.html +2 -2
- package/.next/server/app/settings/tokens.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/_full.segment.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/_head.segment.rsc +1 -1
- package/.next/server/app/settings/tokens.segments/_index.segment.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +1 -1
- package/.next/server/app/settings/tokens.segments/settings.segment.rsc +1 -1
- package/.next/server/app/settings.html +2 -2
- package/.next/server/app/settings.rsc +3 -3
- package/.next/server/app/settings.segments/_full.segment.rsc +3 -3
- package/.next/server/app/settings.segments/_head.segment.rsc +1 -1
- package/.next/server/app/settings.segments/_index.segment.rsc +2 -2
- package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/settings.segments/settings.segment.rsc +1 -1
- package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
- package/.next/server/app/tasks.html +2 -2
- package/.next/server/app/tasks.rsc +2 -2
- package/.next/server/app/tasks.segments/_full.segment.rsc +2 -2
- package/.next/server/app/tasks.segments/_head.segment.rsc +1 -1
- package/.next/server/app/tasks.segments/_index.segment.rsc +2 -2
- package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/tasks.segments/tasks.segment.rsc +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__091su60._.js +2 -2
- package/.next/server/chunks/ssr/[root-of-the-server]__091su60._.js.map +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0vc.3k7._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0vc.3k7._.js.map +1 -1
- package/.next/server/chunks/ssr/dash-loop-wt_app_0~.1~ew._.js +1 -1
- package/.next/server/chunks/ssr/dash-loop-wt_app_0~.1~ew._.js.map +1 -1
- package/.next/server/chunks/ssr/dash-loop-wt_app_13k3-ai._.js +1 -1
- package/.next/server/chunks/ssr/dash-loop-wt_app_13k3-ai._.js.map +1 -1
- package/.next/server/chunks/ssr/dash-loop-wt_app_nodes_page_tsx_0ma3oqt._.js +1 -1
- package/.next/server/chunks/ssr/dash-loop-wt_app_nodes_page_tsx_0ma3oqt._.js.map +1 -1
- package/.next/server/middleware-build-manifest.js +3 -3
- package/.next/server/pages/404.html +2 -2
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/0ibr--rd~v6.l.css +1 -0
- package/.next/static/chunks/{08cv_lh8tw-p7.js → 0ih12lups7yq-.js} +1 -1
- package/.next/static/chunks/{0p6os-912pt-t.js → 0rgwdv8ad23sz.js} +1 -1
- package/.next/static/chunks/{0w4ia6l0l2nx6.js → 0sy9m1ltl8su5.js} +1 -1
- package/.next/static/chunks/{0fow_ssfwva9g.js → 0wg01-6hp5w31.js} +2 -2
- package/.next/static/chunks/{06qwjph2.xkng.js → 18314oga3ygn-.js} +2 -2
- package/.next/trace +2 -2
- package/.next/trace-build +1 -1
- package/app/components/TaskChatPanel.tsx +220 -27
- package/app/nodes/page.tsx +10 -0
- package/package.json +1 -1
- package/.next/static/chunks/0xz6zlg032d_4.css +0 -1
- /package/.next/static/{waj5SPgHKhI524_18FcZK → TZI9jxdtoDCWPz_ajnyf9}/_buildManifest.js +0 -0
- /package/.next/static/{waj5SPgHKhI524_18FcZK → TZI9jxdtoDCWPz_ajnyf9}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{waj5SPgHKhI524_18FcZK → TZI9jxdtoDCWPz_ajnyf9}/_ssgManifest.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useRef, useCallback, Fragment } from 'react';
|
|
3
|
+
import { useState, useEffect, useRef, useCallback, useMemo, memo, Fragment } from 'react';
|
|
4
4
|
import { AliasAvatar } from './AliasAvatar';
|
|
5
5
|
|
|
6
6
|
interface ChatTask {
|
|
@@ -58,20 +58,123 @@ function extractAttachmentPreviews(text: string): string[] {
|
|
|
58
58
|
return Array.from(new Set(urls));
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
/* Loop R12 (微信式图片): thumbnails shimmer while loading and degrade to a
|
|
62
|
+
quiet "图片加载失败" tile on error; tapping opens an in-panel lightbox
|
|
63
|
+
(dark backdrop, fit-to-screen, Esc/tap closes) instead of navigating to
|
|
64
|
+
a raw browser tab. */
|
|
65
|
+
function ChatImage({ src, alt, thumbClass, onOpen }: { src: string; alt?: string; thumbClass: string; onOpen: (url: string) => void }) {
|
|
66
|
+
const [state, setState] = useState<'loading' | 'ready' | 'error'>('loading');
|
|
67
|
+
if (state === 'error') {
|
|
68
|
+
return (
|
|
69
|
+
<span className={`${thumbClass} flex items-center justify-center bg-black/20 border border-[var(--border)] text-[10px] text-[var(--fg-dim)]`}>
|
|
70
|
+
图片加载失败
|
|
71
|
+
</span>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return (
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
onClick={() => onOpen(src)}
|
|
78
|
+
aria-label={alt || '查看大图'}
|
|
79
|
+
className={`relative block overflow-hidden ${state === 'loading' ? 'animate-pulse bg-[var(--bg-elevated)]' : ''} ${thumbClass}`}
|
|
80
|
+
>
|
|
81
|
+
<img
|
|
82
|
+
src={src}
|
|
83
|
+
alt={alt || ''}
|
|
84
|
+
loading="lazy"
|
|
85
|
+
onLoad={() => setState('ready')}
|
|
86
|
+
onError={() => setState('error')}
|
|
87
|
+
className={`h-full w-full object-cover transition-opacity duration-200 ${state === 'ready' ? 'opacity-100' : 'opacity-0'}`}
|
|
88
|
+
/>
|
|
89
|
+
</button>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function ImageLightbox({ url, onClose, opener }: { url: string; onClose: () => void; opener: HTMLElement | null }) {
|
|
94
|
+
const [loaded, setLoaded] = useState(false);
|
|
95
|
+
const dialogRef = useRef<HTMLDivElement>(null);
|
|
96
|
+
// onClose is recreated on every parent render (and the panel re-renders
|
|
97
|
+
// every few seconds from soft refreshes) — a [onClose] dep would tear
|
|
98
|
+
// down/re-run this effect mid-open and re-capture the "opener" as an
|
|
99
|
+
// element INSIDE the dialog. Mount-only + ref keeps the true opener.
|
|
100
|
+
const onCloseRef = useRef(onClose);
|
|
101
|
+
onCloseRef.current = onClose;
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
const onClose = () => onCloseRef.current();
|
|
104
|
+
// R13 a11y debt: modal focus management — focus into the dialog, trap
|
|
105
|
+
// Tab, hand focus back on close. The opener is captured by the PANEL at
|
|
106
|
+
// activation time and passed in: by the time this passive effect runs,
|
|
107
|
+
// the thumbnail has already been blurred (measured: activeElement was
|
|
108
|
+
// <body> here), so capturing it locally restored nothing.
|
|
109
|
+
const focusables = () =>
|
|
110
|
+
Array.from(dialogRef.current?.querySelectorAll<HTMLElement>('a[href], button') || []);
|
|
111
|
+
focusables()[0]?.focus();
|
|
112
|
+
// Capture-phase so Escape closes the LIGHTBOX, not the chat panel
|
|
113
|
+
// behind it (panel hosts listen for Escape too).
|
|
114
|
+
const onKey = (e: KeyboardEvent) => {
|
|
115
|
+
if (e.key === 'Escape') { e.stopPropagation(); onClose(); return; }
|
|
116
|
+
if (e.key !== 'Tab') return;
|
|
117
|
+
const items = focusables();
|
|
118
|
+
if (items.length === 0) return;
|
|
119
|
+
const first = items[0], last = items[items.length - 1];
|
|
120
|
+
const active = document.activeElement;
|
|
121
|
+
if (e.shiftKey && (active === first || !dialogRef.current?.contains(active))) {
|
|
122
|
+
e.preventDefault(); last.focus();
|
|
123
|
+
} else if (!e.shiftKey && (active === last || !dialogRef.current?.contains(active))) {
|
|
124
|
+
e.preventDefault(); first.focus();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
window.addEventListener('keydown', onKey, true);
|
|
128
|
+
return () => {
|
|
129
|
+
window.removeEventListener('keydown', onKey, true);
|
|
130
|
+
// After this cleanup React removes the overlay; the browser then
|
|
131
|
+
// blurs to <body>. Restore on a macrotask so we run AFTER that blur.
|
|
132
|
+
setTimeout(() => {
|
|
133
|
+
// Soft refreshes rebuild bubble subtrees, so the opener node may be
|
|
134
|
+
// detached by now (focus() on a detached node is a silent no-op) —
|
|
135
|
+
// re-find its replacement by aria-label.
|
|
136
|
+
let el = opener;
|
|
137
|
+
if (el && !document.contains(el)) {
|
|
138
|
+
const label = el.getAttribute('aria-label');
|
|
139
|
+
if (label) el = document.querySelector<HTMLElement>(`[aria-label="${CSS.escape(label)}"]`);
|
|
140
|
+
}
|
|
141
|
+
el?.focus?.();
|
|
142
|
+
}, 0);
|
|
143
|
+
};
|
|
144
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
145
|
+
}, []); // opener/onCloseRef are stable for one open
|
|
146
|
+
return (
|
|
147
|
+
<div ref={dialogRef} className="fixed inset-0 z-[100] flex items-center justify-center bg-black/85" onClick={onClose} role="dialog" aria-modal="true" aria-label="图片预览">
|
|
148
|
+
{!loaded && <div className="absolute h-6 w-6 animate-spin rounded-full border-2 border-white/30 border-t-white" />}
|
|
149
|
+
<img
|
|
150
|
+
src={url}
|
|
151
|
+
alt=""
|
|
152
|
+
onLoad={() => setLoaded(true)}
|
|
153
|
+
className={`max-h-[92vh] max-w-[92vw] rounded object-contain transition-opacity duration-150 ${loaded ? 'opacity-100' : 'opacity-0'}`}
|
|
154
|
+
/>
|
|
155
|
+
<a
|
|
156
|
+
href={url}
|
|
157
|
+
target="_blank"
|
|
158
|
+
rel="noreferrer"
|
|
159
|
+
onClick={(e) => e.stopPropagation()}
|
|
160
|
+
className="absolute right-12 top-3 text-[11px] text-white/70 underline hover:text-white"
|
|
161
|
+
>
|
|
162
|
+
新标签打开
|
|
163
|
+
</a>
|
|
164
|
+
<button onClick={onClose} aria-label="关闭预览" className="absolute right-2 top-2 flex h-8 w-8 items-center justify-center text-lg text-white/80 hover:text-white">
|
|
165
|
+
×
|
|
166
|
+
</button>
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function AttachmentPreviews({ text, onOpen }: { text: string; onOpen: (url: string) => void }) {
|
|
62
172
|
const urls = extractAttachmentPreviews(text);
|
|
63
173
|
if (urls.length === 0) return null;
|
|
64
174
|
return (
|
|
65
175
|
<div className="mt-2 grid grid-cols-2 gap-2">
|
|
66
176
|
{urls.map((url) => (
|
|
67
|
-
<
|
|
68
|
-
<img
|
|
69
|
-
src={url}
|
|
70
|
-
alt="Dashboard attachment preview"
|
|
71
|
-
className="h-28 w-full object-cover transition-transform duration-200 group-hover:scale-[1.02]"
|
|
72
|
-
loading="lazy"
|
|
73
|
-
/>
|
|
74
|
-
</a>
|
|
177
|
+
<ChatImage key={url} src={url} alt="Dashboard attachment" thumbClass="h-28 w-full rounded-lg border border-cyan-500/20" onOpen={onOpen} />
|
|
75
178
|
))}
|
|
76
179
|
</div>
|
|
77
180
|
);
|
|
@@ -150,13 +253,16 @@ import { markChatRead } from '../lib/chat-unread';
|
|
|
150
253
|
import { readDraft, writeDraft } from '../lib/chat-drafts';
|
|
151
254
|
import { formatWeChatTime } from '../lib/time';
|
|
152
255
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
256
|
+
/* Loop R14 (气泡子树稳定性): the renderer map used to be an OBJECT LITERAL
|
|
257
|
+
OF INLINE ARROWS inside MarkdownContent's JSX — every render produced new
|
|
258
|
+
component types, so React unmounted and remounted the entire rendered
|
|
259
|
+
markdown subtree on every soft refresh (measured: +36 ChatImage mounts in
|
|
260
|
+
45s idle; any focused element inside a bubble fell back to <body>).
|
|
261
|
+
Hoisting the map to a factory + memoizing per onImageClick keeps types
|
|
262
|
+
stable; memo() on MarkdownContent then skips re-render entirely while
|
|
263
|
+
text is unchanged. */
|
|
264
|
+
function buildMdComponents(onImageClick?: (url: string) => void) {
|
|
265
|
+
return {
|
|
160
266
|
h1: ({ children }) => <h1 className="text-base font-semibold text-[var(--fg)] mt-2 mb-1">{children}</h1>,
|
|
161
267
|
h2: ({ children }) => <h2 className="text-sm font-semibold text-[var(--fg)] mt-2 mb-1">{children}</h2>,
|
|
162
268
|
h3: ({ children }) => <h3 className="text-sm font-semibold text-cyan-300 mt-1.5 mb-0.5">{children}</h3>,
|
|
@@ -169,6 +275,12 @@ function MarkdownContent({ text }: { text: string }) {
|
|
|
169
275
|
a: ({ href, children }) => <a href={href} target="_blank" rel="noreferrer" className="text-cyan-300 underline">{children}</a>,
|
|
170
276
|
blockquote: ({ children }) => <blockquote className="border-l-2 border-cyan-500/30 pl-3 my-1 text-[var(--fg-muted)] italic">{children}</blockquote>,
|
|
171
277
|
hr: () => <hr className="border-t border-[var(--border)] my-2" />,
|
|
278
|
+
img: ({ src, alt }) => {
|
|
279
|
+
const url = typeof src === 'string' ? src : '';
|
|
280
|
+
if (!url) return null;
|
|
281
|
+
if (!onImageClick) return <img src={url} alt={alt || ''} loading="lazy" className="my-1 max-h-48 rounded-lg" />;
|
|
282
|
+
return <ChatImage src={url} alt={alt} thumbClass="my-1 h-40 w-full max-w-[240px] rounded-lg" onOpen={onImageClick} />;
|
|
283
|
+
},
|
|
172
284
|
code: ({ className, children }) => {
|
|
173
285
|
const inline = !className?.startsWith('language-');
|
|
174
286
|
if (inline) {
|
|
@@ -192,13 +304,23 @@ function MarkdownContent({ text }: { text: string }) {
|
|
|
192
304
|
tr: ({ children }) => <tr className="border-b border-[var(--border)]">{children}</tr>,
|
|
193
305
|
th: ({ children }) => <th className="text-left text-cyan-300 font-semibold px-2 py-1 border border-[var(--border)]">{children}</th>,
|
|
194
306
|
td: ({ children }) => <td className="px-2 py-1 border border-[var(--border)] align-top">{children}</td>,
|
|
195
|
-
|
|
307
|
+
} satisfies React.ComponentProps<typeof ReactMarkdown>['components'];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const MarkdownContent = memo(function MarkdownContent({ text, onImageClick }: { text: string; onImageClick?: (url: string) => void }) {
|
|
311
|
+
const components = useMemo(() => buildMdComponents(onImageClick), [onImageClick]);
|
|
312
|
+
if (!text) return <span className="text-[var(--fg-dim)] italic">No content</span>;
|
|
313
|
+
return (
|
|
314
|
+
<div className="break-words leading-relaxed">
|
|
315
|
+
<ReactMarkdown
|
|
316
|
+
remarkPlugins={[remarkGfm]}
|
|
317
|
+
components={components}
|
|
196
318
|
>
|
|
197
319
|
{text}
|
|
198
320
|
</ReactMarkdown>
|
|
199
321
|
</div>
|
|
200
322
|
);
|
|
201
|
-
}
|
|
323
|
+
});
|
|
202
324
|
|
|
203
325
|
interface TaskChatPanelProps {
|
|
204
326
|
alias: string;
|
|
@@ -302,6 +424,18 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes, active }
|
|
|
302
424
|
// would always look "not at bottom" because the new content itself
|
|
303
425
|
// grew the distance).
|
|
304
426
|
const [newBelow, setNewBelow] = useState(0);
|
|
427
|
+
const [lightboxUrl, setLightboxUrl] = useState<string | null>(null);
|
|
428
|
+
// Loop R15 (会话内搜索): client-side filter over the LOADED window —
|
|
429
|
+
// honest scope (the hint says so); load earlier messages to widen it.
|
|
430
|
+
const [searchOpen, setSearchOpen] = useState(false);
|
|
431
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
432
|
+
const [searchIdx, setSearchIdx] = useState(0);
|
|
433
|
+
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
434
|
+
const lightboxOpenerRef = useRef<HTMLElement | null>(null);
|
|
435
|
+
const openLightbox = useCallback((url: string) => {
|
|
436
|
+
lightboxOpenerRef.current = document.activeElement as HTMLElement | null;
|
|
437
|
+
setLightboxUrl(url);
|
|
438
|
+
}, []);
|
|
305
439
|
const atBottomRef = useRef(true);
|
|
306
440
|
const prevBubblesRef = useRef(0);
|
|
307
441
|
const forceScrollRef = useRef(false);
|
|
@@ -812,6 +946,29 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes, active }
|
|
|
812
946
|
return a.kind === 'task' ? -1 : 1;
|
|
813
947
|
});
|
|
814
948
|
|
|
949
|
+
const query = searchQuery.trim().toLowerCase();
|
|
950
|
+
const searchMatches = query
|
|
951
|
+
? chatEvents
|
|
952
|
+
.filter((ev) => {
|
|
953
|
+
const text = ev.kind === 'reply' ? ev.task.result : ev.task.content;
|
|
954
|
+
return (text || '').toLowerCase().includes(query);
|
|
955
|
+
})
|
|
956
|
+
.map((ev) => `${ev.task.task_id}:${ev.kind}`)
|
|
957
|
+
: [];
|
|
958
|
+
const currentMatchKey = searchMatches.length ? searchMatches[Math.min(searchIdx, searchMatches.length - 1)] : null;
|
|
959
|
+
// New query starts at the NEWEST match (WeChat searches backwards in time).
|
|
960
|
+
useEffect(() => { setSearchIdx(Math.max(0, searchMatches.length - 1)); /* eslint-disable-line react-hooks/exhaustive-deps */ }, [query]);
|
|
961
|
+
useEffect(() => {
|
|
962
|
+
if (!currentMatchKey) return;
|
|
963
|
+
const el = scrollRef.current?.querySelector(`[data-ev="${CSS.escape(currentMatchKey)}"]`);
|
|
964
|
+
el?.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
965
|
+
}, [currentMatchKey]);
|
|
966
|
+
const stepMatch = (delta: number) => {
|
|
967
|
+
if (!searchMatches.length) return;
|
|
968
|
+
setSearchIdx((i) => (i + delta + searchMatches.length) % searchMatches.length);
|
|
969
|
+
};
|
|
970
|
+
const closeSearch = () => { setSearchOpen(false); setSearchQuery(''); };
|
|
971
|
+
|
|
815
972
|
// Inline mode: just the chat content, no panel chrome
|
|
816
973
|
const chatContent = (
|
|
817
974
|
<>
|
|
@@ -824,6 +981,41 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes, active }
|
|
|
824
981
|
to space-y-1.5 so messages read denser without losing the
|
|
825
982
|
speaker-turn rhythm. Desktop unchanged at sm: and up. */}
|
|
826
983
|
<div ref={scrollRef} onScroll={onMessagesScroll} className="flex-1 overflow-y-auto px-3 py-3 sm:px-4 sm:py-4 space-y-2 sm:space-y-4">
|
|
984
|
+
{searchOpen ? (
|
|
985
|
+
<div className="sticky top-0 z-20 -mx-1 flex items-center gap-2 rounded-lg border border-[var(--border)] bg-[var(--bg-elevated)]/95 px-2 py-1.5 shadow-lg shadow-black/30 backdrop-blur">
|
|
986
|
+
<input
|
|
987
|
+
ref={searchInputRef}
|
|
988
|
+
value={searchQuery}
|
|
989
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
990
|
+
onKeyDown={(e) => {
|
|
991
|
+
if (e.key === 'Escape') { e.stopPropagation(); closeSearch(); }
|
|
992
|
+
else if (e.key === 'Enter' && e.shiftKey) { e.preventDefault(); stepMatch(1); }
|
|
993
|
+
else if (e.key === 'Enter') { e.preventDefault(); stepMatch(-1); }
|
|
994
|
+
}}
|
|
995
|
+
placeholder="搜索会话…"
|
|
996
|
+
className="min-w-0 flex-1 bg-transparent text-xs text-[var(--fg)] placeholder-[var(--fg-dim)] focus:outline-none"
|
|
997
|
+
/>
|
|
998
|
+
<span className="shrink-0 text-[10px] tabular-nums text-[var(--fg-dim)]">
|
|
999
|
+
{searchMatches.length ? `${Math.min(searchIdx, searchMatches.length - 1) + 1}/${searchMatches.length}` : searchQuery.trim() ? '0 条' : `${chatEvents.length} 条内`}
|
|
1000
|
+
</span>
|
|
1001
|
+
<button type="button" onClick={() => stepMatch(-1)} aria-label="上一条(更早)" className="shrink-0 rounded px-1 text-[var(--fg-dim)] hover:text-cyan-300">↑</button>
|
|
1002
|
+
<button type="button" onClick={() => stepMatch(1)} aria-label="下一条(更新)" className="shrink-0 rounded px-1 text-[var(--fg-dim)] hover:text-cyan-300">↓</button>
|
|
1003
|
+
<button type="button" onClick={closeSearch} aria-label="关闭搜索" className="shrink-0 rounded px-1 text-[var(--fg-dim)] hover:text-[var(--fg)]">×</button>
|
|
1004
|
+
</div>
|
|
1005
|
+
) : (
|
|
1006
|
+
<div className="sticky top-0 z-20 flex justify-end pointer-events-none -mb-8">
|
|
1007
|
+
<button
|
|
1008
|
+
type="button"
|
|
1009
|
+
onClick={() => { setSearchOpen(true); setTimeout(() => searchInputRef.current?.focus(), 50); }}
|
|
1010
|
+
aria-label="搜索会话"
|
|
1011
|
+
className="pointer-events-auto rounded-full border border-[var(--border)] bg-[var(--bg-elevated)]/80 p-1.5 text-[var(--fg-dim)] shadow hover:text-cyan-300"
|
|
1012
|
+
>
|
|
1013
|
+
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
1014
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-4.35-4.35M17 11a6 6 0 11-12 0 6 6 0 0112 0z" />
|
|
1015
|
+
</svg>
|
|
1016
|
+
</button>
|
|
1017
|
+
</div>
|
|
1018
|
+
)}
|
|
827
1019
|
{!historyLoaded && (
|
|
828
1020
|
<div className="flex justify-center py-8">
|
|
829
1021
|
<div className="w-5 h-5 border-2 border-cyan-500/30 border-t-cyan-500 rounded-full animate-spin" />
|
|
@@ -896,8 +1088,8 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes, active }
|
|
|
896
1088
|
return (
|
|
897
1089
|
<Fragment key={`${m.task_id}:reply`}>
|
|
898
1090
|
{sepEl}
|
|
899
|
-
<div className="flex justify-start">
|
|
900
|
-
<div title={event.at} className=
|
|
1091
|
+
<div className="flex justify-start" data-ev={`${m.task_id}:reply`}>
|
|
1092
|
+
<div title={event.at} className={`max-w-[92%] sm:max-w-[min(85%,56ch)] bg-green-500/8 border border-green-500/15 rounded-2xl rounded-bl-md px-3 py-2.5 sm:px-4 shadow-sm ${currentMatchKey === `${m.task_id}:reply` ? 'ring-2 ring-yellow-400/60' : ''}`}>
|
|
901
1093
|
<div className="flex items-center justify-between gap-2 mb-2">
|
|
902
1094
|
<div className="flex items-center gap-1.5 min-w-0">
|
|
903
1095
|
{m.to_name && <AliasAvatar alias={m.to_name} size={14} />}
|
|
@@ -914,8 +1106,8 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes, active }
|
|
|
914
1106
|
</div>
|
|
915
1107
|
</div>
|
|
916
1108
|
<div className="text-[14px] leading-[1.65] text-[var(--fg)]">
|
|
917
|
-
<CollapsibleText><MarkdownContent text={m.result} /></CollapsibleText>
|
|
918
|
-
<AttachmentPreviews text={m.result} />
|
|
1109
|
+
<CollapsibleText><MarkdownContent text={m.result} onImageClick={openLightbox} /></CollapsibleText>
|
|
1110
|
+
<AttachmentPreviews text={m.result} onOpen={openLightbox} />
|
|
919
1111
|
</div>
|
|
920
1112
|
</div>
|
|
921
1113
|
</div>
|
|
@@ -927,15 +1119,15 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes, active }
|
|
|
927
1119
|
{sepEl}
|
|
928
1120
|
<div className="space-y-1.5 sm:space-y-2">
|
|
929
1121
|
{/* Outgoing task — labeled with origin so peer-forwarded tasks are obvious */}
|
|
930
|
-
<div className="flex justify-end">
|
|
931
|
-
<div title={m.created_at} className=
|
|
1122
|
+
<div className="flex justify-end" data-ev={`${m.task_id}:task`}>
|
|
1123
|
+
<div title={m.created_at} className={`max-w-[92%] sm:max-w-[min(85%,56ch)] bg-cyan-500/8 border border-cyan-500/15 rounded-2xl rounded-br-md px-3 py-2.5 sm:px-4 shadow-sm ${currentMatchKey === `${m.task_id}:task` ? 'ring-2 ring-yellow-400/60' : ''}`}>
|
|
932
1124
|
<div className="flex items-center gap-2 mb-1">
|
|
933
1125
|
<span className={`text-[10px] font-medium ${fromUser ? 'text-cyan-400' : 'text-purple-300'}`}>{senderLabel}</span>
|
|
934
1126
|
{!fromUser && <span className="text-[9px] text-[var(--fg-dim)]">forwarded to {m.to_name}</span>}
|
|
935
1127
|
</div>
|
|
936
1128
|
<div className="text-[14px] leading-[1.65] text-[var(--fg)]">
|
|
937
|
-
<CollapsibleText><MarkdownContent text={m.content} /></CollapsibleText>
|
|
938
|
-
<AttachmentPreviews text={m.content} />
|
|
1129
|
+
<CollapsibleText><MarkdownContent text={m.content} onImageClick={openLightbox} /></CollapsibleText>
|
|
1130
|
+
<AttachmentPreviews text={m.content} onOpen={openLightbox} />
|
|
939
1131
|
</div>
|
|
940
1132
|
<div className="flex flex-wrap items-center justify-between mt-1.5 gap-2 sm:gap-3">
|
|
941
1133
|
<StatusBar status={m.status} />
|
|
@@ -1072,6 +1264,7 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes, active }
|
|
|
1072
1264
|
<span>Enter to send · paste image</span>
|
|
1073
1265
|
</div>
|
|
1074
1266
|
</div>
|
|
1267
|
+
{lightboxUrl && <ImageLightbox url={lightboxUrl} opener={lightboxOpenerRef.current} onClose={() => setLightboxUrl(null)} />}
|
|
1075
1268
|
</>
|
|
1076
1269
|
);
|
|
1077
1270
|
|
package/app/nodes/page.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import { CreateNodeWizard } from '../components/CreateNodeWizard';
|
|
|
9
9
|
import { NodeLifecycleMenu } from '../components/NodeLifecycleMenu';
|
|
10
10
|
import { EmptyState, NodesEmptyState } from '../components/EmptyState';
|
|
11
11
|
import { AliasAvatar } from '../components/AliasAvatar';
|
|
12
|
+
import { useHasDraft } from '../lib/chat-drafts';
|
|
12
13
|
import { useCollapsibleSearch } from '../components/CollapsibleSearch';
|
|
13
14
|
import type { Session } from '../components/types';
|
|
14
15
|
import { SESSION_STATUS_CHIP_CLASS as STATUS_COLORS } from '../lib/status';
|
|
@@ -25,6 +26,13 @@ function shortServer(server: string | null | undefined): string {
|
|
|
25
26
|
type ViewMode = 'list' | 'grid';
|
|
26
27
|
type SessionRow = Session & { online: boolean };
|
|
27
28
|
|
|
29
|
+
/** R13 (草稿闭环还债): same [草稿] mark as AgentCard/CommandCenter tabs. */
|
|
30
|
+
function DraftMark({ alias }: { alias: string }) {
|
|
31
|
+
const hasDraft = useHasDraft(alias);
|
|
32
|
+
if (!hasDraft) return null;
|
|
33
|
+
return <span className="shrink-0 text-[9px] text-red-400/90" aria-label="有未发送草稿">[草稿]</span>;
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
export default function NodesPage() {
|
|
29
37
|
const { sessions, isLoading: loading } = useSessions();
|
|
30
38
|
const { health } = useHealth();
|
|
@@ -223,6 +231,7 @@ export default function NodesPage() {
|
|
|
223
231
|
<div className="min-w-0 flex-1">
|
|
224
232
|
<div className="flex items-center gap-2">
|
|
225
233
|
<span className="truncate text-base font-semibold text-white">{s.alias}</span>
|
|
234
|
+
<DraftMark alias={s.alias} />
|
|
226
235
|
{unread && <span className="shrink-0 rounded-full bg-red-500/15 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-red-300">New</span>}
|
|
227
236
|
<span className={`shrink-0 rounded-md border px-2 py-0.5 text-[10px] uppercase tracking-wide ${STATUS_COLORS[statusKey] || STATUS_COLORS.offline}`}>
|
|
228
237
|
{statusKey}
|
|
@@ -310,6 +319,7 @@ export default function NodesPage() {
|
|
|
310
319
|
{unread && <span className="absolute -right-1 -top-1 h-2.5 w-2.5 rounded-full border border-[#161618] bg-red-500" />}
|
|
311
320
|
</div>
|
|
312
321
|
<span className="truncate text-sm font-medium text-white">{s.alias}</span>
|
|
322
|
+
<DraftMark alias={s.alias} />
|
|
313
323
|
{unread && <span className="shrink-0 rounded-full bg-red-500/15 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-red-300">New</span>}
|
|
314
324
|
</div>
|
|
315
325
|
</div>
|
package/package.json
CHANGED