@sleep2agi/agent-network-dashboard 0.6.3-preview.26 → 0.6.3-preview.27
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 +56 -56
- 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 +13 -13
- package/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
- package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/server/app/_not-found.segments/_index.segment.rsc +8 -8
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- 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 +15 -15
- package/.next/server/app/admin.segments/_full.segment.rsc +15 -15
- package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
- package/.next/server/app/admin.segments/_index.segment.rsc +8 -8
- package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
- package/.next/server/app/index.html +2 -2
- package/.next/server/app/index.rsc +15 -15
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/server/app/index.segments/_index.segment.rsc +8 -8
- 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 +15 -15
- package/.next/server/app/login.segments/_full.segment.rsc +15 -15
- package/.next/server/app/login.segments/_head.segment.rsc +4 -4
- package/.next/server/app/login.segments/_index.segment.rsc +8 -8
- package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/login.segments/login.segment.rsc +3 -3
- 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 +15 -15
- package/.next/server/app/logs.segments/_full.segment.rsc +15 -15
- package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
- package/.next/server/app/logs.segments/_index.segment.rsc +8 -8
- package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
- 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 +15 -15
- package/.next/server/app/messages.segments/_full.segment.rsc +15 -15
- package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
- package/.next/server/app/messages.segments/_index.segment.rsc +8 -8
- package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
- 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 +15 -15
- package/.next/server/app/node.segments/_full.segment.rsc +15 -15
- package/.next/server/app/node.segments/_head.segment.rsc +4 -4
- package/.next/server/app/node.segments/_index.segment.rsc +8 -8
- package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/node.segments/node.segment.rsc +3 -3
- 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 +15 -15
- package/.next/server/app/nodes.segments/_full.segment.rsc +15 -15
- package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
- package/.next/server/app/nodes.segments/_index.segment.rsc +8 -8
- package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
- 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 +15 -15
- package/.next/server/app/providers.segments/_full.segment.rsc +15 -15
- package/.next/server/app/providers.segments/_head.segment.rsc +4 -4
- package/.next/server/app/providers.segments/_index.segment.rsc +8 -8
- package/.next/server/app/providers.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/providers.segments/providers/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/providers.segments/providers.segment.rsc +3 -3
- 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 +15 -15
- package/.next/server/app/server-logs.segments/_full.segment.rsc +15 -15
- package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
- package/.next/server/app/server-logs.segments/_index.segment.rsc +8 -8
- package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
- 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 +15 -15
- package/.next/server/app/servers.segments/_full.segment.rsc +15 -15
- package/.next/server/app/servers.segments/_head.segment.rsc +4 -4
- package/.next/server/app/servers.segments/_index.segment.rsc +8 -8
- package/.next/server/app/servers.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/servers.segments/servers/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/servers.segments/servers.segment.rsc +3 -3
- 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 +15 -15
- package/.next/server/app/settings/networks.segments/_full.segment.rsc +15 -15
- package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings/networks.segments/_index.segment.rsc +8 -8
- package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
- package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
- 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 +15 -15
- package/.next/server/app/settings/tokens.segments/_full.segment.rsc +15 -15
- package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings/tokens.segments/_index.segment.rsc +8 -8
- package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
- package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
- package/.next/server/app/settings.html +2 -2
- package/.next/server/app/settings.rsc +15 -15
- package/.next/server/app/settings.segments/_full.segment.rsc +15 -15
- package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings.segments/_index.segment.rsc +8 -8
- package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
- 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 +15 -15
- package/.next/server/app/tasks.segments/_full.segment.rsc +15 -15
- package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
- package/.next/server/app/tasks.segments/_index.segment.rsc +8 -8
- package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__01ba829._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__01ba829._.js.map +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__03jniap._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__03jniap._.js.map +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/dash-loop-wt_0rh8p9o._.js +2 -2
- package/.next/server/chunks/ssr/dash-loop-wt_0rh8p9o._.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_components_0cym4cd._.js +1 -1
- package/.next/server/chunks/ssr/dash-loop-wt_app_components_0cym4cd._.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/003mjjqx3v6yk.js +1 -0
- package/.next/static/chunks/{0.wghorif1wb7.js → 06.n2y2k~yrt~.js} +1 -1
- package/.next/static/chunks/{0r7w27h~adl.3.js → 0an42rwo7varw.js} +2 -2
- package/.next/static/chunks/{0~iyv4rh~-isz.js → 0cezceovnbf62.js} +1 -1
- package/.next/static/chunks/{13f074b29_3mi.js → 0cikm6bvsx0mf.js} +1 -1
- package/.next/static/chunks/0lxgv945mayng.css +1 -0
- package/.next/static/chunks/{100b9zpd7fl1_.js → 0t6h_sz7ghqia.js} +2 -2
- package/.next/static/chunks/{0.n6i7_v_qiyj.js → 0z2gn4t9od9qr.js} +1 -1
- package/.next/static/chunks/{0qgybbsb9e6q4.js → 178ne2q2eh~.y.js} +1 -1
- package/.next/trace +2 -2
- package/.next/trace-build +1 -1
- package/app/components/AgentCard.tsx +9 -1
- package/app/components/CommandCenter.tsx +12 -1
- package/app/components/MobileNav.tsx +4 -1
- package/app/components/NodeSettingsPanel.tsx +27 -0
- package/app/components/TaskChatPanel.tsx +10 -1
- package/app/lib/chat-mute.ts +58 -0
- package/app/lib/chat-unread.ts +14 -5
- package/app/nodes/page.tsx +11 -2
- package/package.json +1 -1
- package/.next/static/chunks/01ehs~t9be8tg.css +0 -1
- package/.next/static/chunks/11.tobnbsiypu.js +0 -1
- /package/.next/static/{IBftMMAovgdSJ2wqVhkUA → 6ay82HXjtazDvlzvX_UGl}/_buildManifest.js +0 -0
- /package/.next/static/{IBftMMAovgdSJ2wqVhkUA → 6ay82HXjtazDvlzvX_UGl}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{IBftMMAovgdSJ2wqVhkUA → 6ay82HXjtazDvlzvX_UGl}/_ssgManifest.js +0 -0
|
@@ -5,6 +5,7 @@ import { useChatUnread } from '../lib/chat-unread';
|
|
|
5
5
|
import { useHealth } from '../lib/hooks';
|
|
6
6
|
import { useNetworkId } from '../lib/network-context';
|
|
7
7
|
import { useHasDraft } from '../lib/chat-drafts';
|
|
8
|
+
import { isMuted, useMuteVersion } from '../lib/chat-mute';
|
|
8
9
|
import { TaskChatPanel } from './TaskChatPanel';
|
|
9
10
|
import { t } from '../lib/ui-lang';
|
|
10
11
|
|
|
@@ -24,6 +25,8 @@ interface CommandCenterProps {
|
|
|
24
25
|
*/
|
|
25
26
|
export function CommandCenter({ tabs, activeTab, onOpenTab, onCloseTab, onSetActive, onClose }: CommandCenterProps) {
|
|
26
27
|
const { unreadCount } = useChatUnread();
|
|
28
|
+
const muteVersion = useMuteVersion();
|
|
29
|
+
void muteVersion;
|
|
27
30
|
// R33: tab dot now shows PRESENCE (green online / gray offline, same SSE
|
|
28
31
|
// definition as everywhere) — the active tab is already marked by the
|
|
29
32
|
// border+text color, so the old active-state dot was redundant.
|
|
@@ -62,10 +65,18 @@ export function CommandCenter({ tabs, activeTab, onOpenTab, onCloseTab, onSetAct
|
|
|
62
65
|
>
|
|
63
66
|
<div className={`w-2 h-2 rounded-full ${isOnline(alias) ? 'bg-green-400' : 'bg-gray-600'}`} />
|
|
64
67
|
<span className="max-w-[80px] truncate">{alias}</span>
|
|
68
|
+
{isMuted(alias) && (
|
|
69
|
+
<svg aria-label="已开启免打扰" role="img" className="h-3 w-3 shrink-0 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
|
|
70
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9.88 4.12A6 6 0 0118 9v3.6l1.5 2.4H12M6.3 6.3A5.98 5.98 0 006 9v3.6L4.5 15H13M10 19a2 2 0 004 0M3 3l18 18" />
|
|
71
|
+
</svg>
|
|
72
|
+
)}
|
|
65
73
|
{/* R8: WeChat unread badge on BACKGROUND tabs (the active
|
|
66
74
|
tab is being read — no badge). */}
|
|
67
75
|
{activeTab !== alias && <TabDraftMark alias={alias} />}
|
|
68
|
-
{activeTab !== alias && unreadCount(alias) > 0 && (
|
|
76
|
+
{activeTab !== alias && unreadCount(alias) > 0 && isMuted(alias) && (
|
|
77
|
+
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500/80" />
|
|
78
|
+
)}
|
|
79
|
+
{activeTab !== alias && unreadCount(alias) > 0 && !isMuted(alias) && (
|
|
69
80
|
<span className="min-w-[15px] h-[15px] px-1 rounded-full bg-red-500 text-white text-[9px] font-semibold leading-[15px] text-center">
|
|
70
81
|
{unreadCount(alias) > 99 ? '99+' : unreadCount(alias)}
|
|
71
82
|
</span>
|
|
@@ -28,7 +28,7 @@ export function MobileNav() {
|
|
|
28
28
|
const pathname = usePathname();
|
|
29
29
|
// R8: WeChat-style total unread on the Agents tab (a glance answers
|
|
30
30
|
// "有没有人找我" from anywhere in the app).
|
|
31
|
-
const { totalUnread } = useChatUnread();
|
|
31
|
+
const { totalUnread, hasMutedUnread } = useChatUnread();
|
|
32
32
|
// Match Sidebar.tsx isActive — /settings must be exact-match because
|
|
33
33
|
// Settings subpages (/settings/tokens, /settings/networks) have their
|
|
34
34
|
// own headers; startsWith would keep the bottom-nav highlighted there
|
|
@@ -60,6 +60,9 @@ export function MobileNav() {
|
|
|
60
60
|
<svg className="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.6}>
|
|
61
61
|
<path strokeLinecap="round" strokeLinejoin="round" d={item.icon} />
|
|
62
62
|
</svg>
|
|
63
|
+
{item.href === '/nodes' && totalUnread === 0 && hasMutedUnread && (
|
|
64
|
+
<span aria-label="有未读(已静音)" className="absolute -top-0.5 right-1 h-2 w-2 rounded-full bg-red-500/80" />
|
|
65
|
+
)}
|
|
63
66
|
{item.href === '/nodes' && totalUnread > 0 && (
|
|
64
67
|
<span className="absolute -top-1 -right-2.5 min-w-[15px] h-[15px] px-1 rounded-full bg-red-500 text-white text-[9px] font-semibold leading-[15px] text-center">
|
|
65
68
|
{badgeLabel(totalUnread)}
|
|
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
4
4
|
import { Session } from './types';
|
|
5
5
|
import { AliasAvatar } from './AliasAvatar';
|
|
6
6
|
import { setAvatarUrl, useAvatarUrl } from '../lib/avatars';
|
|
7
|
+
import { toggleMute, useMuted } from '../lib/chat-mute';
|
|
7
8
|
import { useLifecycleCaps } from './NodeLifecycleMenu';
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -148,6 +149,30 @@ function SectionTitle({ children }: { children: React.ReactNode }) {
|
|
|
148
149
|
return <h3 className="text-[11px] font-medium uppercase tracking-wide text-gray-500 mb-2">{children}</h3>;
|
|
149
150
|
}
|
|
150
151
|
|
|
152
|
+
/* R34 (微信消息免打扰): per-conversation mute switch. Muted = badge shows
|
|
153
|
+
a dot instead of the count and the global total excludes it. */
|
|
154
|
+
function MuteSection({ alias }: { alias: string }) {
|
|
155
|
+
const muted = useMuted(alias);
|
|
156
|
+
return (
|
|
157
|
+
<section>
|
|
158
|
+
<SectionTitle>聊天</SectionTitle>
|
|
159
|
+
<button
|
|
160
|
+
type="button"
|
|
161
|
+
role="switch"
|
|
162
|
+
aria-checked={muted}
|
|
163
|
+
onClick={() => toggleMute(alias)}
|
|
164
|
+
className="flex w-full items-center justify-between rounded-lg border border-[#26262b] bg-[#161618] px-3 py-2.5"
|
|
165
|
+
>
|
|
166
|
+
<span className="text-[12px] text-gray-300">消息免打扰</span>
|
|
167
|
+
<span className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${muted ? 'bg-cyan-600' : 'bg-[#2a2a30]'}`}>
|
|
168
|
+
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${muted ? 'translate-x-4.5' : 'translate-x-0.5'}`} style={{ transform: muted ? 'translateX(18px)' : 'translateX(2px)' }} />
|
|
169
|
+
</span>
|
|
170
|
+
</button>
|
|
171
|
+
<p className="mt-1.5 text-[10px] text-gray-600">静音后未读只显红点、不计入全局数字(存本浏览器)</p>
|
|
172
|
+
</section>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
151
176
|
/* R24 (Vincent 亲点: 头像换血) — per-node custom avatar. Stored browser-side
|
|
152
177
|
(localStorage via app/lib/avatars) until the hub grows an RFC-024
|
|
153
178
|
whitelist entry for avatar_url; the designed default set rides
|
|
@@ -639,6 +664,8 @@ export function NodeSettingsPanel({ session: s, onClose }: { session: Session; o
|
|
|
639
664
|
|
|
640
665
|
<AvatarSection alias={s.alias} />
|
|
641
666
|
|
|
667
|
+
<MuteSection alias={s.alias} />
|
|
668
|
+
|
|
642
669
|
{/* B. Runtime + 模型 (model editable) */}
|
|
643
670
|
<section>
|
|
644
671
|
<SectionTitle>Runtime · 模型</SectionTitle>
|
|
@@ -321,6 +321,7 @@ import remarkGfm from 'remark-gfm';
|
|
|
321
321
|
import { useSSE } from '../lib/useSSE';
|
|
322
322
|
import { useNetworkId } from '../lib/network-context';
|
|
323
323
|
import { useHealth } from '../lib/hooks';
|
|
324
|
+
import { useMuted } from '../lib/chat-mute';
|
|
324
325
|
import { markChatRead } from '../lib/chat-unread';
|
|
325
326
|
import { readDraft, writeDraft } from '../lib/chat-drafts';
|
|
326
327
|
import { pinyinMatch, usePinyinReady } from '../lib/pinyin-match';
|
|
@@ -464,6 +465,7 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes, active }
|
|
|
464
465
|
// (#214 "one definition"); useHealth is SWR-deduped with the page-level
|
|
465
466
|
// poll, so this adds no extra requests while Overview is mounted.
|
|
466
467
|
const { health } = useHealth();
|
|
468
|
+
const chatMuted = useMuted(alias);
|
|
467
469
|
const sseOnline = (() => {
|
|
468
470
|
const m = health?.sse_sessions;
|
|
469
471
|
if (!m) return null;
|
|
@@ -1444,7 +1446,14 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes, active }
|
|
|
1444
1446
|
<div className="flex items-center gap-3">
|
|
1445
1447
|
<AliasAvatar alias={alias} size={32} />
|
|
1446
1448
|
<div>
|
|
1447
|
-
<div className="text-sm font-semibold text-[var(--fg)]">
|
|
1449
|
+
<div className="flex items-center gap-1.5 text-sm font-semibold text-[var(--fg)]">
|
|
1450
|
+
<span>{alias}</span>
|
|
1451
|
+
{chatMuted && (
|
|
1452
|
+
<svg aria-label={t('已开启免打扰', 'Muted')} role="img" className="h-3.5 w-3.5 text-[var(--fg-dim)]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
|
|
1453
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9.88 4.12A6 6 0 0118 9v3.6l1.5 2.4H12M6.3 6.3A5.98 5.98 0 006 9v3.6L4.5 15H13M10 19a2 2 0 004 0M3 3l18 18" />
|
|
1454
|
+
</svg>
|
|
1455
|
+
)}
|
|
1456
|
+
</div>
|
|
1448
1457
|
<div className="flex items-center gap-1.5 text-[10px] text-[var(--fg-muted)]">
|
|
1449
1458
|
{sseOnline !== null && (
|
|
1450
1459
|
<span aria-hidden className={`inline-block h-1.5 w-1.5 rounded-full ${sseOnline ? 'bg-green-400' : 'bg-gray-600'}`} />
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useSyncExternalStore } from 'react';
|
|
4
|
+
|
|
5
|
+
/* Loop R34 (微信消息免打扰, 克制版): per-conversation mute. Muted
|
|
6
|
+
* conversations keep their red badge but show a DOT instead of the count,
|
|
7
|
+
* and their unreads are excluded from the global total (WeChat semantics).
|
|
8
|
+
* Same storage/reactivity pattern as chat-drafts. */
|
|
9
|
+
|
|
10
|
+
const STORE_KEY = 'anet_chat_mute_v1';
|
|
11
|
+
const MUTE_EVENT = 'anet-chat-mute-updated';
|
|
12
|
+
|
|
13
|
+
function readSet(): Set<string> {
|
|
14
|
+
if (typeof window === 'undefined') return new Set();
|
|
15
|
+
try {
|
|
16
|
+
const parsed = JSON.parse(window.localStorage.getItem(STORE_KEY) || '[]');
|
|
17
|
+
return new Set(Array.isArray(parsed) ? parsed : []);
|
|
18
|
+
} catch {
|
|
19
|
+
return new Set();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isMuted(alias?: string | null): boolean {
|
|
24
|
+
if (!alias) return false;
|
|
25
|
+
return readSet().has(alias);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function toggleMute(alias: string) {
|
|
29
|
+
if (typeof window === 'undefined' || !alias) return;
|
|
30
|
+
try {
|
|
31
|
+
const s = readSet();
|
|
32
|
+
if (s.has(alias)) s.delete(alias);
|
|
33
|
+
else s.add(alias);
|
|
34
|
+
window.localStorage.setItem(STORE_KEY, JSON.stringify([...s]));
|
|
35
|
+
window.dispatchEvent(new Event(MUTE_EVENT));
|
|
36
|
+
} catch {}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let version = 0;
|
|
40
|
+
function subscribe(callback: () => void) {
|
|
41
|
+
const bump = () => { version++; callback(); };
|
|
42
|
+
window.addEventListener(MUTE_EVENT, bump);
|
|
43
|
+
window.addEventListener('storage', bump);
|
|
44
|
+
return () => {
|
|
45
|
+
window.removeEventListener(MUTE_EVENT, bump);
|
|
46
|
+
window.removeEventListener('storage', bump);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function useMuted(alias?: string | null): boolean {
|
|
51
|
+
return useSyncExternalStore(subscribe, () => (alias ? isMuted(alias) : false), () => false);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Monotonic version for callers that use plain isMuted() inside render
|
|
55
|
+
* loops (map callbacks can't call hooks). */
|
|
56
|
+
export function useMuteVersion(): number {
|
|
57
|
+
return useSyncExternalStore(subscribe, () => version, () => 0);
|
|
58
|
+
}
|
package/app/lib/chat-unread.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
4
4
|
import useSWR from 'swr';
|
|
5
5
|
import { useNetworkId } from './network-context';
|
|
6
|
+
import { isMuted, useMuteVersion } from './chat-mute';
|
|
6
7
|
|
|
7
8
|
const STORAGE_PREFIX = 'anet_chat_read_v1:';
|
|
8
9
|
const UNREAD_EVENT = 'anet-chat-read-updated';
|
|
@@ -181,17 +182,25 @@ export function useChatUnread() {
|
|
|
181
182
|
return unreadMap.lastActivity.get(alias) || 0;
|
|
182
183
|
}, [unreadMap]);
|
|
183
184
|
|
|
184
|
-
|
|
185
|
+
// R34: muted conversations are excluded from the global count (WeChat
|
|
186
|
+
// semantics); hasMutedUnread lets entry badges fall back to a dot.
|
|
187
|
+
const muteVersion = useMuteVersion();
|
|
188
|
+
const { totalUnread, hasMutedUnread } = useMemo(() => {
|
|
189
|
+
void muteVersion;
|
|
185
190
|
let t = 0;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
191
|
+
let mutedAny = false;
|
|
192
|
+
for (const [alias, n] of unreadMap.counts) {
|
|
193
|
+
if (isMuted(alias)) { if (n > 0) mutedAny = true; continue; }
|
|
194
|
+
t += n;
|
|
195
|
+
}
|
|
196
|
+
return { totalUnread: t, hasMutedUnread: mutedAny };
|
|
197
|
+
}, [unreadMap, muteVersion]);
|
|
189
198
|
|
|
190
199
|
const clearUnread = useCallback((alias: string) => {
|
|
191
200
|
markChatRead(alias, networkId);
|
|
192
201
|
}, [networkId]);
|
|
193
202
|
|
|
194
|
-
return { unreadMap, hasUnread, unreadCount, lastActivityAt, totalUnread, clearUnread };
|
|
203
|
+
return { unreadMap, hasUnread, unreadCount, lastActivityAt, totalUnread, hasMutedUnread, clearUnread };
|
|
195
204
|
}
|
|
196
205
|
|
|
197
206
|
/** WeChat badge label: 1-99 as-is, above → "99+"; 0/negative → '' (hide). */
|
package/app/nodes/page.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import { NodeLifecycleMenu } from '../components/NodeLifecycleMenu';
|
|
|
10
10
|
import { EmptyState, NodesEmptyState } from '../components/EmptyState';
|
|
11
11
|
import { AliasAvatar } from '../components/AliasAvatar';
|
|
12
12
|
import { useHasDraft } from '../lib/chat-drafts';
|
|
13
|
+
import { isMuted, useMuteVersion } from '../lib/chat-mute';
|
|
13
14
|
import { pinyinMatch, usePinyinReady } from '../lib/pinyin-match';
|
|
14
15
|
import { useCollapsibleSearch } from '../components/CollapsibleSearch';
|
|
15
16
|
import type { Session } from '../components/types';
|
|
@@ -56,6 +57,8 @@ export default function NodesPage() {
|
|
|
56
57
|
const [filterStatus, setFilterStatus] = useState('');
|
|
57
58
|
const [search, setSearch] = useState('');
|
|
58
59
|
const pinyinReady = usePinyinReady();
|
|
60
|
+
const muteVersion = useMuteVersion();
|
|
61
|
+
void muteVersion; // re-render on mute toggles (isMuted used in map callbacks)
|
|
59
62
|
void pinyinReady; // R25: re-filter once the lazy pinyin dict lands
|
|
60
63
|
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
|
61
64
|
const [chatAlias, setChatAlias] = useState<string | null>(null);
|
|
@@ -231,7 +234,10 @@ export default function NodesPage() {
|
|
|
231
234
|
<div className="relative shrink-0">
|
|
232
235
|
<span className="relative inline-flex shrink-0">
|
|
233
236
|
<AliasAvatar alias={s.alias} size={36} />
|
|
234
|
-
{unreadCount(s.alias) > 0 && (
|
|
237
|
+
{unreadCount(s.alias) > 0 && isMuted(s.alias) && (
|
|
238
|
+
<span aria-label={t('有未读(已静音)', 'Unread (muted)')} className="absolute -top-0.5 -right-1 h-2.5 w-2.5 rounded-full bg-red-500/80 ring-2 ring-[#161618]" />
|
|
239
|
+
)}
|
|
240
|
+
{unreadCount(s.alias) > 0 && !isMuted(s.alias) && (
|
|
235
241
|
<span aria-label={t(`${unreadCount(s.alias)} 条未读`, `${unreadCount(s.alias)} unread`)} className="absolute -top-1 -right-1.5 min-w-[16px] h-4 px-1 rounded-full bg-red-500 text-white text-[9px] font-semibold leading-4 text-center ring-2 ring-[#161618]">
|
|
236
242
|
{unreadCount(s.alias) > 99 ? '99+' : unreadCount(s.alias)}
|
|
237
243
|
</span>
|
|
@@ -321,7 +327,10 @@ export default function NodesPage() {
|
|
|
321
327
|
<div className="relative shrink-0">
|
|
322
328
|
<span className="relative inline-flex shrink-0">
|
|
323
329
|
<AliasAvatar alias={s.alias} size={20} />
|
|
324
|
-
{unreadCount(s.alias) > 0 && (
|
|
330
|
+
{unreadCount(s.alias) > 0 && isMuted(s.alias) && (
|
|
331
|
+
<span aria-label={t('有未读(已静音)', 'Unread (muted)')} className="absolute -top-1 -right-1 h-2 w-2 rounded-full bg-red-500/80" />
|
|
332
|
+
)}
|
|
333
|
+
{unreadCount(s.alias) > 0 && !isMuted(s.alias) && (
|
|
325
334
|
<span aria-label={t(`${unreadCount(s.alias)} 条未读`, `${unreadCount(s.alias)} unread`)} className="absolute -top-1.5 -right-2 min-w-[15px] h-[15px] px-1 rounded-full bg-red-500 text-white text-[9px] font-semibold leading-[15px] text-center">
|
|
326
335
|
{unreadCount(s.alias) > 99 ? '99+' : unreadCount(s.alias)}
|
|
327
336
|
</span>
|
package/package.json
CHANGED