@peers-app/peers-ui 0.12.0 → 0.12.2
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/dist/components/chat-overlay.js +12 -1
- package/dist/components/markdown-editor/markdown-plugin.js +1 -1
- package/dist/components/messages/message-compose.d.ts +1 -0
- package/dist/components/messages/message-compose.js +3 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/screens/network-viewer/connection-troubleshooter.d.ts +14 -0
- package/dist/screens/network-viewer/connection-troubleshooter.js +298 -0
- package/dist/screens/network-viewer/index.d.ts +1 -0
- package/dist/screens/network-viewer/index.js +1 -0
- package/dist/screens/network-viewer/network-viewer.js +12 -3
- package/package.json +3 -3
- package/src/components/chat-overlay.tsx +14 -0
- package/src/components/markdown-editor/markdown-plugin.tsx +1 -2
- package/src/components/messages/message-compose.tsx +4 -2
- package/src/index.tsx +1 -1
- package/src/screens/network-viewer/connection-troubleshooter.tsx +358 -0
- package/src/screens/network-viewer/index.ts +1 -0
- package/src/screens/network-viewer/network-viewer.tsx +24 -8
|
@@ -83,6 +83,7 @@ const ChatOverlay = ({ position = 'bottom-right', }) => {
|
|
|
83
83
|
const [voiceSettings, setVoiceSettings] = (0, react_1.useState)(DEFAULT_VOICE_SETTINGS);
|
|
84
84
|
const [overlayPosition, setOverlayPosition] = (0, react_1.useState)(DEFAULT_POSITION);
|
|
85
85
|
const [isDragging, setIsDragging] = (0, react_1.useState)(false);
|
|
86
|
+
const [pendingInitialMessage, setPendingInitialMessage] = (0, react_1.useState)(undefined);
|
|
86
87
|
const messagesEndRef = (0, react_1.useRef)(null);
|
|
87
88
|
const messagesContainerRef = (0, react_1.useRef)(null);
|
|
88
89
|
const currentAudioRef = (0, react_1.useRef)(null);
|
|
@@ -185,6 +186,14 @@ const ChatOverlay = ({ position = 'bottom-right', }) => {
|
|
|
185
186
|
// Subscribe to voice events
|
|
186
187
|
(0, react_1.useEffect)(() => {
|
|
187
188
|
const subscriptions = [
|
|
189
|
+
(0, peers_sdk_1.subscribe)('chat:openWithMessage', (event) => {
|
|
190
|
+
const { message } = event.data || {};
|
|
191
|
+
if (message) {
|
|
192
|
+
setPendingInitialMessage(message);
|
|
193
|
+
}
|
|
194
|
+
setIsExpanded(true);
|
|
195
|
+
setShowSettings(false);
|
|
196
|
+
}),
|
|
188
197
|
(0, peers_sdk_1.subscribe)('voice:stateChanged', (event) => {
|
|
189
198
|
setVoiceState(event.data.state);
|
|
190
199
|
if (event.data.state === 'idle') {
|
|
@@ -354,6 +363,8 @@ const ChatOverlay = ({ position = 'bottom-right', }) => {
|
|
|
354
363
|
const handleMessageSubmit = (0, react_1.useCallback)(async (message) => {
|
|
355
364
|
// Notify voice service of text activity (resets inactivity timer)
|
|
356
365
|
peers_sdk_1.rpcServerCalls.voiceNotifyTextActivity?.().catch(() => { });
|
|
366
|
+
// Clear any pending initial message that was pre-filled
|
|
367
|
+
setPendingInitialMessage(undefined);
|
|
357
368
|
// If no thread yet, this message becomes the thread root
|
|
358
369
|
if (!threadId) {
|
|
359
370
|
message.messageParentId = undefined;
|
|
@@ -480,7 +491,7 @@ const ChatOverlay = ({ position = 'bottom-right', }) => {
|
|
|
480
491
|
voiceState === 'recording' && (react_1.default.createElement("div", { className: "progress flex-grow-1", style: { height: '4px' } },
|
|
481
492
|
react_1.default.createElement("div", { className: "progress-bar bg-danger", style: { width: `${Math.min(100, volumeLevel * 500)}%` } })))))),
|
|
482
493
|
!showSettings && (react_1.default.createElement("div", { className: "card-footer p-2" },
|
|
483
|
-
react_1.default.createElement(message_compose_1.MessageCompose, { channelId: globals_1.me?.userId || 'default', threadId: threadId || (0, peers_sdk_1.newid)(), onMessageSubmit: handleMessageSubmit }))))),
|
|
494
|
+
react_1.default.createElement(message_compose_1.MessageCompose, { channelId: globals_1.me?.userId || 'default', threadId: threadId || (0, peers_sdk_1.newid)(), onMessageSubmit: handleMessageSubmit, initialMessage: pendingInitialMessage, key: pendingInitialMessage ? `pre-${pendingInitialMessage.slice(0, 20)}` : 'default' }))))),
|
|
484
495
|
react_1.default.createElement("div", { className: "d-flex gap-2 justify-content-end", onMouseDown: handleDragStart, style: { userSelect: isDragging ? 'none' : undefined } },
|
|
485
496
|
voiceSettings.enabled && isExpanded && (react_1.default.createElement("button", { className: getButtonClass(), style: {
|
|
486
497
|
width: '56px',
|
|
@@ -149,7 +149,7 @@ function MarkdownPlugin(props) {
|
|
|
149
149
|
setMarkdown();
|
|
150
150
|
});
|
|
151
151
|
const sub2Dispose = editor.registerUpdateListener(({ editorState }) => {
|
|
152
|
-
|
|
152
|
+
editorState.read(() => {
|
|
153
153
|
const newMarkdown = (0, markdown_1.$convertToMarkdownString)(exports.customMarkdownTransformers, undefined, true);
|
|
154
154
|
if (newMarkdown !== markdown) {
|
|
155
155
|
markdown = newMarkdown;
|
|
@@ -5,6 +5,7 @@ interface IProps {
|
|
|
5
5
|
channelId: string;
|
|
6
6
|
threadId?: string;
|
|
7
7
|
onMessageSubmit: (message: IMessage, files?: IFile[]) => any;
|
|
8
|
+
initialMessage?: string;
|
|
8
9
|
}
|
|
9
10
|
export declare const MessageCompose: (props: IProps) => React.JSX.Element;
|
|
10
11
|
export declare function isMobile(): boolean;
|
|
@@ -61,7 +61,7 @@ function getContentPersistenceValue(channelId, threadId) {
|
|
|
61
61
|
return pVarCache[contentPersistenceName];
|
|
62
62
|
}
|
|
63
63
|
const MessageCompose = (props) => {
|
|
64
|
-
const { channelId, threadId, onMessageSubmit } = props;
|
|
64
|
+
const { channelId, threadId, onMessageSubmit, initialMessage } = props;
|
|
65
65
|
const [contentObs] = (0, react_1.useState)(() => (0, peers_sdk_1.observable)(''));
|
|
66
66
|
const [mdEffects] = (0, react_1.useState)(() => ({}));
|
|
67
67
|
const composeBottomRef = react_1.default.useRef(null);
|
|
@@ -75,7 +75,8 @@ const MessageCompose = (props) => {
|
|
|
75
75
|
let sub;
|
|
76
76
|
getContentPersistenceValue(channelId, threadId).loadingPromise.then(pVar => {
|
|
77
77
|
if (!disposed) {
|
|
78
|
-
|
|
78
|
+
const persistedValue = pVar() ?? '';
|
|
79
|
+
contentObs(initialMessage ?? persistedValue);
|
|
79
80
|
sub = contentObs.subscribe(() => pVar(contentObs()));
|
|
80
81
|
setTimeout(() => scrollToBottom(), 100);
|
|
81
82
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from "./screens/events/cron";
|
|
2
2
|
export * from "./tabs-layout/tabs-layout";
|
|
3
|
+
export { activeTabId, activeTabs, TabState } from "./tabs-layout/tabs-state";
|
|
3
4
|
export * from "./components/voice-indicator";
|
|
4
5
|
export * from "./components/chat-overlay";
|
|
5
6
|
export * from "./components/sortable-list";
|
package/dist/index.js
CHANGED
|
@@ -14,8 +14,12 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.activeTabs = exports.activeTabId = void 0;
|
|
17
18
|
__exportStar(require("./screens/events/cron"), exports);
|
|
18
19
|
__exportStar(require("./tabs-layout/tabs-layout"), exports);
|
|
20
|
+
var tabs_state_1 = require("./tabs-layout/tabs-state");
|
|
21
|
+
Object.defineProperty(exports, "activeTabId", { enumerable: true, get: function () { return tabs_state_1.activeTabId; } });
|
|
22
|
+
Object.defineProperty(exports, "activeTabs", { enumerable: true, get: function () { return tabs_state_1.activeTabs; } });
|
|
19
23
|
__exportStar(require("./components/voice-indicator"), exports);
|
|
20
24
|
__exportStar(require("./components/chat-overlay"), exports);
|
|
21
25
|
__exportStar(require("./components/sortable-list"), exports);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface IDiagnosticCheck {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
layer: string;
|
|
6
|
+
status: 'pass' | 'fail' | 'warn' | 'skip' | 'running';
|
|
7
|
+
detail: string;
|
|
8
|
+
suggestion?: string;
|
|
9
|
+
}
|
|
10
|
+
interface Props {
|
|
11
|
+
onBack: () => void;
|
|
12
|
+
}
|
|
13
|
+
export declare function ConnectionTroubleshooter({ onBack }: Props): React.JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ConnectionTroubleshooter = ConnectionTroubleshooter;
|
|
37
|
+
const react_1 = __importStar(require("react"));
|
|
38
|
+
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
39
|
+
const STATUS_ICON = {
|
|
40
|
+
pass: react_1.default.createElement("i", { className: "bi bi-check-circle-fill text-success" }),
|
|
41
|
+
fail: react_1.default.createElement("i", { className: "bi bi-x-circle-fill text-danger" }),
|
|
42
|
+
warn: react_1.default.createElement("i", { className: "bi bi-exclamation-triangle-fill text-warning" }),
|
|
43
|
+
skip: react_1.default.createElement("i", { className: "bi bi-dash-circle text-muted" }),
|
|
44
|
+
running: (react_1.default.createElement("span", { className: "spinner-border spinner-border-sm text-primary", role: "status", "aria-hidden": "true" })),
|
|
45
|
+
};
|
|
46
|
+
const STATUS_BADGE_CLASS = {
|
|
47
|
+
pass: 'bg-success',
|
|
48
|
+
fail: 'bg-danger',
|
|
49
|
+
warn: 'bg-warning text-dark',
|
|
50
|
+
skip: 'bg-secondary',
|
|
51
|
+
running: 'bg-primary',
|
|
52
|
+
};
|
|
53
|
+
const LAYER_ORDER = [
|
|
54
|
+
'Local System',
|
|
55
|
+
'Cloud Relay',
|
|
56
|
+
'Local Network',
|
|
57
|
+
'Protocol Support',
|
|
58
|
+
'WebRTC / TURN',
|
|
59
|
+
'Peer State',
|
|
60
|
+
'Active Connections',
|
|
61
|
+
];
|
|
62
|
+
function groupByLayer(checks) {
|
|
63
|
+
const groups = {};
|
|
64
|
+
for (const check of checks) {
|
|
65
|
+
if (!groups[check.layer])
|
|
66
|
+
groups[check.layer] = [];
|
|
67
|
+
groups[check.layer].push(check);
|
|
68
|
+
}
|
|
69
|
+
return groups;
|
|
70
|
+
}
|
|
71
|
+
function getSummary(checks) {
|
|
72
|
+
return {
|
|
73
|
+
issues: checks.filter(c => c.status === 'fail').length,
|
|
74
|
+
warnings: checks.filter(c => c.status === 'warn').length,
|
|
75
|
+
passes: checks.filter(c => c.status === 'pass').length,
|
|
76
|
+
skips: checks.filter(c => c.status === 'skip').length,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function formatReportForAI(checks) {
|
|
80
|
+
const lines = ['Connection Troubleshooter Report', '================================'];
|
|
81
|
+
const groups = groupByLayer(checks);
|
|
82
|
+
for (const layer of LAYER_ORDER) {
|
|
83
|
+
const layerChecks = groups[layer];
|
|
84
|
+
if (!layerChecks)
|
|
85
|
+
continue;
|
|
86
|
+
lines.push(`\n[${layer}]`);
|
|
87
|
+
for (const check of layerChecks) {
|
|
88
|
+
const statusStr = check.status.toUpperCase().padEnd(7);
|
|
89
|
+
lines.push(` ${statusStr} ${check.label}: ${check.detail}`);
|
|
90
|
+
if (check.suggestion) {
|
|
91
|
+
lines.push(` Suggestion: ${check.suggestion}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const { issues, warnings, skips } = getSummary(checks);
|
|
96
|
+
lines.push(`\nSummary: ${issues} issue(s), ${warnings} warning(s), ${skips} skipped.`);
|
|
97
|
+
return lines.join('\n');
|
|
98
|
+
}
|
|
99
|
+
function ConnectionTroubleshooter({ onBack }) {
|
|
100
|
+
const [checks, setChecks] = (0, react_1.useState)([]);
|
|
101
|
+
const [running, setRunning] = (0, react_1.useState)(false);
|
|
102
|
+
const [done, setDone] = (0, react_1.useState)(false);
|
|
103
|
+
const [expanded, setExpanded] = (0, react_1.useState)(new Set());
|
|
104
|
+
const hasRun = (0, react_1.useRef)(false);
|
|
105
|
+
const runDiagnostics = async () => {
|
|
106
|
+
setRunning(true);
|
|
107
|
+
setDone(false);
|
|
108
|
+
setChecks([]);
|
|
109
|
+
setExpanded(new Set());
|
|
110
|
+
const api = window.electronAPI?.networkViewer;
|
|
111
|
+
if (!api) {
|
|
112
|
+
setChecks([{
|
|
113
|
+
id: 'no-api',
|
|
114
|
+
label: 'Electron API unavailable',
|
|
115
|
+
layer: 'Local System',
|
|
116
|
+
status: 'fail',
|
|
117
|
+
detail: 'The Electron network API is not available. This tool only works in the desktop app.',
|
|
118
|
+
}]);
|
|
119
|
+
setRunning(false);
|
|
120
|
+
setDone(true);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
// Stream results by running all checks on the backend and updating as they come in.
|
|
125
|
+
// Show a placeholder "running" row immediately while the full batch executes.
|
|
126
|
+
const placeholders = LAYER_ORDER.map(layer => ({
|
|
127
|
+
id: `placeholder-${layer}`,
|
|
128
|
+
label: `Checking ${layer}…`,
|
|
129
|
+
layer,
|
|
130
|
+
status: 'running',
|
|
131
|
+
detail: '',
|
|
132
|
+
}));
|
|
133
|
+
setChecks(placeholders);
|
|
134
|
+
const results = await api.runDiagnostics();
|
|
135
|
+
setChecks(results);
|
|
136
|
+
// Auto-expand all failed/warned checks so they're visible immediately
|
|
137
|
+
const toExpand = new Set();
|
|
138
|
+
for (const r of results) {
|
|
139
|
+
if ((r.status === 'fail' || r.status === 'warn') && r.suggestion) {
|
|
140
|
+
toExpand.add(r.id);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
setExpanded(toExpand);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
setChecks([{
|
|
147
|
+
id: 'run-error',
|
|
148
|
+
label: 'Diagnostics failed to run',
|
|
149
|
+
layer: 'Local System',
|
|
150
|
+
status: 'fail',
|
|
151
|
+
detail: `Error: ${err?.message || 'Unknown error'}`,
|
|
152
|
+
suggestion: 'Restart the application and try again.',
|
|
153
|
+
}]);
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
setRunning(false);
|
|
157
|
+
setDone(true);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
(0, react_1.useEffect)(() => {
|
|
161
|
+
if (!hasRun.current) {
|
|
162
|
+
hasRun.current = true;
|
|
163
|
+
runDiagnostics();
|
|
164
|
+
}
|
|
165
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
166
|
+
}, []);
|
|
167
|
+
const toggleExpand = (id) => {
|
|
168
|
+
setExpanded(prev => {
|
|
169
|
+
const next = new Set(prev);
|
|
170
|
+
if (next.has(id))
|
|
171
|
+
next.delete(id);
|
|
172
|
+
else
|
|
173
|
+
next.add(id);
|
|
174
|
+
return next;
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
const handleAskAssistant = () => {
|
|
178
|
+
const report = formatReportForAI(checks.filter(c => c.status !== 'running'));
|
|
179
|
+
const message = `I ran the Peers connection troubleshooter and got the following results. Please help me understand what's wrong and how to fix it:\n\n${report}`;
|
|
180
|
+
(0, peers_sdk_1.emit)({ name: 'chat:openWithMessage', data: { message } }).catch(() => {
|
|
181
|
+
// Fallback: copy to clipboard
|
|
182
|
+
navigator.clipboard.writeText(message).then(() => {
|
|
183
|
+
alert('Diagnostic report copied to clipboard. Paste it into the AI assistant chat.');
|
|
184
|
+
}).catch(() => {
|
|
185
|
+
alert('Could not open AI assistant. Please open the chat manually and paste the diagnostic report.');
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
};
|
|
189
|
+
const groups = groupByLayer(checks.filter(c => !c.id.startsWith('placeholder-')));
|
|
190
|
+
const placeholders = checks.filter(c => c.id.startsWith('placeholder-'));
|
|
191
|
+
const summary = getSummary(checks);
|
|
192
|
+
const completedCount = checks.filter(c => c.status !== 'running').length;
|
|
193
|
+
const totalCount = checks.length;
|
|
194
|
+
const progressPct = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0;
|
|
195
|
+
const overallStatus = !done
|
|
196
|
+
? 'running'
|
|
197
|
+
: summary.issues > 0
|
|
198
|
+
? 'fail'
|
|
199
|
+
: summary.warnings > 0
|
|
200
|
+
? 'warn'
|
|
201
|
+
: 'pass';
|
|
202
|
+
return (react_1.default.createElement("div", { className: "container-fluid p-4" },
|
|
203
|
+
react_1.default.createElement("div", { className: "d-flex justify-content-between align-items-center mb-3" },
|
|
204
|
+
react_1.default.createElement("div", { className: "d-flex align-items-center gap-3" },
|
|
205
|
+
react_1.default.createElement("button", { className: "btn btn-sm btn-outline-secondary", onClick: onBack },
|
|
206
|
+
react_1.default.createElement("i", { className: "bi bi-arrow-left me-1" }),
|
|
207
|
+
"Back"),
|
|
208
|
+
react_1.default.createElement("h4", { className: "mb-0" },
|
|
209
|
+
react_1.default.createElement("i", { className: "bi bi-stethoscope me-2 text-primary" }),
|
|
210
|
+
"Connection Troubleshooter")),
|
|
211
|
+
react_1.default.createElement("button", { className: "btn btn-sm btn-primary", onClick: runDiagnostics, disabled: running }, running ? (react_1.default.createElement(react_1.default.Fragment, null,
|
|
212
|
+
react_1.default.createElement("span", { className: "spinner-border spinner-border-sm me-1", role: "status", "aria-hidden": "true" }),
|
|
213
|
+
"Running\u2026")) : (react_1.default.createElement(react_1.default.Fragment, null,
|
|
214
|
+
react_1.default.createElement("i", { className: "bi bi-arrow-clockwise me-1" }),
|
|
215
|
+
"Re-run")))),
|
|
216
|
+
running && (react_1.default.createElement("div", { className: "mb-4" },
|
|
217
|
+
react_1.default.createElement("div", { className: "d-flex justify-content-between mb-1" },
|
|
218
|
+
react_1.default.createElement("small", { className: "text-muted" }, "Running diagnostics\u2026"),
|
|
219
|
+
react_1.default.createElement("small", { className: "text-muted" },
|
|
220
|
+
completedCount,
|
|
221
|
+
" / ",
|
|
222
|
+
totalCount)),
|
|
223
|
+
react_1.default.createElement("div", { className: "progress", style: { height: '6px' } },
|
|
224
|
+
react_1.default.createElement("div", { className: "progress-bar progress-bar-striped progress-bar-animated", style: { width: `${progressPct}%` } })))),
|
|
225
|
+
done && checks.length > 0 && (react_1.default.createElement("div", { className: `alert ${overallStatus === 'pass' ? 'alert-success' : overallStatus === 'warn' ? 'alert-warning' : 'alert-danger'} d-flex justify-content-between align-items-center mb-4` },
|
|
226
|
+
react_1.default.createElement("div", { className: "d-flex align-items-center gap-2" },
|
|
227
|
+
overallStatus === 'pass' && react_1.default.createElement("i", { className: "bi bi-check-circle-fill fs-5" }),
|
|
228
|
+
overallStatus === 'warn' && react_1.default.createElement("i", { className: "bi bi-exclamation-triangle-fill fs-5" }),
|
|
229
|
+
overallStatus === 'fail' && react_1.default.createElement("i", { className: "bi bi-x-circle-fill fs-5" }),
|
|
230
|
+
react_1.default.createElement("div", null,
|
|
231
|
+
overallStatus === 'pass' && react_1.default.createElement("strong", null, "All checks passed."),
|
|
232
|
+
overallStatus === 'warn' && react_1.default.createElement("strong", null,
|
|
233
|
+
summary.warnings,
|
|
234
|
+
" warning",
|
|
235
|
+
summary.warnings > 1 ? 's' : '',
|
|
236
|
+
" found."),
|
|
237
|
+
overallStatus === 'fail' && react_1.default.createElement("strong", null,
|
|
238
|
+
summary.issues,
|
|
239
|
+
" issue",
|
|
240
|
+
summary.issues > 1 ? 's' : '',
|
|
241
|
+
" found",
|
|
242
|
+
summary.warnings > 0 ? `, ${summary.warnings} warning${summary.warnings > 1 ? 's' : ''}` : '',
|
|
243
|
+
"."),
|
|
244
|
+
' ',
|
|
245
|
+
react_1.default.createElement("span", { className: "opacity-75" },
|
|
246
|
+
summary.passes,
|
|
247
|
+
" passed \u00B7 ",
|
|
248
|
+
summary.issues,
|
|
249
|
+
" failed \u00B7 ",
|
|
250
|
+
summary.warnings,
|
|
251
|
+
" warnings \u00B7 ",
|
|
252
|
+
summary.skips,
|
|
253
|
+
" skipped"))),
|
|
254
|
+
react_1.default.createElement("button", { className: "btn btn-sm btn-dark d-flex align-items-center gap-1", onClick: handleAskAssistant },
|
|
255
|
+
react_1.default.createElement("i", { className: "bi bi-chat-dots-fill" }),
|
|
256
|
+
"Ask AI Assistant"))),
|
|
257
|
+
running && placeholders.length > 0 && (react_1.default.createElement("div", { className: "card mb-3" },
|
|
258
|
+
react_1.default.createElement("div", { className: "card-body p-2" }, placeholders.map(p => (react_1.default.createElement("div", { key: p.id, className: "d-flex align-items-center gap-2 p-2 border-bottom" },
|
|
259
|
+
STATUS_ICON[p.status],
|
|
260
|
+
react_1.default.createElement("span", { className: "text-muted small" }, p.label))))))),
|
|
261
|
+
LAYER_ORDER.map(layer => {
|
|
262
|
+
const layerChecks = groups[layer];
|
|
263
|
+
if (!layerChecks || layerChecks.length === 0)
|
|
264
|
+
return null;
|
|
265
|
+
const layerStatus = layerChecks.some(c => c.status === 'fail')
|
|
266
|
+
? 'fail'
|
|
267
|
+
: layerChecks.some(c => c.status === 'warn')
|
|
268
|
+
? 'warn'
|
|
269
|
+
: layerChecks.some(c => c.status === 'running')
|
|
270
|
+
? 'running'
|
|
271
|
+
: layerChecks.every(c => c.status === 'skip')
|
|
272
|
+
? 'skip'
|
|
273
|
+
: 'pass';
|
|
274
|
+
return (react_1.default.createElement("div", { className: "card mb-3", key: layer },
|
|
275
|
+
react_1.default.createElement("div", { className: "card-header d-flex align-items-center justify-content-between py-2" },
|
|
276
|
+
react_1.default.createElement("div", { className: "d-flex align-items-center gap-2" },
|
|
277
|
+
STATUS_ICON[layerStatus],
|
|
278
|
+
react_1.default.createElement("strong", { className: "small" }, layer)),
|
|
279
|
+
react_1.default.createElement("span", { className: `badge ${STATUS_BADGE_CLASS[layerStatus]}` }, layerStatus === 'running' ? 'running' : layerStatus)),
|
|
280
|
+
react_1.default.createElement("div", { className: "list-group list-group-flush" }, layerChecks.map(check => (react_1.default.createElement("div", { key: check.id, className: "list-group-item p-0" },
|
|
281
|
+
react_1.default.createElement("div", { className: `d-flex align-items-start gap-2 px-3 py-2 ${check.suggestion ? 'cursor-pointer' : ''}`, style: { cursor: check.suggestion ? 'pointer' : 'default' }, onClick: () => check.suggestion && toggleExpand(check.id), role: check.suggestion ? 'button' : undefined },
|
|
282
|
+
react_1.default.createElement("div", { className: "pt-1 flex-shrink-0" }, STATUS_ICON[check.status]),
|
|
283
|
+
react_1.default.createElement("div", { className: "flex-grow-1 min-width-0" },
|
|
284
|
+
react_1.default.createElement("div", { className: "d-flex justify-content-between align-items-start" },
|
|
285
|
+
react_1.default.createElement("span", { className: "small fw-medium" }, check.label),
|
|
286
|
+
check.suggestion && (react_1.default.createElement("i", { className: `bi bi-chevron-${expanded.has(check.id) ? 'up' : 'down'} text-muted ms-2 flex-shrink-0 small` }))),
|
|
287
|
+
react_1.default.createElement("div", { className: "small text-muted mt-1" }, check.detail))),
|
|
288
|
+
check.suggestion && expanded.has(check.id) && (react_1.default.createElement("div", { className: "px-3 pb-2 pt-0 ms-4" },
|
|
289
|
+
react_1.default.createElement("div", { className: "alert alert-info py-2 px-3 mb-0 small d-flex gap-2" },
|
|
290
|
+
react_1.default.createElement("i", { className: "bi bi-lightbulb-fill text-info flex-shrink-0 mt-1" }),
|
|
291
|
+
react_1.default.createElement("span", null, check.suggestion))))))))));
|
|
292
|
+
}),
|
|
293
|
+
done && checks.length > 0 && (react_1.default.createElement("div", { className: "text-center py-3" },
|
|
294
|
+
react_1.default.createElement("button", { className: "btn btn-outline-primary", onClick: handleAskAssistant },
|
|
295
|
+
react_1.default.createElement("i", { className: "bi bi-chat-dots-fill me-2" }),
|
|
296
|
+
"Ask AI Assistant for Help"),
|
|
297
|
+
react_1.default.createElement("p", { className: "text-muted small mt-2 mb-0" }, "Sends the full diagnostic report to the AI assistant for guided troubleshooting.")))));
|
|
298
|
+
}
|
|
@@ -20,3 +20,4 @@ __exportStar(require("./network-viewer"), exports);
|
|
|
20
20
|
__exportStar(require("./device-details-modal"), exports);
|
|
21
21
|
__exportStar(require("./cpu-usage-graph"), exports);
|
|
22
22
|
__exportStar(require("./usage-graph"), exports);
|
|
23
|
+
__exportStar(require("./connection-troubleshooter"), exports);
|
|
@@ -43,6 +43,7 @@ const globals_1 = require("../../globals");
|
|
|
43
43
|
const device_details_modal_1 = require("./device-details-modal");
|
|
44
44
|
const group_details_modal_1 = require("./group-details-modal");
|
|
45
45
|
const usage_graph_1 = require("./usage-graph");
|
|
46
|
+
const connection_troubleshooter_1 = require("./connection-troubleshooter");
|
|
46
47
|
/** Format bytes to human-readable string (KB, MB, GB) */
|
|
47
48
|
function formatBytes(bytes) {
|
|
48
49
|
if (bytes < 1024)
|
|
@@ -74,6 +75,7 @@ function NetworkViewerList() {
|
|
|
74
75
|
const [selectedDeviceId, setSelectedDeviceId] = (0, react_1.useState)(null);
|
|
75
76
|
const [selectedGroupId, setSelectedGroupId] = (0, react_1.useState)(null);
|
|
76
77
|
const [connecting, setConnecting] = (0, react_1.useState)(new Set());
|
|
78
|
+
const [showTroubleshooter, setShowTroubleshooter] = (0, react_1.useState)(false);
|
|
77
79
|
const loadData = async () => {
|
|
78
80
|
try {
|
|
79
81
|
if (!globals_1.isDesktop)
|
|
@@ -198,12 +200,19 @@ function NetworkViewerList() {
|
|
|
198
200
|
return (react_1.default.createElement("div", { className: "container-fluid p-4" },
|
|
199
201
|
react_1.default.createElement("div", { className: "alert alert-warning" }, "Unable to load network information.")));
|
|
200
202
|
}
|
|
203
|
+
if (showTroubleshooter) {
|
|
204
|
+
return react_1.default.createElement(connection_troubleshooter_1.ConnectionTroubleshooter, { onBack: () => setShowTroubleshooter(false) });
|
|
205
|
+
}
|
|
201
206
|
return (react_1.default.createElement("div", { className: "container-fluid p-4" },
|
|
202
207
|
react_1.default.createElement("div", { className: "d-flex justify-content-between align-items-center mb-4" },
|
|
203
208
|
react_1.default.createElement("h2", null, "Network Viewer"),
|
|
204
|
-
react_1.default.createElement("
|
|
205
|
-
react_1.default.createElement("
|
|
206
|
-
|
|
209
|
+
react_1.default.createElement("div", { className: "d-flex gap-2" },
|
|
210
|
+
react_1.default.createElement("button", { className: "btn btn-sm btn-outline-warning", onClick: () => setShowTroubleshooter(true), title: "Run connection diagnostics" },
|
|
211
|
+
react_1.default.createElement("i", { className: "bi bi-stethoscope me-1" }),
|
|
212
|
+
"Troubleshoot"),
|
|
213
|
+
react_1.default.createElement("button", { className: "btn btn-sm btn-primary", onClick: handleRefresh, disabled: refreshing },
|
|
214
|
+
react_1.default.createElement("i", { className: `bi bi-arrow-clockwise ${refreshing ? 'spin' : ''}` }),
|
|
215
|
+
refreshing ? ' Refreshing...' : ' Refresh'))),
|
|
207
216
|
react_1.default.createElement("div", { className: "card mb-2" },
|
|
208
217
|
react_1.default.createElement("div", { className: "card-body" },
|
|
209
218
|
react_1.default.createElement("h5", { className: "card-title" }, "Local Device"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peers-app/peers-ui",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/peers-app/peers-ui.git"
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"test:coverage": "jest --coverage"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
|
-
"@peers-app/peers-sdk": "^0.12.
|
|
29
|
+
"@peers-app/peers-sdk": "^0.12.2",
|
|
30
30
|
"bootstrap": "^5.3.3",
|
|
31
31
|
"react": "^18.0.0",
|
|
32
32
|
"react-dom": "^18.0.0"
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"@babel/preset-env": "^7.24.5",
|
|
37
37
|
"@babel/preset-react": "^7.24.1",
|
|
38
38
|
"@babel/preset-typescript": "^7.27.1",
|
|
39
|
-
"@peers-app/peers-sdk": "0.12.
|
|
39
|
+
"@peers-app/peers-sdk": "0.12.2",
|
|
40
40
|
"@testing-library/dom": "^10.4.0",
|
|
41
41
|
"@testing-library/jest-dom": "^6.6.3",
|
|
42
42
|
"@testing-library/react": "^16.3.0",
|
|
@@ -82,6 +82,7 @@ export const ChatOverlay: React.FC<ChatOverlayProps> = ({
|
|
|
82
82
|
const [voiceSettings, setVoiceSettings] = useState<VoiceSettingsData>(DEFAULT_VOICE_SETTINGS);
|
|
83
83
|
const [overlayPosition, setOverlayPosition] = useState<OverlayPosition>(DEFAULT_POSITION);
|
|
84
84
|
const [isDragging, setIsDragging] = useState(false);
|
|
85
|
+
const [pendingInitialMessage, setPendingInitialMessage] = useState<string | undefined>(undefined);
|
|
85
86
|
|
|
86
87
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
87
88
|
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
|
@@ -201,6 +202,14 @@ export const ChatOverlay: React.FC<ChatOverlayProps> = ({
|
|
|
201
202
|
// Subscribe to voice events
|
|
202
203
|
useEffect(() => {
|
|
203
204
|
const subscriptions = [
|
|
205
|
+
subscribe('chat:openWithMessage', (event: any) => {
|
|
206
|
+
const { message } = event.data || {};
|
|
207
|
+
if (message) {
|
|
208
|
+
setPendingInitialMessage(message);
|
|
209
|
+
}
|
|
210
|
+
setIsExpanded(true);
|
|
211
|
+
setShowSettings(false);
|
|
212
|
+
}),
|
|
204
213
|
subscribe('voice:stateChanged', (event: any) => {
|
|
205
214
|
setVoiceState(event.data.state);
|
|
206
215
|
if (event.data.state === 'idle') {
|
|
@@ -385,6 +394,9 @@ export const ChatOverlay: React.FC<ChatOverlayProps> = ({
|
|
|
385
394
|
const handleMessageSubmit = useCallback(async (message: IMessage) => {
|
|
386
395
|
// Notify voice service of text activity (resets inactivity timer)
|
|
387
396
|
rpcServerCalls.voiceNotifyTextActivity?.().catch(() => {});
|
|
397
|
+
|
|
398
|
+
// Clear any pending initial message that was pre-filled
|
|
399
|
+
setPendingInitialMessage(undefined);
|
|
388
400
|
|
|
389
401
|
// If no thread yet, this message becomes the thread root
|
|
390
402
|
if (!threadId) {
|
|
@@ -646,6 +658,8 @@ export const ChatOverlay: React.FC<ChatOverlayProps> = ({
|
|
|
646
658
|
channelId={me?.userId || 'default'}
|
|
647
659
|
threadId={threadId || newid()}
|
|
648
660
|
onMessageSubmit={handleMessageSubmit}
|
|
661
|
+
initialMessage={pendingInitialMessage}
|
|
662
|
+
key={pendingInitialMessage ? `pre-${pendingInitialMessage.slice(0, 20)}` : 'default'}
|
|
649
663
|
/>
|
|
650
664
|
</div>
|
|
651
665
|
)}
|
|
@@ -173,14 +173,13 @@ export function MarkdownPlugin(props: IProps) {
|
|
|
173
173
|
});
|
|
174
174
|
|
|
175
175
|
const sub2Dispose = editor.registerUpdateListener(({ editorState }) => {
|
|
176
|
-
|
|
176
|
+
editorState.read(() => {
|
|
177
177
|
const newMarkdown = $convertToMarkdownString(customMarkdownTransformers, undefined, true);
|
|
178
178
|
if (newMarkdown !== markdown) {
|
|
179
179
|
markdown = newMarkdown;
|
|
180
180
|
markdownObs(markdown);
|
|
181
181
|
}
|
|
182
182
|
});
|
|
183
|
-
|
|
184
183
|
});
|
|
185
184
|
|
|
186
185
|
return () => {
|
|
@@ -32,10 +32,11 @@ interface IProps {
|
|
|
32
32
|
channelId: string
|
|
33
33
|
threadId?: string
|
|
34
34
|
onMessageSubmit: (message: IMessage, files?: IFile[]) => any
|
|
35
|
+
initialMessage?: string
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export const MessageCompose = (props: IProps) => {
|
|
38
|
-
const { channelId, threadId, onMessageSubmit } = props;
|
|
39
|
+
const { channelId, threadId, onMessageSubmit, initialMessage } = props;
|
|
39
40
|
|
|
40
41
|
const [contentObs] = useState(() => observable(''));
|
|
41
42
|
const [mdEffects] = useState<IEditorEffects>(() => ({}));
|
|
@@ -52,7 +53,8 @@ export const MessageCompose = (props: IProps) => {
|
|
|
52
53
|
let sub: Subscription | undefined;
|
|
53
54
|
getContentPersistenceValue(channelId, threadId).loadingPromise.then(pVar => {
|
|
54
55
|
if (!disposed) {
|
|
55
|
-
|
|
56
|
+
const persistedValue = pVar() ?? '';
|
|
57
|
+
contentObs(initialMessage ?? persistedValue);
|
|
56
58
|
sub = contentObs.subscribe(() => pVar(contentObs()));
|
|
57
59
|
setTimeout(() => scrollToBottom(), 100);
|
|
58
60
|
}
|
package/src/index.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * from "./screens/events/cron";
|
|
2
2
|
|
|
3
3
|
export * from "./tabs-layout/tabs-layout";
|
|
4
|
-
|
|
4
|
+
export { activeTabId, activeTabs, TabState } from "./tabs-layout/tabs-state";
|
|
5
5
|
export * from "./components/voice-indicator";
|
|
6
6
|
export * from "./components/chat-overlay";
|
|
7
7
|
export * from "./components/sortable-list";
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { emit } from '@peers-app/peers-sdk';
|
|
3
|
+
|
|
4
|
+
export interface IDiagnosticCheck {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
layer: string;
|
|
8
|
+
status: 'pass' | 'fail' | 'warn' | 'skip' | 'running';
|
|
9
|
+
detail: string;
|
|
10
|
+
suggestion?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
onBack: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const STATUS_ICON: Record<IDiagnosticCheck['status'], React.ReactNode> = {
|
|
18
|
+
pass: <i className="bi bi-check-circle-fill text-success" />,
|
|
19
|
+
fail: <i className="bi bi-x-circle-fill text-danger" />,
|
|
20
|
+
warn: <i className="bi bi-exclamation-triangle-fill text-warning" />,
|
|
21
|
+
skip: <i className="bi bi-dash-circle text-muted" />,
|
|
22
|
+
running: (
|
|
23
|
+
<span className="spinner-border spinner-border-sm text-primary" role="status" aria-hidden="true" />
|
|
24
|
+
),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const STATUS_BADGE_CLASS: Record<IDiagnosticCheck['status'], string> = {
|
|
28
|
+
pass: 'bg-success',
|
|
29
|
+
fail: 'bg-danger',
|
|
30
|
+
warn: 'bg-warning text-dark',
|
|
31
|
+
skip: 'bg-secondary',
|
|
32
|
+
running: 'bg-primary',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const LAYER_ORDER = [
|
|
36
|
+
'Local System',
|
|
37
|
+
'Cloud Relay',
|
|
38
|
+
'Local Network',
|
|
39
|
+
'Protocol Support',
|
|
40
|
+
'WebRTC / TURN',
|
|
41
|
+
'Peer State',
|
|
42
|
+
'Active Connections',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
function groupByLayer(checks: IDiagnosticCheck[]): Record<string, IDiagnosticCheck[]> {
|
|
46
|
+
const groups: Record<string, IDiagnosticCheck[]> = {};
|
|
47
|
+
for (const check of checks) {
|
|
48
|
+
if (!groups[check.layer]) groups[check.layer] = [];
|
|
49
|
+
groups[check.layer].push(check);
|
|
50
|
+
}
|
|
51
|
+
return groups;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getSummary(checks: IDiagnosticCheck[]): { issues: number; warnings: number; passes: number; skips: number } {
|
|
55
|
+
return {
|
|
56
|
+
issues: checks.filter(c => c.status === 'fail').length,
|
|
57
|
+
warnings: checks.filter(c => c.status === 'warn').length,
|
|
58
|
+
passes: checks.filter(c => c.status === 'pass').length,
|
|
59
|
+
skips: checks.filter(c => c.status === 'skip').length,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function formatReportForAI(checks: IDiagnosticCheck[]): string {
|
|
64
|
+
const lines: string[] = ['Connection Troubleshooter Report', '================================'];
|
|
65
|
+
const groups = groupByLayer(checks);
|
|
66
|
+
|
|
67
|
+
for (const layer of LAYER_ORDER) {
|
|
68
|
+
const layerChecks = groups[layer];
|
|
69
|
+
if (!layerChecks) continue;
|
|
70
|
+
lines.push(`\n[${layer}]`);
|
|
71
|
+
for (const check of layerChecks) {
|
|
72
|
+
const statusStr = check.status.toUpperCase().padEnd(7);
|
|
73
|
+
lines.push(` ${statusStr} ${check.label}: ${check.detail}`);
|
|
74
|
+
if (check.suggestion) {
|
|
75
|
+
lines.push(` Suggestion: ${check.suggestion}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const { issues, warnings, skips } = getSummary(checks);
|
|
81
|
+
lines.push(`\nSummary: ${issues} issue(s), ${warnings} warning(s), ${skips} skipped.`);
|
|
82
|
+
|
|
83
|
+
return lines.join('\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function ConnectionTroubleshooter({ onBack }: Props) {
|
|
87
|
+
const [checks, setChecks] = useState<IDiagnosticCheck[]>([]);
|
|
88
|
+
const [running, setRunning] = useState(false);
|
|
89
|
+
const [done, setDone] = useState(false);
|
|
90
|
+
const [expanded, setExpanded] = useState<Set<string>>(new Set());
|
|
91
|
+
const hasRun = useRef(false);
|
|
92
|
+
|
|
93
|
+
const runDiagnostics = async () => {
|
|
94
|
+
setRunning(true);
|
|
95
|
+
setDone(false);
|
|
96
|
+
setChecks([]);
|
|
97
|
+
setExpanded(new Set());
|
|
98
|
+
|
|
99
|
+
const api = (window as any).electronAPI?.networkViewer;
|
|
100
|
+
if (!api) {
|
|
101
|
+
setChecks([{
|
|
102
|
+
id: 'no-api',
|
|
103
|
+
label: 'Electron API unavailable',
|
|
104
|
+
layer: 'Local System',
|
|
105
|
+
status: 'fail',
|
|
106
|
+
detail: 'The Electron network API is not available. This tool only works in the desktop app.',
|
|
107
|
+
}]);
|
|
108
|
+
setRunning(false);
|
|
109
|
+
setDone(true);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
// Stream results by running all checks on the backend and updating as they come in.
|
|
115
|
+
// Show a placeholder "running" row immediately while the full batch executes.
|
|
116
|
+
const placeholders: IDiagnosticCheck[] = LAYER_ORDER.map(layer => ({
|
|
117
|
+
id: `placeholder-${layer}`,
|
|
118
|
+
label: `Checking ${layer}…`,
|
|
119
|
+
layer,
|
|
120
|
+
status: 'running',
|
|
121
|
+
detail: '',
|
|
122
|
+
}));
|
|
123
|
+
setChecks(placeholders);
|
|
124
|
+
|
|
125
|
+
const results: IDiagnosticCheck[] = await api.runDiagnostics();
|
|
126
|
+
setChecks(results);
|
|
127
|
+
|
|
128
|
+
// Auto-expand all failed/warned checks so they're visible immediately
|
|
129
|
+
const toExpand = new Set<string>();
|
|
130
|
+
for (const r of results) {
|
|
131
|
+
if ((r.status === 'fail' || r.status === 'warn') && r.suggestion) {
|
|
132
|
+
toExpand.add(r.id);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
setExpanded(toExpand);
|
|
136
|
+
} catch (err: any) {
|
|
137
|
+
setChecks([{
|
|
138
|
+
id: 'run-error',
|
|
139
|
+
label: 'Diagnostics failed to run',
|
|
140
|
+
layer: 'Local System',
|
|
141
|
+
status: 'fail',
|
|
142
|
+
detail: `Error: ${err?.message || 'Unknown error'}`,
|
|
143
|
+
suggestion: 'Restart the application and try again.',
|
|
144
|
+
}]);
|
|
145
|
+
} finally {
|
|
146
|
+
setRunning(false);
|
|
147
|
+
setDone(true);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (!hasRun.current) {
|
|
153
|
+
hasRun.current = true;
|
|
154
|
+
runDiagnostics();
|
|
155
|
+
}
|
|
156
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
157
|
+
}, []);
|
|
158
|
+
|
|
159
|
+
const toggleExpand = (id: string) => {
|
|
160
|
+
setExpanded(prev => {
|
|
161
|
+
const next = new Set(prev);
|
|
162
|
+
if (next.has(id)) next.delete(id);
|
|
163
|
+
else next.add(id);
|
|
164
|
+
return next;
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const handleAskAssistant = () => {
|
|
169
|
+
const report = formatReportForAI(checks.filter(c => c.status !== 'running'));
|
|
170
|
+
const message = `I ran the Peers connection troubleshooter and got the following results. Please help me understand what's wrong and how to fix it:\n\n${report}`;
|
|
171
|
+
emit({ name: 'chat:openWithMessage', data: { message } }).catch(() => {
|
|
172
|
+
// Fallback: copy to clipboard
|
|
173
|
+
navigator.clipboard.writeText(message).then(() => {
|
|
174
|
+
alert('Diagnostic report copied to clipboard. Paste it into the AI assistant chat.');
|
|
175
|
+
}).catch(() => {
|
|
176
|
+
alert('Could not open AI assistant. Please open the chat manually and paste the diagnostic report.');
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const groups = groupByLayer(checks.filter(c => !c.id.startsWith('placeholder-')));
|
|
182
|
+
const placeholders = checks.filter(c => c.id.startsWith('placeholder-'));
|
|
183
|
+
const summary = getSummary(checks);
|
|
184
|
+
const completedCount = checks.filter(c => c.status !== 'running').length;
|
|
185
|
+
const totalCount = checks.length;
|
|
186
|
+
const progressPct = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0;
|
|
187
|
+
|
|
188
|
+
const overallStatus: IDiagnosticCheck['status'] = !done
|
|
189
|
+
? 'running'
|
|
190
|
+
: summary.issues > 0
|
|
191
|
+
? 'fail'
|
|
192
|
+
: summary.warnings > 0
|
|
193
|
+
? 'warn'
|
|
194
|
+
: 'pass';
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<div className="container-fluid p-4">
|
|
198
|
+
{/* Header */}
|
|
199
|
+
<div className="d-flex justify-content-between align-items-center mb-3">
|
|
200
|
+
<div className="d-flex align-items-center gap-3">
|
|
201
|
+
<button className="btn btn-sm btn-outline-secondary" onClick={onBack}>
|
|
202
|
+
<i className="bi bi-arrow-left me-1" />
|
|
203
|
+
Back
|
|
204
|
+
</button>
|
|
205
|
+
<h4 className="mb-0">
|
|
206
|
+
<i className="bi bi-stethoscope me-2 text-primary" />
|
|
207
|
+
Connection Troubleshooter
|
|
208
|
+
</h4>
|
|
209
|
+
</div>
|
|
210
|
+
<button
|
|
211
|
+
className="btn btn-sm btn-primary"
|
|
212
|
+
onClick={runDiagnostics}
|
|
213
|
+
disabled={running}
|
|
214
|
+
>
|
|
215
|
+
{running ? (
|
|
216
|
+
<>
|
|
217
|
+
<span className="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true" />
|
|
218
|
+
Running…
|
|
219
|
+
</>
|
|
220
|
+
) : (
|
|
221
|
+
<>
|
|
222
|
+
<i className="bi bi-arrow-clockwise me-1" />
|
|
223
|
+
Re-run
|
|
224
|
+
</>
|
|
225
|
+
)}
|
|
226
|
+
</button>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
{/* Progress bar */}
|
|
230
|
+
{running && (
|
|
231
|
+
<div className="mb-4">
|
|
232
|
+
<div className="d-flex justify-content-between mb-1">
|
|
233
|
+
<small className="text-muted">Running diagnostics…</small>
|
|
234
|
+
<small className="text-muted">{completedCount} / {totalCount}</small>
|
|
235
|
+
</div>
|
|
236
|
+
<div className="progress" style={{ height: '6px' }}>
|
|
237
|
+
<div
|
|
238
|
+
className="progress-bar progress-bar-striped progress-bar-animated"
|
|
239
|
+
style={{ width: `${progressPct}%` }}
|
|
240
|
+
/>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
)}
|
|
244
|
+
|
|
245
|
+
{/* Summary card — shown when done */}
|
|
246
|
+
{done && checks.length > 0 && (
|
|
247
|
+
<div className={`alert ${overallStatus === 'pass' ? 'alert-success' : overallStatus === 'warn' ? 'alert-warning' : 'alert-danger'} d-flex justify-content-between align-items-center mb-4`}>
|
|
248
|
+
<div className="d-flex align-items-center gap-2">
|
|
249
|
+
{overallStatus === 'pass' && <i className="bi bi-check-circle-fill fs-5" />}
|
|
250
|
+
{overallStatus === 'warn' && <i className="bi bi-exclamation-triangle-fill fs-5" />}
|
|
251
|
+
{overallStatus === 'fail' && <i className="bi bi-x-circle-fill fs-5" />}
|
|
252
|
+
<div>
|
|
253
|
+
{overallStatus === 'pass' && <strong>All checks passed.</strong>}
|
|
254
|
+
{overallStatus === 'warn' && <strong>{summary.warnings} warning{summary.warnings > 1 ? 's' : ''} found.</strong>}
|
|
255
|
+
{overallStatus === 'fail' && <strong>{summary.issues} issue{summary.issues > 1 ? 's' : ''} found{summary.warnings > 0 ? `, ${summary.warnings} warning${summary.warnings > 1 ? 's' : ''}` : ''}.</strong>}
|
|
256
|
+
{' '}
|
|
257
|
+
<span className="opacity-75">
|
|
258
|
+
{summary.passes} passed · {summary.issues} failed · {summary.warnings} warnings · {summary.skips} skipped
|
|
259
|
+
</span>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
<button className="btn btn-sm btn-dark d-flex align-items-center gap-1" onClick={handleAskAssistant}>
|
|
263
|
+
<i className="bi bi-chat-dots-fill" />
|
|
264
|
+
Ask AI Assistant
|
|
265
|
+
</button>
|
|
266
|
+
</div>
|
|
267
|
+
)}
|
|
268
|
+
|
|
269
|
+
{/* Placeholder rows while waiting for full results */}
|
|
270
|
+
{running && placeholders.length > 0 && (
|
|
271
|
+
<div className="card mb-3">
|
|
272
|
+
<div className="card-body p-2">
|
|
273
|
+
{placeholders.map(p => (
|
|
274
|
+
<div key={p.id} className="d-flex align-items-center gap-2 p-2 border-bottom">
|
|
275
|
+
{STATUS_ICON[p.status]}
|
|
276
|
+
<span className="text-muted small">{p.label}</span>
|
|
277
|
+
</div>
|
|
278
|
+
))}
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
|
|
283
|
+
{/* Results grouped by layer */}
|
|
284
|
+
{LAYER_ORDER.map(layer => {
|
|
285
|
+
const layerChecks = groups[layer];
|
|
286
|
+
if (!layerChecks || layerChecks.length === 0) return null;
|
|
287
|
+
|
|
288
|
+
const layerStatus: IDiagnosticCheck['status'] = layerChecks.some(c => c.status === 'fail')
|
|
289
|
+
? 'fail'
|
|
290
|
+
: layerChecks.some(c => c.status === 'warn')
|
|
291
|
+
? 'warn'
|
|
292
|
+
: layerChecks.some(c => c.status === 'running')
|
|
293
|
+
? 'running'
|
|
294
|
+
: layerChecks.every(c => c.status === 'skip')
|
|
295
|
+
? 'skip'
|
|
296
|
+
: 'pass';
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<div className="card mb-3" key={layer}>
|
|
300
|
+
<div className="card-header d-flex align-items-center justify-content-between py-2">
|
|
301
|
+
<div className="d-flex align-items-center gap-2">
|
|
302
|
+
{STATUS_ICON[layerStatus]}
|
|
303
|
+
<strong className="small">{layer}</strong>
|
|
304
|
+
</div>
|
|
305
|
+
<span className={`badge ${STATUS_BADGE_CLASS[layerStatus]}`}>
|
|
306
|
+
{layerStatus === 'running' ? 'running' : layerStatus}
|
|
307
|
+
</span>
|
|
308
|
+
</div>
|
|
309
|
+
<div className="list-group list-group-flush">
|
|
310
|
+
{layerChecks.map(check => (
|
|
311
|
+
<div key={check.id} className="list-group-item p-0">
|
|
312
|
+
<div
|
|
313
|
+
className={`d-flex align-items-start gap-2 px-3 py-2 ${check.suggestion ? 'cursor-pointer' : ''}`}
|
|
314
|
+
style={{ cursor: check.suggestion ? 'pointer' : 'default' }}
|
|
315
|
+
onClick={() => check.suggestion && toggleExpand(check.id)}
|
|
316
|
+
role={check.suggestion ? 'button' : undefined}
|
|
317
|
+
>
|
|
318
|
+
<div className="pt-1 flex-shrink-0">{STATUS_ICON[check.status]}</div>
|
|
319
|
+
<div className="flex-grow-1 min-width-0">
|
|
320
|
+
<div className="d-flex justify-content-between align-items-start">
|
|
321
|
+
<span className="small fw-medium">{check.label}</span>
|
|
322
|
+
{check.suggestion && (
|
|
323
|
+
<i className={`bi bi-chevron-${expanded.has(check.id) ? 'up' : 'down'} text-muted ms-2 flex-shrink-0 small`} />
|
|
324
|
+
)}
|
|
325
|
+
</div>
|
|
326
|
+
<div className="small text-muted mt-1">{check.detail}</div>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
{check.suggestion && expanded.has(check.id) && (
|
|
330
|
+
<div className="px-3 pb-2 pt-0 ms-4">
|
|
331
|
+
<div className="alert alert-info py-2 px-3 mb-0 small d-flex gap-2">
|
|
332
|
+
<i className="bi bi-lightbulb-fill text-info flex-shrink-0 mt-1" />
|
|
333
|
+
<span>{check.suggestion}</span>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
)}
|
|
337
|
+
</div>
|
|
338
|
+
))}
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
);
|
|
342
|
+
})}
|
|
343
|
+
|
|
344
|
+
{/* Ask AI Assistant button at bottom (when done, as a secondary option) */}
|
|
345
|
+
{done && checks.length > 0 && (
|
|
346
|
+
<div className="text-center py-3">
|
|
347
|
+
<button className="btn btn-outline-primary" onClick={handleAskAssistant}>
|
|
348
|
+
<i className="bi bi-chat-dots-fill me-2" />
|
|
349
|
+
Ask AI Assistant for Help
|
|
350
|
+
</button>
|
|
351
|
+
<p className="text-muted small mt-2 mb-0">
|
|
352
|
+
Sends the full diagnostic report to the AI assistant for guided troubleshooting.
|
|
353
|
+
</p>
|
|
354
|
+
</div>
|
|
355
|
+
)}
|
|
356
|
+
</div>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
@@ -7,6 +7,7 @@ import { isDesktop } from '../../globals';
|
|
|
7
7
|
import { DeviceDetailsModal } from './device-details-modal';
|
|
8
8
|
import { GroupDetailsModal } from './group-details-modal';
|
|
9
9
|
import { UsageGraph } from './usage-graph';
|
|
10
|
+
import { ConnectionTroubleshooter } from './connection-troubleshooter';
|
|
10
11
|
|
|
11
12
|
/** Format bytes to human-readable string (KB, MB, GB) */
|
|
12
13
|
function formatBytes(bytes: number): string {
|
|
@@ -107,6 +108,7 @@ export function NetworkViewerList() {
|
|
|
107
108
|
const [selectedDeviceId, setSelectedDeviceId] = useState<string | null>(null);
|
|
108
109
|
const [selectedGroupId, setSelectedGroupId] = useState<string | null>(null);
|
|
109
110
|
const [connecting, setConnecting] = useState<Set<string>>(new Set());
|
|
111
|
+
const [showTroubleshooter, setShowTroubleshooter] = useState(false);
|
|
110
112
|
|
|
111
113
|
const loadData = async () => {
|
|
112
114
|
try {
|
|
@@ -244,18 +246,32 @@ export function NetworkViewerList() {
|
|
|
244
246
|
);
|
|
245
247
|
}
|
|
246
248
|
|
|
249
|
+
if (showTroubleshooter) {
|
|
250
|
+
return <ConnectionTroubleshooter onBack={() => setShowTroubleshooter(false)} />;
|
|
251
|
+
}
|
|
252
|
+
|
|
247
253
|
return (
|
|
248
254
|
<div className="container-fluid p-4">
|
|
249
255
|
<div className="d-flex justify-content-between align-items-center mb-4">
|
|
250
256
|
<h2>Network Viewer</h2>
|
|
251
|
-
<
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
257
|
+
<div className="d-flex gap-2">
|
|
258
|
+
<button
|
|
259
|
+
className="btn btn-sm btn-outline-warning"
|
|
260
|
+
onClick={() => setShowTroubleshooter(true)}
|
|
261
|
+
title="Run connection diagnostics"
|
|
262
|
+
>
|
|
263
|
+
<i className="bi bi-stethoscope me-1"></i>
|
|
264
|
+
Troubleshoot
|
|
265
|
+
</button>
|
|
266
|
+
<button
|
|
267
|
+
className="btn btn-sm btn-primary"
|
|
268
|
+
onClick={handleRefresh}
|
|
269
|
+
disabled={refreshing}
|
|
270
|
+
>
|
|
271
|
+
<i className={`bi bi-arrow-clockwise ${refreshing ? 'spin' : ''}`}></i>
|
|
272
|
+
{refreshing ? ' Refreshing...' : ' Refresh'}
|
|
273
|
+
</button>
|
|
274
|
+
</div>
|
|
259
275
|
</div>
|
|
260
276
|
|
|
261
277
|
{/* Device Identifiers */}
|