hale-commenting-system 1.0.2 → 1.0.4
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/cli/dist/index.js +12 -11
- package/cli/dist/index.js.map +1 -1
- package/dist/index.d.mts +151 -52
- package/dist/index.d.ts +151 -52
- package/dist/index.js +2171 -68
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2200 -68
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -43,124 +53,2217 @@ __export(index_exports, {
|
|
|
43
53
|
module.exports = __toCommonJS(index_exports);
|
|
44
54
|
|
|
45
55
|
// src/components/CommentOverlay.tsx
|
|
46
|
-
var
|
|
47
|
-
var
|
|
48
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: "Comment Overlay Placeholder" }) });
|
|
49
|
-
};
|
|
56
|
+
var React3 = __toESM(require("react"));
|
|
57
|
+
var import_react_router_dom = require("react-router-dom");
|
|
50
58
|
|
|
51
|
-
// src/
|
|
52
|
-
var
|
|
53
|
-
var CommentDrawer = ({ selectedThreadId, onThreadSelect, children }) => {
|
|
54
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { children });
|
|
55
|
-
};
|
|
59
|
+
// src/contexts/CommentContext.tsx
|
|
60
|
+
var React = __toESM(require("react"));
|
|
56
61
|
|
|
57
|
-
// src/
|
|
58
|
-
var
|
|
59
|
-
var
|
|
60
|
-
|
|
62
|
+
// src/services/githubAdapter.ts
|
|
63
|
+
var import_axios = __toESM(require("axios"));
|
|
64
|
+
var getApiBase = () => {
|
|
65
|
+
if (typeof window === "undefined") return "/api/github-api";
|
|
66
|
+
const isNetlify = window.location.hostname.includes("netlify.app");
|
|
67
|
+
const apiBase = isNetlify ? "/.netlify/functions/github-api" : "/api/github-api";
|
|
68
|
+
console.log("\u{1F50D} API detection:", { hostname: window.location.hostname, isNetlify, apiBase });
|
|
69
|
+
return apiBase;
|
|
70
|
+
};
|
|
71
|
+
var getStoredToken = () => {
|
|
72
|
+
if (typeof window === "undefined") return null;
|
|
73
|
+
const token = localStorage.getItem("github_access_token");
|
|
74
|
+
console.log("\u{1F511} getStoredToken:", token ? "Token found" : "No token found");
|
|
75
|
+
return token;
|
|
76
|
+
};
|
|
77
|
+
var getStoredUser = () => {
|
|
78
|
+
if (typeof window === "undefined") return null;
|
|
79
|
+
const userStr = localStorage.getItem("github_user");
|
|
80
|
+
return userStr ? JSON.parse(userStr) : null;
|
|
81
|
+
};
|
|
82
|
+
var storeGitHubAuth = (token, login, avatar) => {
|
|
83
|
+
localStorage.setItem("github_access_token", token);
|
|
84
|
+
localStorage.setItem("github_user", JSON.stringify({ login, avatar }));
|
|
61
85
|
};
|
|
86
|
+
var clearGitHubAuth = () => {
|
|
87
|
+
localStorage.removeItem("github_access_token");
|
|
88
|
+
localStorage.removeItem("github_user");
|
|
89
|
+
};
|
|
90
|
+
var getAuthenticatedUser = () => {
|
|
91
|
+
return getStoredUser();
|
|
92
|
+
};
|
|
93
|
+
var isGitHubConfigured = () => {
|
|
94
|
+
const token = getStoredToken();
|
|
95
|
+
const owner = process.env.VITE_GITHUB_OWNER;
|
|
96
|
+
const repo = process.env.VITE_GITHUB_REPO;
|
|
97
|
+
console.log("\u{1F50D} isGitHubConfigured check:", { hasToken: !!token, owner, repo });
|
|
98
|
+
return !!(token && owner && repo);
|
|
99
|
+
};
|
|
100
|
+
async function makeGitHubRequest(method, endpoint, data) {
|
|
101
|
+
const token = getStoredToken();
|
|
102
|
+
if (!token) {
|
|
103
|
+
throw new Error("Not authenticated with GitHub");
|
|
104
|
+
}
|
|
105
|
+
const response = await import_axios.default.post(getApiBase(), {
|
|
106
|
+
token,
|
|
107
|
+
method,
|
|
108
|
+
endpoint,
|
|
109
|
+
data
|
|
110
|
+
});
|
|
111
|
+
return response.data;
|
|
112
|
+
}
|
|
113
|
+
var githubAdapter = {
|
|
114
|
+
/**
|
|
115
|
+
* Create a new GitHub Issue for a comment thread
|
|
116
|
+
*/
|
|
117
|
+
async createIssue(title, body, route, x, y, version) {
|
|
118
|
+
console.log("\u{1F535} createIssue called", { title, route, x, y, version });
|
|
119
|
+
if (!isGitHubConfigured()) {
|
|
120
|
+
console.warn("\u26A0\uFE0F GitHub not configured. Skipping issue creation.");
|
|
121
|
+
return { success: false, error: "Please sign in with GitHub" };
|
|
122
|
+
}
|
|
123
|
+
const owner = process.env.VITE_GITHUB_OWNER;
|
|
124
|
+
const repo = process.env.VITE_GITHUB_REPO;
|
|
125
|
+
console.log("\u{1F535} GitHub config:", { owner, repo, hasToken: !!getStoredToken() });
|
|
126
|
+
try {
|
|
127
|
+
const metadata = [
|
|
128
|
+
`- Route: \`${route}\``,
|
|
129
|
+
version ? `- Version: \`${version}\`` : null,
|
|
130
|
+
`- Coordinates: \`(${Math.round(x)}, ${Math.round(y)})\``
|
|
131
|
+
].filter(Boolean).join("\n");
|
|
132
|
+
const issueBody = {
|
|
133
|
+
title,
|
|
134
|
+
body: `${body}
|
|
62
135
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
136
|
+
---
|
|
137
|
+
**Metadata:**
|
|
138
|
+
${metadata}`
|
|
139
|
+
};
|
|
140
|
+
console.log("\u{1F535} Calling makeGitHubRequest...");
|
|
141
|
+
const issueData = await makeGitHubRequest("POST", `/repos/${owner}/${repo}/issues`, issueBody);
|
|
142
|
+
console.log("\u2705 Created GitHub Issue #", issueData.number);
|
|
143
|
+
try {
|
|
144
|
+
const labels = [
|
|
145
|
+
"apollo-comment",
|
|
146
|
+
`route:${route}`,
|
|
147
|
+
`coords:${Math.round(x)},${Math.round(y)}`
|
|
148
|
+
];
|
|
149
|
+
if (version) labels.push(`version:${version}`);
|
|
150
|
+
await makeGitHubRequest(
|
|
151
|
+
"POST",
|
|
152
|
+
`/repos/${owner}/${repo}/issues/${issueData.number}/labels`,
|
|
153
|
+
{ labels }
|
|
154
|
+
);
|
|
155
|
+
console.log("\u2705 Added labels to Issue #", issueData.number);
|
|
156
|
+
} catch (labelError) {
|
|
157
|
+
console.warn("\u26A0\uFE0F Could not add labels (labels may not exist in repo)");
|
|
158
|
+
}
|
|
159
|
+
return { success: true, data: issueData };
|
|
160
|
+
} catch (error) {
|
|
161
|
+
const errorMessage = error?.response?.data?.message || error?.message || "Failed to create issue";
|
|
162
|
+
console.error("\u274C Failed to create GitHub Issue:", {
|
|
163
|
+
message: errorMessage,
|
|
164
|
+
error: error?.response?.data || error,
|
|
165
|
+
status: error?.response?.status
|
|
166
|
+
});
|
|
167
|
+
return { success: false, error: errorMessage };
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
/**
|
|
171
|
+
* Add a comment to an existing GitHub Issue
|
|
172
|
+
*/
|
|
173
|
+
async createComment(issueNumber, body) {
|
|
174
|
+
if (!isGitHubConfigured()) {
|
|
175
|
+
return { success: false, error: "Please sign in with GitHub" };
|
|
176
|
+
}
|
|
177
|
+
const owner = process.env.VITE_GITHUB_OWNER;
|
|
178
|
+
const repo = process.env.VITE_GITHUB_REPO;
|
|
179
|
+
try {
|
|
180
|
+
const commentData = await makeGitHubRequest(
|
|
181
|
+
"POST",
|
|
182
|
+
`/repos/${owner}/${repo}/issues/${issueNumber}/comments`,
|
|
183
|
+
{ body }
|
|
184
|
+
);
|
|
185
|
+
console.log(`\u2705 Added comment to Issue #${issueNumber}`);
|
|
186
|
+
return { success: true, data: commentData };
|
|
187
|
+
} catch (error) {
|
|
188
|
+
const errorMessage = error?.response?.data?.message || error?.message || "Failed to create comment";
|
|
189
|
+
console.error(`\u274C Failed to add comment to Issue #${issueNumber}:`, errorMessage);
|
|
190
|
+
return { success: false, error: errorMessage };
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
/**
|
|
194
|
+
* Fetch all issues for a specific route
|
|
195
|
+
*/
|
|
196
|
+
async fetchIssues(route) {
|
|
197
|
+
if (!isGitHubConfigured()) {
|
|
198
|
+
console.warn("GitHub not configured. Skipping issue fetch.");
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
const owner = process.env.VITE_GITHUB_OWNER;
|
|
202
|
+
const repo = process.env.VITE_GITHUB_REPO;
|
|
203
|
+
try {
|
|
204
|
+
const issues = await makeGitHubRequest(
|
|
205
|
+
"GET",
|
|
206
|
+
`/repos/${owner}/${repo}/issues?state=open`
|
|
207
|
+
);
|
|
208
|
+
const filteredIssues = issues.filter((issue) => {
|
|
209
|
+
if (issue.body && issue.body.includes(`Route: \`${route}\``)) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
if (issue.labels && issue.labels.some((l) => {
|
|
213
|
+
const labelName = typeof l === "string" ? l : l.name;
|
|
214
|
+
return labelName === `route:${route}`;
|
|
215
|
+
})) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
return false;
|
|
219
|
+
});
|
|
220
|
+
console.log(`\u2705 Fetched ${filteredIssues.length} issues for route: ${route}`);
|
|
221
|
+
return filteredIssues;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.error(`\u274C Failed to fetch issues for route ${route}:`, error);
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
/**
|
|
228
|
+
* Fetch all comments for a specific issue
|
|
229
|
+
*/
|
|
230
|
+
async fetchComments(issueNumber) {
|
|
231
|
+
if (!isGitHubConfigured()) {
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
const owner = process.env.VITE_GITHUB_OWNER;
|
|
235
|
+
const repo = process.env.VITE_GITHUB_REPO;
|
|
236
|
+
try {
|
|
237
|
+
const comments = await makeGitHubRequest(
|
|
238
|
+
"GET",
|
|
239
|
+
`/repos/${owner}/${repo}/issues/${issueNumber}/comments`
|
|
240
|
+
);
|
|
241
|
+
console.log(`\u2705 Fetched ${comments.length} comments for Issue #${issueNumber}`);
|
|
242
|
+
return comments;
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.error(`\u274C Failed to fetch comments for Issue #${issueNumber}:`, error);
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
/**
|
|
249
|
+
* Close a GitHub Issue (when deleting a thread)
|
|
250
|
+
*/
|
|
251
|
+
async closeIssue(issueNumber) {
|
|
252
|
+
if (!isGitHubConfigured()) {
|
|
253
|
+
return { success: false, error: "Please sign in with GitHub" };
|
|
254
|
+
}
|
|
255
|
+
const owner = process.env.VITE_GITHUB_OWNER;
|
|
256
|
+
const repo = process.env.VITE_GITHUB_REPO;
|
|
257
|
+
try {
|
|
258
|
+
const issueData = await makeGitHubRequest(
|
|
259
|
+
"PATCH",
|
|
260
|
+
`/repos/${owner}/${repo}/issues/${issueNumber}`,
|
|
261
|
+
{ state: "closed" }
|
|
262
|
+
);
|
|
263
|
+
console.log(`\u2705 Closed Issue #${issueNumber}`);
|
|
264
|
+
return { success: true, data: issueData };
|
|
265
|
+
} catch (error) {
|
|
266
|
+
const errorMessage = error?.response?.data?.message || error?.message || "Failed to close issue";
|
|
267
|
+
console.error(`\u274C Failed to close Issue #${issueNumber}:`, errorMessage);
|
|
268
|
+
return { success: false, error: errorMessage };
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
/**
|
|
272
|
+
* Update an existing comment on a GitHub Issue
|
|
273
|
+
*/
|
|
274
|
+
async updateComment(commentId, body) {
|
|
275
|
+
if (!isGitHubConfigured()) {
|
|
276
|
+
return { success: false, error: "Please sign in with GitHub" };
|
|
277
|
+
}
|
|
278
|
+
const owner = process.env.VITE_GITHUB_OWNER;
|
|
279
|
+
const repo = process.env.VITE_GITHUB_REPO;
|
|
280
|
+
try {
|
|
281
|
+
const commentData = await makeGitHubRequest(
|
|
282
|
+
"PATCH",
|
|
283
|
+
`/repos/${owner}/${repo}/issues/comments/${commentId}`,
|
|
284
|
+
{ body }
|
|
285
|
+
);
|
|
286
|
+
console.log(`\u2705 Updated comment #${commentId}`);
|
|
287
|
+
return { success: true, data: commentData };
|
|
288
|
+
} catch (error) {
|
|
289
|
+
const errorMessage = error?.response?.data?.message || error?.message || "Failed to update comment";
|
|
290
|
+
console.error(`\u274C Failed to update comment #${commentId}:`, errorMessage);
|
|
291
|
+
return { success: false, error: errorMessage };
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
/**
|
|
295
|
+
* Delete a comment on a GitHub Issue
|
|
296
|
+
*/
|
|
297
|
+
async deleteComment(commentId) {
|
|
298
|
+
if (!isGitHubConfigured()) {
|
|
299
|
+
return { success: false, error: "Please sign in with GitHub" };
|
|
300
|
+
}
|
|
301
|
+
const owner = process.env.VITE_GITHUB_OWNER;
|
|
302
|
+
const repo = process.env.VITE_GITHUB_REPO;
|
|
303
|
+
try {
|
|
304
|
+
await makeGitHubRequest(
|
|
305
|
+
"DELETE",
|
|
306
|
+
`/repos/${owner}/${repo}/issues/comments/${commentId}`
|
|
307
|
+
);
|
|
308
|
+
console.log(`\u2705 Deleted comment #${commentId}`);
|
|
309
|
+
return { success: true };
|
|
310
|
+
} catch (error) {
|
|
311
|
+
const errorMessage = error?.response?.data?.message || error?.message || "Failed to delete comment";
|
|
312
|
+
console.error(`\u274C Failed to delete comment #${commentId}:`, errorMessage);
|
|
313
|
+
return { success: false, error: errorMessage };
|
|
314
|
+
}
|
|
315
|
+
}
|
|
67
316
|
};
|
|
68
317
|
|
|
69
|
-
// src/
|
|
70
|
-
var
|
|
71
|
-
var
|
|
72
|
-
|
|
318
|
+
// src/services/gitlabAdapter.ts
|
|
319
|
+
var import_axios2 = __toESM(require("axios"));
|
|
320
|
+
var getApiBase2 = () => {
|
|
321
|
+
if (typeof window === "undefined") return "/api/gitlab-api";
|
|
322
|
+
const isNetlify = window.location.hostname.includes("netlify.app");
|
|
323
|
+
return isNetlify ? "/.netlify/functions/gitlab-api" : "/api/gitlab-api";
|
|
324
|
+
};
|
|
325
|
+
var getStoredToken2 = () => {
|
|
326
|
+
if (typeof window === "undefined") return null;
|
|
327
|
+
return localStorage.getItem("gitlab_access_token");
|
|
328
|
+
};
|
|
329
|
+
var isGitLabConfigured = () => {
|
|
330
|
+
const token = getStoredToken2();
|
|
331
|
+
const projectPath = process.env.VITE_GITLAB_PROJECT_PATH;
|
|
332
|
+
const baseUrl = process.env.VITE_GITLAB_BASE_URL || "https://gitlab.com";
|
|
333
|
+
return !!(token && projectPath && baseUrl);
|
|
334
|
+
};
|
|
335
|
+
async function makeGitLabRequest(method, endpoint, data) {
|
|
336
|
+
const token = getStoredToken2();
|
|
337
|
+
if (!token) {
|
|
338
|
+
throw new Error("Not authenticated with GitLab");
|
|
339
|
+
}
|
|
340
|
+
const baseUrl = process.env.VITE_GITLAB_BASE_URL || "https://gitlab.com";
|
|
341
|
+
const response = await import_axios2.default.post(getApiBase2(), {
|
|
342
|
+
token,
|
|
343
|
+
method,
|
|
344
|
+
endpoint,
|
|
345
|
+
data,
|
|
346
|
+
baseUrl
|
|
347
|
+
});
|
|
348
|
+
return response.data;
|
|
349
|
+
}
|
|
350
|
+
var encodeProject = (p) => encodeURIComponent(p);
|
|
351
|
+
var gitlabAdapter = {
|
|
352
|
+
async createIssue(title, body, route, x, y, version) {
|
|
353
|
+
if (!isGitLabConfigured()) {
|
|
354
|
+
return { success: false, error: "Please sign in with GitLab" };
|
|
355
|
+
}
|
|
356
|
+
try {
|
|
357
|
+
const projectPath = process.env.VITE_GITLAB_PROJECT_PATH;
|
|
358
|
+
const labels = ["apollo-comment", `route:${route}`, `coords:${Math.round(x)},${Math.round(y)}`];
|
|
359
|
+
if (version) labels.push(`version:${version}`);
|
|
360
|
+
const issue = await makeGitLabRequest(
|
|
361
|
+
"POST",
|
|
362
|
+
`/projects/${encodeProject(projectPath)}/issues`,
|
|
363
|
+
{ title, description: body, labels: labels.join(",") }
|
|
364
|
+
);
|
|
365
|
+
return { success: true, data: { ...issue, number: issue.iid } };
|
|
366
|
+
} catch (error) {
|
|
367
|
+
const message = error?.response?.data?.message || error?.message || "Failed to create GitLab issue";
|
|
368
|
+
return { success: false, error: message };
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
async createComment(issueNumber, body) {
|
|
372
|
+
if (!isGitLabConfigured()) {
|
|
373
|
+
return { success: false, error: "Please sign in with GitLab" };
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
const projectPath = process.env.VITE_GITLAB_PROJECT_PATH;
|
|
377
|
+
const note = await makeGitLabRequest(
|
|
378
|
+
"POST",
|
|
379
|
+
`/projects/${encodeProject(projectPath)}/issues/${issueNumber}/notes`,
|
|
380
|
+
{ body }
|
|
381
|
+
);
|
|
382
|
+
return { success: true, data: note };
|
|
383
|
+
} catch (error) {
|
|
384
|
+
const message = error?.response?.data?.message || error?.message || "Failed to create GitLab comment";
|
|
385
|
+
return { success: false, error: message };
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
async updateComment(commentId, body) {
|
|
389
|
+
if (!isGitLabConfigured()) {
|
|
390
|
+
return { success: false, error: "Please sign in with GitLab" };
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
const projectPath = process.env.VITE_GITLAB_PROJECT_PATH;
|
|
394
|
+
const note = await makeGitLabRequest(
|
|
395
|
+
"PUT",
|
|
396
|
+
`/projects/${encodeProject(projectPath)}/notes/${commentId}`,
|
|
397
|
+
{ body }
|
|
398
|
+
);
|
|
399
|
+
return { success: true, data: note };
|
|
400
|
+
} catch (error) {
|
|
401
|
+
const message = error?.response?.data?.message || error?.message || "Failed to update GitLab comment";
|
|
402
|
+
return { success: false, error: message };
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
async deleteComment(commentId) {
|
|
406
|
+
if (!isGitLabConfigured()) {
|
|
407
|
+
return { success: false, error: "Please sign in with GitLab" };
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
const projectPath = process.env.VITE_GITLAB_PROJECT_PATH;
|
|
411
|
+
await makeGitLabRequest(
|
|
412
|
+
"DELETE",
|
|
413
|
+
`/projects/${encodeProject(projectPath)}/notes/${commentId}`
|
|
414
|
+
);
|
|
415
|
+
return { success: true };
|
|
416
|
+
} catch (error) {
|
|
417
|
+
const message = error?.response?.data?.message || error?.message || "Failed to delete GitLab comment";
|
|
418
|
+
return { success: false, error: message };
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
async closeIssue(issueNumber) {
|
|
422
|
+
if (!isGitLabConfigured()) {
|
|
423
|
+
return { success: false, error: "Please sign in with GitLab" };
|
|
424
|
+
}
|
|
425
|
+
try {
|
|
426
|
+
const projectPath = process.env.VITE_GITLAB_PROJECT_PATH;
|
|
427
|
+
const issue = await makeGitLabRequest(
|
|
428
|
+
"PUT",
|
|
429
|
+
`/projects/${encodeProject(projectPath)}/issues/${issueNumber}`,
|
|
430
|
+
{ state_event: "close" }
|
|
431
|
+
);
|
|
432
|
+
return { success: true, data: issue };
|
|
433
|
+
} catch (error) {
|
|
434
|
+
const message = error?.response?.data?.message || error?.message || "Failed to close GitLab issue";
|
|
435
|
+
return { success: false, error: message };
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
async fetchIssues(route) {
|
|
439
|
+
if (!isGitLabConfigured()) {
|
|
440
|
+
return [];
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
const projectPath = process.env.VITE_GITLAB_PROJECT_PATH;
|
|
444
|
+
const issues = await makeGitLabRequest(
|
|
445
|
+
"GET",
|
|
446
|
+
`/projects/${encodeProject(projectPath)}/issues?state=opened&per_page=100`
|
|
447
|
+
);
|
|
448
|
+
return (issues || []).filter((issue) => {
|
|
449
|
+
const labels = issue.labels || [];
|
|
450
|
+
const routeLabel = labels.some((l) => l === `route:${route}`);
|
|
451
|
+
const inBody = (issue.description || "").includes(`Route: \`${route}\``);
|
|
452
|
+
return routeLabel || inBody;
|
|
453
|
+
});
|
|
454
|
+
} catch {
|
|
455
|
+
return [];
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
async fetchComments(issueNumber) {
|
|
459
|
+
if (!isGitLabConfigured()) {
|
|
460
|
+
return [];
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
const projectPath = process.env.VITE_GITLAB_PROJECT_PATH;
|
|
464
|
+
const notes = await makeGitLabRequest(
|
|
465
|
+
"GET",
|
|
466
|
+
`/projects/${encodeProject(projectPath)}/issues/${issueNumber}/notes?per_page=100`
|
|
467
|
+
);
|
|
468
|
+
return notes || [];
|
|
469
|
+
} catch {
|
|
470
|
+
return [];
|
|
471
|
+
}
|
|
472
|
+
}
|
|
73
473
|
};
|
|
74
474
|
|
|
75
475
|
// src/contexts/CommentContext.tsx
|
|
76
|
-
var
|
|
77
|
-
var
|
|
78
|
-
var
|
|
476
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
477
|
+
var CommentContext = React.createContext(void 0);
|
|
478
|
+
var STORAGE_KEY = "apollo-threads";
|
|
479
|
+
var SHOW_PINS_KEY = "apollo-show-pins";
|
|
480
|
+
var ENABLE_COMMENTING_KEY = "apollo-enable-commenting";
|
|
481
|
+
var migrateOldComments = () => {
|
|
482
|
+
try {
|
|
483
|
+
const oldComments = localStorage.getItem("apollo-comments");
|
|
484
|
+
if (oldComments) {
|
|
485
|
+
const parsed = JSON.parse(oldComments);
|
|
486
|
+
const threads = parsed.map((oldComment) => ({
|
|
487
|
+
id: oldComment.id,
|
|
488
|
+
x: oldComment.x,
|
|
489
|
+
y: oldComment.y,
|
|
490
|
+
route: oldComment.route,
|
|
491
|
+
comments: [
|
|
492
|
+
{
|
|
493
|
+
id: `${oldComment.id}-comment-0`,
|
|
494
|
+
text: oldComment.text || "",
|
|
495
|
+
createdAt: oldComment.createdAt
|
|
496
|
+
}
|
|
497
|
+
]
|
|
498
|
+
}));
|
|
499
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(threads));
|
|
500
|
+
localStorage.removeItem("apollo-comments");
|
|
501
|
+
return threads;
|
|
502
|
+
}
|
|
503
|
+
} catch (error) {
|
|
504
|
+
console.error("Failed to migrate old comments:", error);
|
|
505
|
+
}
|
|
506
|
+
return [];
|
|
507
|
+
};
|
|
79
508
|
var CommentProvider = ({ children }) => {
|
|
80
|
-
|
|
509
|
+
const [threads, setThreads] = React.useState(() => {
|
|
510
|
+
try {
|
|
511
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
512
|
+
if (stored) {
|
|
513
|
+
return JSON.parse(stored);
|
|
514
|
+
}
|
|
515
|
+
return migrateOldComments();
|
|
516
|
+
} catch (error) {
|
|
517
|
+
console.error("Failed to load threads from localStorage:", error);
|
|
518
|
+
return [];
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
const [showPins, setShowPins] = React.useState(() => {
|
|
522
|
+
try {
|
|
523
|
+
const stored = localStorage.getItem(SHOW_PINS_KEY);
|
|
524
|
+
return stored === "true";
|
|
525
|
+
} catch (error) {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
const [enableCommenting, setEnableCommenting] = React.useState(() => {
|
|
530
|
+
try {
|
|
531
|
+
const stored = localStorage.getItem(ENABLE_COMMENTING_KEY);
|
|
532
|
+
return stored === "true";
|
|
533
|
+
} catch (error) {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
const [isSyncing, setIsSyncing] = React.useState(false);
|
|
538
|
+
React.useEffect(() => {
|
|
539
|
+
try {
|
|
540
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(threads));
|
|
541
|
+
} catch (error) {
|
|
542
|
+
console.error("Failed to save threads to localStorage:", error);
|
|
543
|
+
}
|
|
544
|
+
}, [threads]);
|
|
545
|
+
React.useEffect(() => {
|
|
546
|
+
try {
|
|
547
|
+
localStorage.setItem(SHOW_PINS_KEY, String(showPins));
|
|
548
|
+
} catch (error) {
|
|
549
|
+
console.error("Failed to save showPins to localStorage:", error);
|
|
550
|
+
}
|
|
551
|
+
}, [showPins]);
|
|
552
|
+
React.useEffect(() => {
|
|
553
|
+
try {
|
|
554
|
+
localStorage.setItem(ENABLE_COMMENTING_KEY, String(enableCommenting));
|
|
555
|
+
} catch (error) {
|
|
556
|
+
console.error("Failed to save enableCommenting to localStorage:", error);
|
|
557
|
+
}
|
|
558
|
+
}, [enableCommenting]);
|
|
559
|
+
const toggleShowPins = React.useCallback(() => {
|
|
560
|
+
setShowPins((prev) => !prev);
|
|
561
|
+
}, []);
|
|
562
|
+
const toggleEnableCommenting = React.useCallback(() => {
|
|
563
|
+
setEnableCommenting((prev) => !prev);
|
|
564
|
+
}, []);
|
|
565
|
+
const addThread = React.useCallback((x, y, route, version) => {
|
|
566
|
+
const threadId = `thread-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
567
|
+
const newThread = {
|
|
568
|
+
id: threadId,
|
|
569
|
+
x,
|
|
570
|
+
y,
|
|
571
|
+
route,
|
|
572
|
+
comments: [],
|
|
573
|
+
// Start with no comments
|
|
574
|
+
syncStatus: "local",
|
|
575
|
+
version
|
|
576
|
+
};
|
|
577
|
+
setThreads((prev) => [...prev, newThread]);
|
|
578
|
+
if (isGitHubConfigured() || isGitLabConfigured()) {
|
|
579
|
+
(async () => {
|
|
580
|
+
setThreads((prev) => prev.map(
|
|
581
|
+
(t) => t.id === threadId ? { ...t, syncStatus: "syncing" } : t
|
|
582
|
+
));
|
|
583
|
+
const pageName = route === "/" ? "Home page" : route.split("/").filter(Boolean).join(" > ") || "Page";
|
|
584
|
+
const versionStr = version ? ` [v${version}]` : "";
|
|
585
|
+
let createdProvider;
|
|
586
|
+
let createdNumber;
|
|
587
|
+
let createError;
|
|
588
|
+
if (isGitHubConfigured()) {
|
|
589
|
+
const issue = await githubAdapter.createIssue(
|
|
590
|
+
`\u{1F4AC} ${pageName} comment${versionStr}`,
|
|
591
|
+
`Thread created on route: ${route}
|
|
592
|
+
|
|
593
|
+
Coordinates: (${Math.round(x)}, ${Math.round(y)})
|
|
594
|
+
|
|
595
|
+
**Version:** ${version || "N/A"}
|
|
596
|
+
|
|
597
|
+
(Initial comment will be added as a reply)`,
|
|
598
|
+
route,
|
|
599
|
+
x,
|
|
600
|
+
y,
|
|
601
|
+
version
|
|
602
|
+
);
|
|
603
|
+
if (issue.success && issue.data) {
|
|
604
|
+
createdProvider = "github";
|
|
605
|
+
createdNumber = issue.data.number;
|
|
606
|
+
} else {
|
|
607
|
+
createError = issue.error;
|
|
608
|
+
}
|
|
609
|
+
} else if (isGitLabConfigured()) {
|
|
610
|
+
const issue = await gitlabAdapter.createIssue(
|
|
611
|
+
`\u{1F4AC} ${pageName} comment${versionStr}`,
|
|
612
|
+
`Thread created on route: ${route}
|
|
613
|
+
|
|
614
|
+
Coordinates: (${Math.round(x)}, ${Math.round(y)})
|
|
615
|
+
|
|
616
|
+
**Version:** ${version || "N/A"}
|
|
617
|
+
|
|
618
|
+
(Initial comment will be added as a reply)`,
|
|
619
|
+
route,
|
|
620
|
+
x,
|
|
621
|
+
y,
|
|
622
|
+
version
|
|
623
|
+
);
|
|
624
|
+
if (issue.success && issue.data) {
|
|
625
|
+
createdProvider = "gitlab";
|
|
626
|
+
createdNumber = issue.data.number;
|
|
627
|
+
} else {
|
|
628
|
+
createError = issue.error;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (createdNumber && createdProvider) {
|
|
632
|
+
setThreads((prev) => prev.map(
|
|
633
|
+
(t) => t.id === threadId ? { ...t, issueNumber: createdNumber, provider: createdProvider, syncStatus: "synced" } : t
|
|
634
|
+
));
|
|
635
|
+
} else if (createError) {
|
|
636
|
+
setThreads((prev) => prev.map(
|
|
637
|
+
(t) => t.id === threadId ? { ...t, syncStatus: "error", syncError: createError } : t
|
|
638
|
+
));
|
|
639
|
+
}
|
|
640
|
+
})();
|
|
641
|
+
}
|
|
642
|
+
return threadId;
|
|
643
|
+
}, []);
|
|
644
|
+
const addReply = React.useCallback(async (threadId, text) => {
|
|
645
|
+
const commentId = `${threadId}-comment-${Date.now()}`;
|
|
646
|
+
const newComment = {
|
|
647
|
+
id: commentId,
|
|
648
|
+
text,
|
|
649
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
650
|
+
};
|
|
651
|
+
const thread = threads.find((t) => t.id === threadId);
|
|
652
|
+
setThreads(
|
|
653
|
+
(prev) => prev.map((t) => {
|
|
654
|
+
if (t.id === threadId) {
|
|
655
|
+
return {
|
|
656
|
+
...t,
|
|
657
|
+
comments: [...t.comments, newComment],
|
|
658
|
+
syncStatus: "pending"
|
|
659
|
+
// Mark as pending sync
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
return t;
|
|
663
|
+
})
|
|
664
|
+
);
|
|
665
|
+
if (thread) {
|
|
666
|
+
if (!thread.issueNumber) {
|
|
667
|
+
console.log("\u{1F535} Thread has no issue number, creating remote issue first...");
|
|
668
|
+
setThreads((prev) => prev.map(
|
|
669
|
+
(t) => t.id === threadId ? { ...t, syncStatus: "syncing" } : t
|
|
670
|
+
));
|
|
671
|
+
const pageName = thread.route === "/" ? "Home page" : thread.route.split("/").filter(Boolean).join(" > ") || "Page";
|
|
672
|
+
const versionStr = thread.version ? ` [v${thread.version}]` : "";
|
|
673
|
+
let createdProvider;
|
|
674
|
+
let createdNumber;
|
|
675
|
+
const providerPref = thread.provider || (isGitHubConfigured() ? "github" : isGitLabConfigured() ? "gitlab" : void 0);
|
|
676
|
+
if (providerPref === "github" && isGitHubConfigured()) {
|
|
677
|
+
const issue = await githubAdapter.createIssue(
|
|
678
|
+
`\u{1F4AC} ${pageName} comment${versionStr}`,
|
|
679
|
+
`Thread created on route: ${thread.route}
|
|
680
|
+
|
|
681
|
+
Coordinates: (${Math.round(thread.x)}, ${Math.round(thread.y)})
|
|
682
|
+
|
|
683
|
+
**Version:** ${thread.version || "N/A"}`,
|
|
684
|
+
thread.route,
|
|
685
|
+
thread.x,
|
|
686
|
+
thread.y,
|
|
687
|
+
thread.version
|
|
688
|
+
);
|
|
689
|
+
if (issue.success && issue.data) {
|
|
690
|
+
createdProvider = "github";
|
|
691
|
+
createdNumber = issue.data.number;
|
|
692
|
+
} else {
|
|
693
|
+
console.error("\u274C Failed to create GitHub issue:", issue.error);
|
|
694
|
+
}
|
|
695
|
+
} else if (providerPref === "gitlab" && isGitLabConfigured()) {
|
|
696
|
+
const issue = await gitlabAdapter.createIssue(
|
|
697
|
+
`\u{1F4AC} ${pageName} comment${versionStr}`,
|
|
698
|
+
`Thread created on route: ${thread.route}
|
|
699
|
+
|
|
700
|
+
Coordinates: (${Math.round(thread.x)}, ${Math.round(thread.y)})
|
|
701
|
+
|
|
702
|
+
**Version:** ${thread.version || "N/A"}`,
|
|
703
|
+
thread.route,
|
|
704
|
+
thread.x,
|
|
705
|
+
thread.y,
|
|
706
|
+
thread.version
|
|
707
|
+
);
|
|
708
|
+
if (issue.success && issue.data) {
|
|
709
|
+
createdProvider = "gitlab";
|
|
710
|
+
createdNumber = issue.data.number;
|
|
711
|
+
} else {
|
|
712
|
+
console.error("\u274C Failed to create GitLab issue:", issue.error);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (createdNumber && createdProvider) {
|
|
716
|
+
console.log("\u2705 Created remote issue #", createdNumber);
|
|
717
|
+
setThreads((prev) => prev.map(
|
|
718
|
+
(t) => t.id === threadId ? { ...t, issueNumber: createdNumber, provider: createdProvider, syncStatus: "pending" } : t
|
|
719
|
+
));
|
|
720
|
+
const updatedThread = threads.find((t) => t.id === threadId);
|
|
721
|
+
if (updatedThread) {
|
|
722
|
+
for (const comment of updatedThread.comments) {
|
|
723
|
+
if (!comment.githubCommentId) {
|
|
724
|
+
const commentResult = createdProvider === "github" ? await githubAdapter.createComment(createdNumber, comment.text) : await gitlabAdapter.createComment(createdNumber, comment.text);
|
|
725
|
+
if (commentResult.success && commentResult.data) {
|
|
726
|
+
setThreads((prev) => prev.map((t) => {
|
|
727
|
+
if (t.id === threadId) {
|
|
728
|
+
return {
|
|
729
|
+
...t,
|
|
730
|
+
comments: t.comments.map(
|
|
731
|
+
(c) => c.id === comment.id ? { ...c, githubCommentId: commentResult.data.id } : c
|
|
732
|
+
)
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
return t;
|
|
736
|
+
}));
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
const result = createdProvider === "github" ? await githubAdapter.createComment(createdNumber, text) : await gitlabAdapter.createComment(createdNumber, text);
|
|
742
|
+
if (result.success && result.data) {
|
|
743
|
+
setThreads(
|
|
744
|
+
(prev) => prev.map((t) => {
|
|
745
|
+
if (t.id === threadId) {
|
|
746
|
+
return {
|
|
747
|
+
...t,
|
|
748
|
+
syncStatus: "synced",
|
|
749
|
+
comments: t.comments.map(
|
|
750
|
+
(c) => c.id === commentId ? { ...c, githubCommentId: result.data.id } : c
|
|
751
|
+
)
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
return t;
|
|
755
|
+
})
|
|
756
|
+
);
|
|
757
|
+
} else {
|
|
758
|
+
setThreads(
|
|
759
|
+
(prev) => prev.map(
|
|
760
|
+
(t) => t.id === threadId ? { ...t, syncStatus: "error", syncError: result.error } : t
|
|
761
|
+
)
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
} else {
|
|
765
|
+
setThreads((prev) => prev.map(
|
|
766
|
+
(t) => t.id === threadId ? { ...t, syncStatus: "error", syncError: "Failed to create remote issue" } : t
|
|
767
|
+
));
|
|
768
|
+
}
|
|
769
|
+
} else {
|
|
770
|
+
const isGitHub = thread.provider === "github";
|
|
771
|
+
const isGitLab = thread.provider === "gitlab";
|
|
772
|
+
let result;
|
|
773
|
+
if (isGitHub && isGitHubConfigured()) {
|
|
774
|
+
result = await githubAdapter.createComment(thread.issueNumber, text);
|
|
775
|
+
} else if (isGitLab && isGitLabConfigured()) {
|
|
776
|
+
result = await gitlabAdapter.createComment(thread.issueNumber, text);
|
|
777
|
+
} else {
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
if (result.success && result.data) {
|
|
781
|
+
setThreads(
|
|
782
|
+
(prev) => prev.map((t) => {
|
|
783
|
+
if (t.id === threadId) {
|
|
784
|
+
return {
|
|
785
|
+
...t,
|
|
786
|
+
syncStatus: "synced",
|
|
787
|
+
comments: t.comments.map(
|
|
788
|
+
(c) => c.id === commentId ? { ...c, githubCommentId: result.data.id } : c
|
|
789
|
+
)
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
return t;
|
|
793
|
+
})
|
|
794
|
+
);
|
|
795
|
+
} else {
|
|
796
|
+
setThreads(
|
|
797
|
+
(prev) => prev.map(
|
|
798
|
+
(t) => t.id === threadId ? { ...t, syncStatus: "error", syncError: result.error } : t
|
|
799
|
+
)
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}, [threads]);
|
|
805
|
+
const updateComment = React.useCallback(async (threadId, commentId, text) => {
|
|
806
|
+
const thread = threads.find((t) => t.id === threadId);
|
|
807
|
+
const comment = thread?.comments.find((c) => c.id === commentId);
|
|
808
|
+
setThreads(
|
|
809
|
+
(prev) => prev.map((t) => {
|
|
810
|
+
if (t.id === threadId) {
|
|
811
|
+
return {
|
|
812
|
+
...t,
|
|
813
|
+
syncStatus: "pending",
|
|
814
|
+
comments: t.comments.map(
|
|
815
|
+
(c) => c.id === commentId ? { ...c, text } : c
|
|
816
|
+
)
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
return t;
|
|
820
|
+
})
|
|
821
|
+
);
|
|
822
|
+
if (comment?.githubCommentId) {
|
|
823
|
+
let ok = false;
|
|
824
|
+
if (thread?.provider === "github" && isGitHubConfigured()) {
|
|
825
|
+
const result = await githubAdapter.updateComment(comment.githubCommentId, text);
|
|
826
|
+
ok = !!result.success;
|
|
827
|
+
} else if (thread?.provider === "gitlab" && isGitLabConfigured()) {
|
|
828
|
+
const result = await gitlabAdapter.updateComment(comment.githubCommentId, text);
|
|
829
|
+
ok = !!result.success;
|
|
830
|
+
}
|
|
831
|
+
if (ok) {
|
|
832
|
+
setThreads(
|
|
833
|
+
(prev) => prev.map((t) => t.id === threadId ? { ...t, syncStatus: "synced" } : t)
|
|
834
|
+
);
|
|
835
|
+
} else {
|
|
836
|
+
setThreads(
|
|
837
|
+
(prev) => prev.map(
|
|
838
|
+
(t) => t.id === threadId ? { ...t, syncStatus: "error", syncError: "Update failed" } : t
|
|
839
|
+
)
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}, [threads]);
|
|
844
|
+
const deleteComment = React.useCallback(async (threadId, commentId) => {
|
|
845
|
+
const thread = threads.find((t) => t.id === threadId);
|
|
846
|
+
const comment = thread?.comments.find((c) => c.id === commentId);
|
|
847
|
+
setThreads(
|
|
848
|
+
(prev) => prev.map((t) => {
|
|
849
|
+
if (t.id === threadId) {
|
|
850
|
+
return {
|
|
851
|
+
...t,
|
|
852
|
+
syncStatus: "pending",
|
|
853
|
+
comments: t.comments.filter((c) => c.id !== commentId)
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
return t;
|
|
857
|
+
})
|
|
858
|
+
);
|
|
859
|
+
if (comment?.githubCommentId) {
|
|
860
|
+
let ok = false;
|
|
861
|
+
if (thread?.provider === "github" && isGitHubConfigured()) {
|
|
862
|
+
const result = await githubAdapter.deleteComment(comment.githubCommentId);
|
|
863
|
+
ok = !!result.success;
|
|
864
|
+
} else if (thread?.provider === "gitlab" && isGitLabConfigured()) {
|
|
865
|
+
const result = await gitlabAdapter.deleteComment(comment.githubCommentId);
|
|
866
|
+
ok = !!result.success;
|
|
867
|
+
}
|
|
868
|
+
if (ok) {
|
|
869
|
+
setThreads(
|
|
870
|
+
(prev) => prev.map((t) => t.id === threadId ? { ...t, syncStatus: "synced" } : t)
|
|
871
|
+
);
|
|
872
|
+
} else {
|
|
873
|
+
setThreads(
|
|
874
|
+
(prev) => prev.map(
|
|
875
|
+
(t) => t.id === threadId ? { ...t, syncStatus: "error", syncError: "Delete failed" } : t
|
|
876
|
+
)
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}, [threads]);
|
|
881
|
+
const deleteThread = React.useCallback(async (threadId) => {
|
|
882
|
+
const thread = threads.find((t) => t.id === threadId);
|
|
883
|
+
setThreads((prev) => prev.filter((t) => t.id !== threadId));
|
|
884
|
+
if (thread?.issueNumber) {
|
|
885
|
+
if (thread.provider === "github" && isGitHubConfigured()) {
|
|
886
|
+
const result = await githubAdapter.closeIssue(thread.issueNumber);
|
|
887
|
+
if (!result.success) {
|
|
888
|
+
console.error("Failed to close GitHub issue:", result.error);
|
|
889
|
+
}
|
|
890
|
+
} else if (thread.provider === "gitlab" && isGitLabConfigured()) {
|
|
891
|
+
const result = await gitlabAdapter.closeIssue(thread.issueNumber);
|
|
892
|
+
if (!result.success) {
|
|
893
|
+
console.error("Failed to close GitLab issue:", result.error);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}, [threads]);
|
|
898
|
+
const clearAllThreads = React.useCallback(() => {
|
|
899
|
+
setThreads([]);
|
|
900
|
+
}, []);
|
|
901
|
+
const getThreadsForRoute = React.useCallback((route, version) => {
|
|
902
|
+
return threads.filter((thread) => {
|
|
903
|
+
const routeMatch = thread.route === route;
|
|
904
|
+
const threadVersion = thread.version || "3";
|
|
905
|
+
const versionMatch = !version || threadVersion === version;
|
|
906
|
+
return routeMatch && versionMatch;
|
|
907
|
+
});
|
|
908
|
+
}, [threads]);
|
|
909
|
+
const syncFromGitHub = React.useCallback(async (route) => {
|
|
910
|
+
setIsSyncing(true);
|
|
911
|
+
console.log(`\u{1F504} Syncing threads from remote providers for route: ${route}`);
|
|
912
|
+
try {
|
|
913
|
+
const newThreads = [];
|
|
914
|
+
if (isGitHubConfigured()) {
|
|
915
|
+
const issues = await githubAdapter.fetchIssues(route);
|
|
916
|
+
for (const issue of issues) {
|
|
917
|
+
let coords = null;
|
|
918
|
+
if (issue.body) {
|
|
919
|
+
const coordMatch = issue.body.match(/Coordinates: `\((\d+),\s*(\d+)\)`/);
|
|
920
|
+
if (coordMatch) {
|
|
921
|
+
coords = [parseInt(coordMatch[1]), parseInt(coordMatch[2])];
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
if (!coords) {
|
|
925
|
+
const coordLabel = issue.labels.find(
|
|
926
|
+
(l) => typeof l === "string" ? l.startsWith("coords:") : l.name?.startsWith("coords:")
|
|
927
|
+
);
|
|
928
|
+
const coordString = typeof coordLabel === "string" ? coordLabel : coordLabel?.name;
|
|
929
|
+
if (coordString) {
|
|
930
|
+
const coordParts = coordString.replace("coords:", "").split(",").map(Number);
|
|
931
|
+
if (coordParts.length === 2) {
|
|
932
|
+
coords = coordParts;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
if (!coords || coords.length !== 2 || isNaN(coords[0]) || isNaN(coords[1])) {
|
|
937
|
+
console.warn(`Skipping issue #${issue.number}: invalid or missing coords`);
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
const ghComments = await githubAdapter.fetchComments(issue.number);
|
|
941
|
+
const comments = [];
|
|
942
|
+
if (issue.body) {
|
|
943
|
+
comments.push({
|
|
944
|
+
id: `issue-${issue.number}-body`,
|
|
945
|
+
text: issue.body,
|
|
946
|
+
createdAt: issue.created_at,
|
|
947
|
+
author: issue.user?.login,
|
|
948
|
+
githubCommentId: void 0
|
|
949
|
+
// Body is not a comment
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
ghComments.forEach((ghComment) => {
|
|
953
|
+
comments.push({
|
|
954
|
+
id: `comment-${ghComment.id}`,
|
|
955
|
+
text: ghComment.body,
|
|
956
|
+
createdAt: ghComment.created_at,
|
|
957
|
+
author: ghComment.user?.login,
|
|
958
|
+
githubCommentId: ghComment.id
|
|
959
|
+
});
|
|
960
|
+
});
|
|
961
|
+
newThreads.push({
|
|
962
|
+
id: `github-${issue.number}`,
|
|
963
|
+
x: coords[0],
|
|
964
|
+
y: coords[1],
|
|
965
|
+
route,
|
|
966
|
+
comments,
|
|
967
|
+
issueNumber: issue.number,
|
|
968
|
+
provider: "github",
|
|
969
|
+
syncStatus: "synced"
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
if (isGitLabConfigured()) {
|
|
974
|
+
const issues = await gitlabAdapter.fetchIssues(route);
|
|
975
|
+
for (const issue of issues) {
|
|
976
|
+
let coords = null;
|
|
977
|
+
const labels = issue.labels || [];
|
|
978
|
+
const coordLabel = labels.find((l) => l.startsWith("coords:"));
|
|
979
|
+
if (coordLabel) {
|
|
980
|
+
const parts = coordLabel.replace("coords:", "").split(",").map((n) => parseInt(n, 10));
|
|
981
|
+
if (parts.length === 2 && !Number.isNaN(parts[0]) && !Number.isNaN(parts[1])) {
|
|
982
|
+
coords = parts;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
if (!coords && issue.description) {
|
|
986
|
+
const m = issue.description.match(/Coordinates:\s*`?\((\d+),\s*(\d+)\)`?/);
|
|
987
|
+
if (m) coords = [parseInt(m[1], 10), parseInt(m[2], 10)];
|
|
988
|
+
}
|
|
989
|
+
if (!coords) {
|
|
990
|
+
console.warn(`Skipping GitLab issue #${issue.iid}: invalid or missing coords`);
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
const glComments = await gitlabAdapter.fetchComments(issue.iid);
|
|
994
|
+
const comments = [];
|
|
995
|
+
if (issue.description) {
|
|
996
|
+
comments.push({
|
|
997
|
+
id: `issue-${issue.iid}-body`,
|
|
998
|
+
text: issue.description,
|
|
999
|
+
createdAt: issue.created_at,
|
|
1000
|
+
author: issue.author?.username,
|
|
1001
|
+
githubCommentId: void 0
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
glComments.forEach((note) => {
|
|
1005
|
+
comments.push({
|
|
1006
|
+
id: `comment-${note.id}`,
|
|
1007
|
+
text: note.body,
|
|
1008
|
+
createdAt: note.created_at,
|
|
1009
|
+
author: note.author?.username,
|
|
1010
|
+
githubCommentId: note.id
|
|
1011
|
+
});
|
|
1012
|
+
});
|
|
1013
|
+
newThreads.push({
|
|
1014
|
+
id: `gitlab-${issue.iid}`,
|
|
1015
|
+
x: coords[0],
|
|
1016
|
+
y: coords[1],
|
|
1017
|
+
route,
|
|
1018
|
+
comments,
|
|
1019
|
+
issueNumber: issue.iid,
|
|
1020
|
+
provider: "gitlab",
|
|
1021
|
+
syncStatus: "synced"
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
setThreads((prev) => {
|
|
1026
|
+
const localOnlyThreads = prev.filter((t) => t.route === route && !t.issueNumber);
|
|
1027
|
+
const mergedThreads = [
|
|
1028
|
+
...prev.filter((t) => t.route !== route),
|
|
1029
|
+
// Keep threads from other routes
|
|
1030
|
+
...newThreads,
|
|
1031
|
+
// Add synced threads
|
|
1032
|
+
...localOnlyThreads
|
|
1033
|
+
// Keep local-only threads
|
|
1034
|
+
];
|
|
1035
|
+
return mergedThreads;
|
|
1036
|
+
});
|
|
1037
|
+
console.log(`\u2705 Synced ${newThreads.length} threads from providers`);
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
console.error("Failed to sync from providers:", error);
|
|
1040
|
+
} finally {
|
|
1041
|
+
setIsSyncing(false);
|
|
1042
|
+
}
|
|
1043
|
+
}, []);
|
|
1044
|
+
const retrySync = React.useCallback(async () => {
|
|
1045
|
+
console.log("\u{1F504} Retrying sync for pending/error threads...");
|
|
1046
|
+
const threadsToSync = threads.filter(
|
|
1047
|
+
(t) => (t.syncStatus === "pending" || t.syncStatus === "error") && !t.issueNumber
|
|
1048
|
+
);
|
|
1049
|
+
if (threadsToSync.length === 0) {
|
|
1050
|
+
console.log("No threads to sync");
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
if (!isGitHubConfigured()) {
|
|
1054
|
+
console.warn("GitHub not configured. Cannot retry sync.");
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
for (const thread of threadsToSync) {
|
|
1058
|
+
console.log(`\u{1F535} Syncing thread ${thread.id}...`);
|
|
1059
|
+
setThreads((prev) => prev.map(
|
|
1060
|
+
(t) => t.id === thread.id ? { ...t, syncStatus: "syncing" } : t
|
|
1061
|
+
));
|
|
1062
|
+
const pageName = thread.route === "/" ? "Home page" : thread.route.split("/").filter(Boolean).join(" > ") || "Page";
|
|
1063
|
+
const versionStr = thread.version ? ` [v${thread.version}]` : "";
|
|
1064
|
+
const issue = await githubAdapter.createIssue(
|
|
1065
|
+
`\u{1F4AC} ${pageName} comment${versionStr}`,
|
|
1066
|
+
`Thread created on route: ${thread.route}
|
|
1067
|
+
|
|
1068
|
+
Coordinates: (${Math.round(thread.x)}, ${Math.round(thread.y)})
|
|
1069
|
+
|
|
1070
|
+
**Version:** ${thread.version || "N/A"}`,
|
|
1071
|
+
thread.route,
|
|
1072
|
+
thread.x,
|
|
1073
|
+
thread.y,
|
|
1074
|
+
thread.version
|
|
1075
|
+
);
|
|
1076
|
+
if (issue.success && issue.data) {
|
|
1077
|
+
console.log("\u2705 Created GitHub issue #", issue.data.number);
|
|
1078
|
+
setThreads((prev) => prev.map(
|
|
1079
|
+
(t) => t.id === thread.id ? { ...t, issueNumber: issue.data.number } : t
|
|
1080
|
+
));
|
|
1081
|
+
for (const comment of thread.comments) {
|
|
1082
|
+
if (!comment.githubCommentId) {
|
|
1083
|
+
const commentResult = await githubAdapter.createComment(issue.data.number, comment.text);
|
|
1084
|
+
if (commentResult.success && commentResult.data) {
|
|
1085
|
+
setThreads((prev) => prev.map((t) => {
|
|
1086
|
+
if (t.id === thread.id) {
|
|
1087
|
+
return {
|
|
1088
|
+
...t,
|
|
1089
|
+
comments: t.comments.map(
|
|
1090
|
+
(c) => c.id === comment.id ? { ...c, githubCommentId: commentResult.data.id } : c
|
|
1091
|
+
)
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
return t;
|
|
1095
|
+
}));
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
setThreads((prev) => prev.map(
|
|
1100
|
+
(t) => t.id === thread.id ? { ...t, syncStatus: "synced" } : t
|
|
1101
|
+
));
|
|
1102
|
+
} else {
|
|
1103
|
+
console.error("\u274C Failed to create GitHub issue:", issue.error);
|
|
1104
|
+
setThreads((prev) => prev.map(
|
|
1105
|
+
(t) => t.id === thread.id ? { ...t, syncStatus: "error", syncError: issue.error } : t
|
|
1106
|
+
));
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
console.log("\u2705 Retry sync complete");
|
|
1110
|
+
}, [threads]);
|
|
1111
|
+
const hasPendingSync = React.useMemo(() => {
|
|
1112
|
+
return threads.some((t) => t.syncStatus === "pending" || t.syncStatus === "error");
|
|
1113
|
+
}, [threads]);
|
|
1114
|
+
const value = React.useMemo(
|
|
1115
|
+
() => ({
|
|
1116
|
+
threads,
|
|
1117
|
+
showPins,
|
|
1118
|
+
enableCommenting,
|
|
1119
|
+
toggleShowPins,
|
|
1120
|
+
toggleEnableCommenting,
|
|
1121
|
+
addThread,
|
|
1122
|
+
addReply,
|
|
1123
|
+
updateComment,
|
|
1124
|
+
deleteComment,
|
|
1125
|
+
deleteThread,
|
|
1126
|
+
clearAllThreads,
|
|
1127
|
+
getThreadsForRoute,
|
|
1128
|
+
syncFromGitHub,
|
|
1129
|
+
retrySync,
|
|
1130
|
+
isSyncing,
|
|
1131
|
+
hasPendingSync
|
|
1132
|
+
}),
|
|
1133
|
+
[threads, showPins, enableCommenting, toggleShowPins, toggleEnableCommenting, addThread, addReply, updateComment, deleteComment, deleteThread, clearAllThreads, getThreadsForRoute, syncFromGitHub, retrySync, isSyncing, hasPendingSync]
|
|
1134
|
+
);
|
|
1135
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CommentContext.Provider, { value, children });
|
|
81
1136
|
};
|
|
82
1137
|
var useComments = () => {
|
|
83
|
-
const context =
|
|
1138
|
+
const context = React.useContext(CommentContext);
|
|
84
1139
|
if (!context) {
|
|
85
|
-
throw new Error("useComments must be used within CommentProvider");
|
|
1140
|
+
throw new Error("useComments must be used within a CommentProvider");
|
|
86
1141
|
}
|
|
87
1142
|
return context;
|
|
88
1143
|
};
|
|
89
1144
|
|
|
90
1145
|
// src/contexts/VersionContext.tsx
|
|
91
|
-
var
|
|
92
|
-
var
|
|
93
|
-
var VersionContext =
|
|
1146
|
+
var React2 = __toESM(require("react"));
|
|
1147
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
1148
|
+
var VersionContext = React2.createContext(void 0);
|
|
1149
|
+
var VERSION_STORAGE_KEY = "apollo-current-version";
|
|
94
1150
|
var VersionProvider = ({ children }) => {
|
|
95
|
-
|
|
1151
|
+
const [currentVersion, setCurrentVersionState] = React2.useState(() => {
|
|
1152
|
+
try {
|
|
1153
|
+
const stored = localStorage.getItem(VERSION_STORAGE_KEY);
|
|
1154
|
+
return stored || "3";
|
|
1155
|
+
} catch (error) {
|
|
1156
|
+
console.error("Failed to load version from localStorage:", error);
|
|
1157
|
+
return "3";
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
React2.useEffect(() => {
|
|
1161
|
+
try {
|
|
1162
|
+
localStorage.setItem(VERSION_STORAGE_KEY, currentVersion);
|
|
1163
|
+
} catch (error) {
|
|
1164
|
+
console.error("Failed to save version to localStorage:", error);
|
|
1165
|
+
}
|
|
1166
|
+
}, [currentVersion]);
|
|
1167
|
+
const setCurrentVersion = React2.useCallback((version) => {
|
|
1168
|
+
setCurrentVersionState(version);
|
|
1169
|
+
}, []);
|
|
1170
|
+
const value = React2.useMemo(
|
|
1171
|
+
() => ({
|
|
1172
|
+
currentVersion,
|
|
1173
|
+
setCurrentVersion
|
|
1174
|
+
}),
|
|
1175
|
+
[currentVersion, setCurrentVersion]
|
|
1176
|
+
);
|
|
1177
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(VersionContext.Provider, { value, children });
|
|
96
1178
|
};
|
|
97
1179
|
var useVersion = () => {
|
|
98
|
-
const context =
|
|
1180
|
+
const context = React2.useContext(VersionContext);
|
|
99
1181
|
if (!context) {
|
|
100
|
-
throw new Error("useVersion must be used within VersionProvider");
|
|
1182
|
+
throw new Error("useVersion must be used within a VersionProvider");
|
|
101
1183
|
}
|
|
102
1184
|
return context;
|
|
103
1185
|
};
|
|
104
1186
|
|
|
105
|
-
// src/
|
|
106
|
-
var
|
|
107
|
-
var
|
|
108
|
-
var
|
|
109
|
-
var
|
|
110
|
-
|
|
1187
|
+
// src/components/CommentPin.tsx
|
|
1188
|
+
var import_react_core = require("@patternfly/react-core");
|
|
1189
|
+
var import_react_icons = require("@patternfly/react-icons");
|
|
1190
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
1191
|
+
var CommentPin = ({
|
|
1192
|
+
thread,
|
|
1193
|
+
onPinClick,
|
|
1194
|
+
isSelected = false
|
|
1195
|
+
}) => {
|
|
1196
|
+
const commentCount = thread.comments.length;
|
|
1197
|
+
const isPending = thread.syncStatus === "pending" || thread.syncStatus === "syncing";
|
|
1198
|
+
const isError = thread.syncStatus === "error";
|
|
1199
|
+
const pulseAnimation = isPending ? {
|
|
1200
|
+
animation: "pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite"
|
|
1201
|
+
} : {};
|
|
1202
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
1203
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("style", { children: `
|
|
1204
|
+
@keyframes pulse {
|
|
1205
|
+
0%, 100% { opacity: 1; }
|
|
1206
|
+
50% { opacity: 0.5; }
|
|
1207
|
+
}
|
|
1208
|
+
` }),
|
|
1209
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1210
|
+
import_react_core.Button,
|
|
1211
|
+
{
|
|
1212
|
+
id: `comment-pin-${thread.id}`,
|
|
1213
|
+
variant: "plain",
|
|
1214
|
+
"aria-label": `Comment thread with ${commentCount} ${commentCount === 1 ? "comment" : "comments"}${isError ? " - sync error" : ""}${isPending ? " - syncing" : ""}`,
|
|
1215
|
+
onClick: (e) => {
|
|
1216
|
+
e.stopPropagation();
|
|
1217
|
+
onPinClick();
|
|
1218
|
+
},
|
|
1219
|
+
style: {
|
|
1220
|
+
position: "absolute",
|
|
1221
|
+
left: `${thread.x}px`,
|
|
1222
|
+
top: `${thread.y}px`,
|
|
1223
|
+
transform: "translate(-50%, -50%)",
|
|
1224
|
+
width: "32px",
|
|
1225
|
+
height: "32px",
|
|
1226
|
+
borderRadius: "50%",
|
|
1227
|
+
backgroundColor: isError ? "#A30000" : isPending ? "#F0AB00" : "#C9190B",
|
|
1228
|
+
color: "white",
|
|
1229
|
+
border: isSelected ? "3px solid #0066CC" : "2px solid white",
|
|
1230
|
+
boxShadow: isSelected ? "0 0 0 2px #0066CC, 0 4px 12px rgba(0, 0, 0, 0.4)" : "0 2px 8px rgba(0, 0, 0, 0.3)",
|
|
1231
|
+
padding: 0,
|
|
1232
|
+
display: "flex",
|
|
1233
|
+
alignItems: "center",
|
|
1234
|
+
justifyContent: "center",
|
|
1235
|
+
cursor: "pointer",
|
|
1236
|
+
zIndex: isSelected ? 1001 : 1e3,
|
|
1237
|
+
transition: "all 0.2s ease",
|
|
1238
|
+
fontSize: commentCount > 1 ? "0.7rem" : void 0,
|
|
1239
|
+
...pulseAnimation
|
|
1240
|
+
},
|
|
1241
|
+
children: isError ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_icons.ExclamationTriangleIcon, { style: { fontSize: "1rem" } }) : commentCount === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontWeight: "bold", fontSize: "0.75rem" }, children: "0" }) : commentCount === 1 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_icons.CommentIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontWeight: "bold" }, children: commentCount })
|
|
1242
|
+
}
|
|
1243
|
+
)
|
|
1244
|
+
] });
|
|
111
1245
|
};
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
1246
|
+
|
|
1247
|
+
// src/components/CommentOverlay.tsx
|
|
1248
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
1249
|
+
var CommentOverlay = ({
|
|
1250
|
+
selectedThreadId,
|
|
1251
|
+
onThreadSelect
|
|
1252
|
+
}) => {
|
|
1253
|
+
const location = (0, import_react_router_dom.useLocation)();
|
|
1254
|
+
const { showPins, enableCommenting, addThread, getThreadsForRoute } = useComments();
|
|
1255
|
+
const { currentVersion } = useVersion();
|
|
1256
|
+
const overlayRef = React3.useRef(null);
|
|
1257
|
+
const currentRouteThreads = React3.useMemo(
|
|
1258
|
+
() => getThreadsForRoute(location.pathname, currentVersion),
|
|
1259
|
+
[getThreadsForRoute, location.pathname, currentVersion]
|
|
1260
|
+
);
|
|
1261
|
+
const handleOverlayClick = React3.useCallback(
|
|
1262
|
+
(event) => {
|
|
1263
|
+
if (!enableCommenting) return;
|
|
1264
|
+
if (event.target === overlayRef.current) {
|
|
1265
|
+
const rect = overlayRef.current.getBoundingClientRect();
|
|
1266
|
+
const x = event.clientX - rect.left;
|
|
1267
|
+
const y = event.clientY - rect.top;
|
|
1268
|
+
const newThreadId = addThread(x, y, location.pathname, currentVersion);
|
|
1269
|
+
onThreadSelect(newThreadId);
|
|
1270
|
+
}
|
|
1271
|
+
},
|
|
1272
|
+
[enableCommenting, addThread, location.pathname, currentVersion, onThreadSelect]
|
|
1273
|
+
);
|
|
1274
|
+
if (!showPins && !enableCommenting) {
|
|
1275
|
+
return null;
|
|
116
1276
|
}
|
|
117
|
-
return
|
|
1277
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1278
|
+
"div",
|
|
1279
|
+
{
|
|
1280
|
+
ref: overlayRef,
|
|
1281
|
+
id: "comment-overlay",
|
|
1282
|
+
onClick: handleOverlayClick,
|
|
1283
|
+
style: {
|
|
1284
|
+
position: "absolute",
|
|
1285
|
+
top: 0,
|
|
1286
|
+
left: 0,
|
|
1287
|
+
right: 0,
|
|
1288
|
+
bottom: 0,
|
|
1289
|
+
pointerEvents: enableCommenting ? "auto" : "none",
|
|
1290
|
+
cursor: enableCommenting ? "crosshair" : "default",
|
|
1291
|
+
zIndex: 999
|
|
1292
|
+
},
|
|
1293
|
+
children: showPins && currentRouteThreads.map((thread) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1294
|
+
"div",
|
|
1295
|
+
{
|
|
1296
|
+
style: { pointerEvents: "auto" },
|
|
1297
|
+
onClick: (e) => e.stopPropagation(),
|
|
1298
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1299
|
+
CommentPin,
|
|
1300
|
+
{
|
|
1301
|
+
thread,
|
|
1302
|
+
onPinClick: () => onThreadSelect(thread.id),
|
|
1303
|
+
isSelected: thread.id === selectedThreadId
|
|
1304
|
+
}
|
|
1305
|
+
)
|
|
1306
|
+
},
|
|
1307
|
+
thread.id
|
|
1308
|
+
))
|
|
1309
|
+
}
|
|
1310
|
+
);
|
|
118
1311
|
};
|
|
119
1312
|
|
|
1313
|
+
// src/components/CommentDrawer.tsx
|
|
1314
|
+
var React6 = __toESM(require("react"));
|
|
1315
|
+
var import_react_core2 = require("@patternfly/react-core");
|
|
1316
|
+
var import_react_icons2 = require("@patternfly/react-icons");
|
|
1317
|
+
var import_react_router_dom2 = require("react-router-dom");
|
|
1318
|
+
|
|
120
1319
|
// src/contexts/GitLabAuthContext.tsx
|
|
121
|
-
var
|
|
122
|
-
var
|
|
123
|
-
var GitLabAuthContext =
|
|
1320
|
+
var React4 = __toESM(require("react"));
|
|
1321
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
1322
|
+
var GitLabAuthContext = React4.createContext(void 0);
|
|
1323
|
+
var GITLAB_TOKEN_KEY = "gitlab_access_token";
|
|
1324
|
+
var GITLAB_USER_KEY = "gitlab_user";
|
|
124
1325
|
var GitLabAuthProvider = ({ children }) => {
|
|
125
|
-
|
|
1326
|
+
const [user, setUser] = React4.useState(null);
|
|
1327
|
+
React4.useEffect(() => {
|
|
1328
|
+
try {
|
|
1329
|
+
const token = localStorage.getItem(GITLAB_TOKEN_KEY);
|
|
1330
|
+
const userStr = localStorage.getItem(GITLAB_USER_KEY);
|
|
1331
|
+
if (token && userStr) {
|
|
1332
|
+
setUser(JSON.parse(userStr));
|
|
1333
|
+
}
|
|
1334
|
+
} catch {
|
|
1335
|
+
}
|
|
1336
|
+
}, []);
|
|
1337
|
+
React4.useEffect(() => {
|
|
1338
|
+
const handleCallback = () => {
|
|
1339
|
+
const hash = window.location.hash;
|
|
1340
|
+
if (hash.includes("#/auth-callback")) {
|
|
1341
|
+
const params = new URLSearchParams(hash.split("?")[1]);
|
|
1342
|
+
const token = params.get("gitlab_token");
|
|
1343
|
+
const username = params.get("gitlab_username") || void 0;
|
|
1344
|
+
const avatar = params.get("gitlab_avatar") || void 0;
|
|
1345
|
+
if (token) {
|
|
1346
|
+
localStorage.setItem(GITLAB_TOKEN_KEY, token);
|
|
1347
|
+
const u = { username, avatar };
|
|
1348
|
+
localStorage.setItem(GITLAB_USER_KEY, JSON.stringify(u));
|
|
1349
|
+
setUser(u);
|
|
1350
|
+
window.location.hash = "/";
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
};
|
|
1354
|
+
handleCallback();
|
|
1355
|
+
}, []);
|
|
1356
|
+
const getToken = React4.useCallback(() => {
|
|
1357
|
+
try {
|
|
1358
|
+
return localStorage.getItem(GITLAB_TOKEN_KEY);
|
|
1359
|
+
} catch {
|
|
1360
|
+
return null;
|
|
1361
|
+
}
|
|
1362
|
+
}, []);
|
|
1363
|
+
const login = () => {
|
|
1364
|
+
const clientId = process.env.VITE_GITLAB_CLIENT_ID;
|
|
1365
|
+
const baseUrl = process.env.VITE_GITLAB_BASE_URL || "https://gitlab.com";
|
|
1366
|
+
if (!clientId) {
|
|
1367
|
+
alert("GitLab login is not configured (missing VITE_GITLAB_CLIENT_ID).");
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
const redirectUri = `${window.location.origin}/api/auth/callback`;
|
|
1371
|
+
const scope = encodeURIComponent("read_user api");
|
|
1372
|
+
const url = `${baseUrl}/oauth/authorize?client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${scope}`;
|
|
1373
|
+
window.location.href = url;
|
|
1374
|
+
};
|
|
1375
|
+
const logout = () => {
|
|
1376
|
+
try {
|
|
1377
|
+
localStorage.removeItem(GITLAB_TOKEN_KEY);
|
|
1378
|
+
localStorage.removeItem(GITLAB_USER_KEY);
|
|
1379
|
+
} finally {
|
|
1380
|
+
setUser(null);
|
|
1381
|
+
}
|
|
1382
|
+
};
|
|
1383
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1384
|
+
GitLabAuthContext.Provider,
|
|
1385
|
+
{
|
|
1386
|
+
value: {
|
|
1387
|
+
user,
|
|
1388
|
+
isAuthenticated: !!getToken(),
|
|
1389
|
+
login,
|
|
1390
|
+
logout,
|
|
1391
|
+
getToken
|
|
1392
|
+
},
|
|
1393
|
+
children
|
|
1394
|
+
}
|
|
1395
|
+
);
|
|
126
1396
|
};
|
|
127
1397
|
var useGitLabAuth = () => {
|
|
128
|
-
const
|
|
129
|
-
if (!
|
|
130
|
-
throw new Error("useGitLabAuth must be used within GitLabAuthProvider");
|
|
1398
|
+
const ctx = React4.useContext(GitLabAuthContext);
|
|
1399
|
+
if (!ctx) {
|
|
1400
|
+
throw new Error("useGitLabAuth must be used within a GitLabAuthProvider");
|
|
131
1401
|
}
|
|
132
|
-
return
|
|
1402
|
+
return ctx;
|
|
133
1403
|
};
|
|
134
1404
|
|
|
135
1405
|
// src/contexts/AIContext.tsx
|
|
136
|
-
var
|
|
137
|
-
var
|
|
138
|
-
var AIContext =
|
|
1406
|
+
var React5 = __toESM(require("react"));
|
|
1407
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
1408
|
+
var AIContext = React5.createContext(void 0);
|
|
1409
|
+
var STORAGE_KEY2 = "apollo-ai-chat-history";
|
|
1410
|
+
var VISIBILITY_KEY = "apollo-ai-chatbot-visible";
|
|
139
1411
|
var AIProvider = ({ children }) => {
|
|
140
|
-
|
|
1412
|
+
const [isChatbotVisible, setIsChatbotVisible] = React5.useState(() => {
|
|
1413
|
+
try {
|
|
1414
|
+
const stored = localStorage.getItem(VISIBILITY_KEY);
|
|
1415
|
+
return stored === "true";
|
|
1416
|
+
} catch (error) {
|
|
1417
|
+
return false;
|
|
1418
|
+
}
|
|
1419
|
+
});
|
|
1420
|
+
const [messages, setMessages] = React5.useState(() => {
|
|
1421
|
+
try {
|
|
1422
|
+
const stored = localStorage.getItem(STORAGE_KEY2);
|
|
1423
|
+
if (stored) {
|
|
1424
|
+
return JSON.parse(stored);
|
|
1425
|
+
}
|
|
1426
|
+
return [];
|
|
1427
|
+
} catch (error) {
|
|
1428
|
+
console.error("Failed to load AI chat history from localStorage:", error);
|
|
1429
|
+
return [];
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1432
|
+
const [isLoading, setIsLoading] = React5.useState(false);
|
|
1433
|
+
React5.useEffect(() => {
|
|
1434
|
+
try {
|
|
1435
|
+
localStorage.setItem(VISIBILITY_KEY, String(isChatbotVisible));
|
|
1436
|
+
} catch (error) {
|
|
1437
|
+
console.error("Failed to save chatbot visibility to localStorage:", error);
|
|
1438
|
+
}
|
|
1439
|
+
}, [isChatbotVisible]);
|
|
1440
|
+
React5.useEffect(() => {
|
|
1441
|
+
try {
|
|
1442
|
+
localStorage.setItem(STORAGE_KEY2, JSON.stringify(messages));
|
|
1443
|
+
} catch (error) {
|
|
1444
|
+
console.error("Failed to save AI chat history to localStorage:", error);
|
|
1445
|
+
}
|
|
1446
|
+
}, [messages]);
|
|
1447
|
+
const toggleChatbot = React5.useCallback(() => {
|
|
1448
|
+
setIsChatbotVisible((prev) => !prev);
|
|
1449
|
+
}, []);
|
|
1450
|
+
const sendMessage = React5.useCallback(async (content, commentContext) => {
|
|
1451
|
+
if (!content.trim()) return;
|
|
1452
|
+
const userMessage = {
|
|
1453
|
+
id: `user-${Date.now()}`,
|
|
1454
|
+
role: "user",
|
|
1455
|
+
content: content.trim(),
|
|
1456
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1457
|
+
};
|
|
1458
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
1459
|
+
setIsLoading(true);
|
|
1460
|
+
try {
|
|
1461
|
+
if (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1") {
|
|
1462
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
1463
|
+
const mockResponse = `**Mock AI Response** (local dev mode)
|
|
1464
|
+
|
|
1465
|
+
I analyzed your query: "${content.trim()}"
|
|
1466
|
+
|
|
1467
|
+
**Summary:**
|
|
1468
|
+
- Found ${commentContext?.threads?.length || 0} comment threads
|
|
1469
|
+
- Version: ${commentContext?.version || "unknown"}
|
|
1470
|
+
- Page: ${commentContext?.route || "unknown"}
|
|
1471
|
+
|
|
1472
|
+
*Note: This is a mock response. Deploy to production to test the real AI.*`;
|
|
1473
|
+
const botMessage2 = {
|
|
1474
|
+
id: `bot-${Date.now()}`,
|
|
1475
|
+
role: "bot",
|
|
1476
|
+
content: mockResponse,
|
|
1477
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1478
|
+
};
|
|
1479
|
+
setMessages((prev) => [...prev, botMessage2]);
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
const response = await fetch("/api/ai-assistant", {
|
|
1483
|
+
method: "POST",
|
|
1484
|
+
headers: {
|
|
1485
|
+
"Content-Type": "application/json"
|
|
1486
|
+
},
|
|
1487
|
+
body: JSON.stringify({
|
|
1488
|
+
query: content.trim(),
|
|
1489
|
+
threads: commentContext?.threads || [],
|
|
1490
|
+
version: commentContext?.version || "unknown",
|
|
1491
|
+
route: commentContext?.route
|
|
1492
|
+
})
|
|
1493
|
+
});
|
|
1494
|
+
if (!response.ok) {
|
|
1495
|
+
const errorData = await response.json();
|
|
1496
|
+
throw new Error(errorData.message || `API error: ${response.status}`);
|
|
1497
|
+
}
|
|
1498
|
+
const data = await response.json();
|
|
1499
|
+
const botMessage = {
|
|
1500
|
+
id: `bot-${Date.now()}`,
|
|
1501
|
+
role: "bot",
|
|
1502
|
+
content: data.message || "No response received.",
|
|
1503
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1504
|
+
};
|
|
1505
|
+
setMessages((prev) => [...prev, botMessage]);
|
|
1506
|
+
} catch (error) {
|
|
1507
|
+
console.error("Failed to send message:", error);
|
|
1508
|
+
const errorMessage = {
|
|
1509
|
+
id: `bot-error-${Date.now()}`,
|
|
1510
|
+
role: "bot",
|
|
1511
|
+
content: error instanceof Error ? `Sorry, I encountered an error: ${error.message}` : "Sorry, something went wrong. Please try again.",
|
|
1512
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1513
|
+
};
|
|
1514
|
+
setMessages((prev) => [...prev, errorMessage]);
|
|
1515
|
+
} finally {
|
|
1516
|
+
setIsLoading(false);
|
|
1517
|
+
}
|
|
1518
|
+
}, []);
|
|
1519
|
+
const clearHistory = React5.useCallback(() => {
|
|
1520
|
+
setMessages([]);
|
|
1521
|
+
try {
|
|
1522
|
+
localStorage.removeItem(STORAGE_KEY2);
|
|
1523
|
+
} catch (error) {
|
|
1524
|
+
console.error("Failed to clear chat history from localStorage:", error);
|
|
1525
|
+
}
|
|
1526
|
+
}, []);
|
|
1527
|
+
const value = React5.useMemo(
|
|
1528
|
+
() => ({
|
|
1529
|
+
isChatbotVisible,
|
|
1530
|
+
messages,
|
|
1531
|
+
isLoading,
|
|
1532
|
+
toggleChatbot,
|
|
1533
|
+
sendMessage,
|
|
1534
|
+
clearHistory
|
|
1535
|
+
}),
|
|
1536
|
+
[isChatbotVisible, messages, isLoading, toggleChatbot, sendMessage, clearHistory]
|
|
1537
|
+
);
|
|
1538
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AIContext.Provider, { value, children });
|
|
141
1539
|
};
|
|
142
1540
|
var useAIContext = () => {
|
|
143
|
-
const context =
|
|
1541
|
+
const context = React5.useContext(AIContext);
|
|
144
1542
|
if (!context) {
|
|
145
|
-
throw new Error("useAIContext must be used within AIProvider");
|
|
1543
|
+
throw new Error("useAIContext must be used within an AIProvider");
|
|
146
1544
|
}
|
|
147
1545
|
return context;
|
|
148
1546
|
};
|
|
149
1547
|
|
|
150
|
-
// src/
|
|
151
|
-
var
|
|
152
|
-
|
|
1548
|
+
// src/components/CommentDrawer.tsx
|
|
1549
|
+
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
1550
|
+
var CommentDrawer = ({
|
|
1551
|
+
children,
|
|
1552
|
+
selectedThreadId,
|
|
1553
|
+
onThreadSelect
|
|
1554
|
+
}) => {
|
|
1555
|
+
const location = (0, import_react_router_dom2.useLocation)();
|
|
1556
|
+
const {
|
|
1557
|
+
getThreadsForRoute,
|
|
1558
|
+
addReply,
|
|
1559
|
+
updateComment,
|
|
1560
|
+
deleteComment,
|
|
1561
|
+
deleteThread,
|
|
1562
|
+
enableCommenting,
|
|
1563
|
+
syncFromGitHub,
|
|
1564
|
+
isSyncing,
|
|
1565
|
+
retrySync,
|
|
1566
|
+
hasPendingSync
|
|
1567
|
+
} = useComments();
|
|
1568
|
+
const { currentVersion } = useVersion();
|
|
1569
|
+
const { isAuthenticated: isGitLabAuthenticated } = useGitLabAuth();
|
|
1570
|
+
const { sendMessage, toggleChatbot, isChatbotVisible } = useAIContext();
|
|
1571
|
+
const [editingCommentId, setEditingCommentId] = React6.useState(null);
|
|
1572
|
+
const [editText, setEditText] = React6.useState("");
|
|
1573
|
+
const [replyText, setReplyText] = React6.useState("");
|
|
1574
|
+
const replyTextAreaRef = React6.useRef(null);
|
|
1575
|
+
const [threadSummaries, setThreadSummaries] = React6.useState({});
|
|
1576
|
+
const [loadingSummary, setLoadingSummary] = React6.useState(false);
|
|
1577
|
+
const [summaryExpanded, setSummaryExpanded] = React6.useState(true);
|
|
1578
|
+
const currentRouteThreads = getThreadsForRoute(location.pathname, currentVersion);
|
|
1579
|
+
const selectedThread = currentRouteThreads.find((t) => t.id === selectedThreadId);
|
|
1580
|
+
const isDrawerOpen = selectedThreadId !== null && selectedThread !== void 0;
|
|
1581
|
+
const isUserAuthenticated = isGitHubConfigured() || isGitLabAuthenticated;
|
|
1582
|
+
React6.useEffect(() => {
|
|
1583
|
+
if (!isDrawerOpen || !enableCommenting) return;
|
|
1584
|
+
const timer = setTimeout(() => {
|
|
1585
|
+
replyTextAreaRef.current?.focus();
|
|
1586
|
+
}, 100);
|
|
1587
|
+
return () => clearTimeout(timer);
|
|
1588
|
+
}, [isDrawerOpen, enableCommenting, selectedThreadId]);
|
|
1589
|
+
React6.useEffect(() => {
|
|
1590
|
+
if (isGitHubConfigured() || isGitLabConfigured()) {
|
|
1591
|
+
syncFromGitHub(location.pathname).catch((err) => {
|
|
1592
|
+
console.error("Failed to sync from providers:", err);
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
}, [location.pathname]);
|
|
1596
|
+
const handleEdit = (commentId, text) => {
|
|
1597
|
+
setEditingCommentId(commentId);
|
|
1598
|
+
setEditText(text);
|
|
1599
|
+
};
|
|
1600
|
+
const handleSave = async (threadId, commentId) => {
|
|
1601
|
+
await updateComment(threadId, commentId, editText);
|
|
1602
|
+
setEditingCommentId(null);
|
|
1603
|
+
};
|
|
1604
|
+
const handleAddReply = async () => {
|
|
1605
|
+
if (selectedThreadId && replyText.trim()) {
|
|
1606
|
+
await addReply(selectedThreadId, replyText);
|
|
1607
|
+
setReplyText("");
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1610
|
+
const handleDeleteThread = async () => {
|
|
1611
|
+
if (selectedThreadId && window.confirm("Delete this entire thread and all its comments?")) {
|
|
1612
|
+
await deleteThread(selectedThreadId);
|
|
1613
|
+
onThreadSelect(null);
|
|
1614
|
+
}
|
|
1615
|
+
};
|
|
1616
|
+
const handleSummarizeThread = async () => {
|
|
1617
|
+
if (!selectedThread) return;
|
|
1618
|
+
setLoadingSummary(true);
|
|
1619
|
+
setSummaryExpanded(true);
|
|
1620
|
+
try {
|
|
1621
|
+
const response = await fetch("/api/ai-assistant", {
|
|
1622
|
+
method: "POST",
|
|
1623
|
+
headers: {
|
|
1624
|
+
"Content-Type": "application/json"
|
|
1625
|
+
},
|
|
1626
|
+
body: JSON.stringify({
|
|
1627
|
+
query: `Summarize the feedback in this thread (${selectedThread.comments.length} comments)`,
|
|
1628
|
+
threads: [selectedThread],
|
|
1629
|
+
version: currentVersion || "unknown",
|
|
1630
|
+
route: location.pathname
|
|
1631
|
+
})
|
|
1632
|
+
});
|
|
1633
|
+
if (!response.ok) {
|
|
1634
|
+
throw new Error(`API error: ${response.status}`);
|
|
1635
|
+
}
|
|
1636
|
+
const data = await response.json();
|
|
1637
|
+
const summary = data.message || "No summary available.";
|
|
1638
|
+
setThreadSummaries((prev) => ({
|
|
1639
|
+
...prev,
|
|
1640
|
+
[selectedThread.id]: summary
|
|
1641
|
+
}));
|
|
1642
|
+
} catch (error) {
|
|
1643
|
+
console.error("Failed to generate summary:", error);
|
|
1644
|
+
setThreadSummaries((prev) => ({
|
|
1645
|
+
...prev,
|
|
1646
|
+
[selectedThread.id]: "Failed to generate summary. Please try again."
|
|
1647
|
+
}));
|
|
1648
|
+
} finally {
|
|
1649
|
+
setLoadingSummary(false);
|
|
1650
|
+
}
|
|
1651
|
+
};
|
|
1652
|
+
const handleDeleteComment = async (threadId, commentId) => {
|
|
1653
|
+
if (window.confirm("Delete this comment?")) {
|
|
1654
|
+
await deleteComment(threadId, commentId);
|
|
1655
|
+
}
|
|
1656
|
+
};
|
|
1657
|
+
const handleSync = async () => {
|
|
1658
|
+
await syncFromGitHub(location.pathname);
|
|
1659
|
+
};
|
|
1660
|
+
const handleRetrySync = async () => {
|
|
1661
|
+
await retrySync();
|
|
1662
|
+
};
|
|
1663
|
+
const getSyncStatusLabel = (status) => {
|
|
1664
|
+
switch (status) {
|
|
1665
|
+
case "synced":
|
|
1666
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_core2.Label, { color: "green", icon: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_icons2.GithubIcon, {}), children: "Synced" });
|
|
1667
|
+
case "local":
|
|
1668
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_core2.Label, { color: "grey", children: "Local Only" });
|
|
1669
|
+
case "pending":
|
|
1670
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_core2.Label, { color: "blue", icon: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_core2.Spinner, { size: "sm" }), children: "Pending..." });
|
|
1671
|
+
case "syncing":
|
|
1672
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_core2.Label, { color: "blue", icon: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_core2.Spinner, { size: "sm" }), children: "Syncing..." });
|
|
1673
|
+
case "error":
|
|
1674
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_core2.Label, { color: "red", children: "Sync Error" });
|
|
1675
|
+
default:
|
|
1676
|
+
return null;
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
const getIssueLink = (provider, issueNumber) => {
|
|
1680
|
+
if (!issueNumber || !provider) return null;
|
|
1681
|
+
try {
|
|
1682
|
+
if (provider === "gitlab") {
|
|
1683
|
+
const baseUrl = process.env.VITE_GITLAB_BASE_URL || "https://gitlab.com";
|
|
1684
|
+
const projectPath = process.env.VITE_GITLAB_PROJECT_PATH;
|
|
1685
|
+
if (!projectPath) return null;
|
|
1686
|
+
return `${baseUrl}/${projectPath}/-/issues/${issueNumber}`;
|
|
1687
|
+
} else {
|
|
1688
|
+
const owner = process.env.VITE_GITHUB_OWNER;
|
|
1689
|
+
const repo = process.env.VITE_GITHUB_REPO;
|
|
1690
|
+
if (!owner || !repo) return null;
|
|
1691
|
+
return `https://github.com/${owner}/${repo}/issues/${issueNumber}`;
|
|
1692
|
+
}
|
|
1693
|
+
} catch (error) {
|
|
1694
|
+
return null;
|
|
1695
|
+
}
|
|
1696
|
+
};
|
|
1697
|
+
const formatDate = (isoDate) => {
|
|
1698
|
+
const date = new Date(isoDate);
|
|
1699
|
+
return date.toLocaleString(void 0, {
|
|
1700
|
+
month: "short",
|
|
1701
|
+
day: "numeric",
|
|
1702
|
+
hour: "2-digit",
|
|
1703
|
+
minute: "2-digit"
|
|
1704
|
+
});
|
|
1705
|
+
};
|
|
1706
|
+
const panelContent = /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_core2.DrawerPanelContent, { isResizable: true, defaultSize: "400px", minSize: "300px", children: [
|
|
1707
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_core2.DrawerHead, { children: [
|
|
1708
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "0.5rem", flex: 1 }, children: [
|
|
1709
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_core2.Title, { headingLevel: "h2", size: "xl", children: [
|
|
1710
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_icons2.CommentIcon, { style: { marginRight: "0.5rem", color: "#C9190B" } }),
|
|
1711
|
+
"Thread"
|
|
1712
|
+
] }),
|
|
1713
|
+
(isGitHubConfigured() || isGitLabConfigured()) && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
|
|
1714
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1715
|
+
import_react_core2.Button,
|
|
1716
|
+
{
|
|
1717
|
+
id: "sync-github-button",
|
|
1718
|
+
variant: "plain",
|
|
1719
|
+
size: "sm",
|
|
1720
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_icons2.SyncAltIcon, {}),
|
|
1721
|
+
onClick: handleSync,
|
|
1722
|
+
isDisabled: isSyncing,
|
|
1723
|
+
"aria-label": "Sync with remote",
|
|
1724
|
+
title: "Sync with remote",
|
|
1725
|
+
children: isSyncing ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_core2.Spinner, { size: "sm" }) : null
|
|
1726
|
+
}
|
|
1727
|
+
),
|
|
1728
|
+
hasPendingSync && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1729
|
+
import_react_core2.Button,
|
|
1730
|
+
{
|
|
1731
|
+
id: "retry-sync-button",
|
|
1732
|
+
variant: "plain",
|
|
1733
|
+
size: "sm",
|
|
1734
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_icons2.RedoIcon, {}),
|
|
1735
|
+
onClick: handleRetrySync,
|
|
1736
|
+
isDisabled: isSyncing,
|
|
1737
|
+
"aria-label": "Retry sync for pending threads",
|
|
1738
|
+
title: "Retry sync for pending threads"
|
|
1739
|
+
}
|
|
1740
|
+
)
|
|
1741
|
+
] })
|
|
1742
|
+
] }),
|
|
1743
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_core2.DrawerActions, { children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_core2.DrawerCloseButton, { onClick: () => onThreadSelect(null) }) })
|
|
1744
|
+
] }),
|
|
1745
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_core2.DrawerContentBody, { style: { padding: "1rem" }, children: !selectedThread ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_core2.EmptyState, { children: [
|
|
1746
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_icons2.CommentIcon, { style: { fontSize: "3rem", color: "var(--pf-v6-global--Color--200)", marginBottom: "1rem" } }),
|
|
1747
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_core2.Title, { headingLevel: "h3", size: "lg", children: "No thread selected" }),
|
|
1748
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_core2.EmptyStateBody, { children: "Click a pin to view its comments." })
|
|
1749
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "1rem" }, children: [
|
|
1750
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_core2.Card, { isCompact: true, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_core2.CardBody, { children: [
|
|
1751
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { fontSize: "0.875rem", marginBottom: "0.5rem" }, children: [
|
|
1752
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("strong", { children: "Location:" }),
|
|
1753
|
+
" (",
|
|
1754
|
+
Math.round(selectedThread.x),
|
|
1755
|
+
", ",
|
|
1756
|
+
Math.round(selectedThread.y),
|
|
1757
|
+
")"
|
|
1758
|
+
] }),
|
|
1759
|
+
selectedThread.version && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { fontSize: "0.875rem", marginBottom: "0.5rem" }, children: [
|
|
1760
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("strong", { children: "Version:" }),
|
|
1761
|
+
" ",
|
|
1762
|
+
selectedThread.version
|
|
1763
|
+
] }),
|
|
1764
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { fontSize: "0.875rem", marginBottom: "0.5rem" }, children: [
|
|
1765
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("strong", { children: "Comments:" }),
|
|
1766
|
+
" ",
|
|
1767
|
+
selectedThread.comments.length
|
|
1768
|
+
] }),
|
|
1769
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "0.5rem", marginBottom: "0.5rem" }, children: [
|
|
1770
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("strong", { style: { fontSize: "0.875rem" }, children: "Status:" }),
|
|
1771
|
+
getSyncStatusLabel(selectedThread.syncStatus)
|
|
1772
|
+
] }),
|
|
1773
|
+
selectedThread.issueNumber && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { fontSize: "0.875rem", marginBottom: "0.5rem" }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
1774
|
+
"a",
|
|
1775
|
+
{
|
|
1776
|
+
href: getIssueLink(selectedThread.provider, selectedThread.issueNumber) || "#",
|
|
1777
|
+
target: "_blank",
|
|
1778
|
+
rel: "noopener noreferrer",
|
|
1779
|
+
style: { display: "inline-flex", alignItems: "center", gap: "0.25rem" },
|
|
1780
|
+
children: [
|
|
1781
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_icons2.GithubIcon, {}),
|
|
1782
|
+
"Issue #",
|
|
1783
|
+
selectedThread.issueNumber,
|
|
1784
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_icons2.ExternalLinkAltIcon, { style: { fontSize: "0.75rem" } })
|
|
1785
|
+
]
|
|
1786
|
+
}
|
|
1787
|
+
) }),
|
|
1788
|
+
selectedThread.comments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1789
|
+
import_react_core2.Button,
|
|
1790
|
+
{
|
|
1791
|
+
id: `ai-summarize-thread-${selectedThread.id}`,
|
|
1792
|
+
variant: "secondary",
|
|
1793
|
+
size: "sm",
|
|
1794
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_icons2.MagicIcon, {}),
|
|
1795
|
+
onClick: handleSummarizeThread,
|
|
1796
|
+
isLoading: loadingSummary,
|
|
1797
|
+
isDisabled: loadingSummary,
|
|
1798
|
+
style: { marginTop: "0.5rem" },
|
|
1799
|
+
children: loadingSummary ? "Generating..." : "AI Summarize Thread"
|
|
1800
|
+
}
|
|
1801
|
+
),
|
|
1802
|
+
enableCommenting && isUserAuthenticated && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1803
|
+
import_react_core2.Button,
|
|
1804
|
+
{
|
|
1805
|
+
id: `delete-thread-${selectedThread.id}`,
|
|
1806
|
+
variant: "danger",
|
|
1807
|
+
size: "sm",
|
|
1808
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_icons2.TimesIcon, {}),
|
|
1809
|
+
onClick: handleDeleteThread,
|
|
1810
|
+
style: { marginTop: "0.5rem", marginLeft: selectedThread.comments.length > 0 ? "0.5rem" : "0" },
|
|
1811
|
+
children: "Delete Thread"
|
|
1812
|
+
}
|
|
1813
|
+
)
|
|
1814
|
+
] }) }),
|
|
1815
|
+
threadSummaries[selectedThread.id] && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1816
|
+
import_react_core2.Alert,
|
|
1817
|
+
{
|
|
1818
|
+
variant: "info",
|
|
1819
|
+
isInline: true,
|
|
1820
|
+
title: "AI Summary",
|
|
1821
|
+
actionClose: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1822
|
+
import_react_core2.Button,
|
|
1823
|
+
{
|
|
1824
|
+
variant: "plain",
|
|
1825
|
+
onClick: () => {
|
|
1826
|
+
const newSummaries = { ...threadSummaries };
|
|
1827
|
+
delete newSummaries[selectedThread.id];
|
|
1828
|
+
setThreadSummaries(newSummaries);
|
|
1829
|
+
},
|
|
1830
|
+
"aria-label": "Clear summary",
|
|
1831
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_icons2.TimesIcon, {})
|
|
1832
|
+
}
|
|
1833
|
+
),
|
|
1834
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1835
|
+
import_react_core2.ExpandableSection,
|
|
1836
|
+
{
|
|
1837
|
+
toggleText: summaryExpanded ? "Hide summary" : "Show summary",
|
|
1838
|
+
onToggle: (_event, isExpanded) => setSummaryExpanded(isExpanded),
|
|
1839
|
+
isExpanded: summaryExpanded,
|
|
1840
|
+
isIndented: true,
|
|
1841
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { fontSize: "0.875rem", lineHeight: "1.5" }, children: threadSummaries[selectedThread.id] })
|
|
1842
|
+
}
|
|
1843
|
+
)
|
|
1844
|
+
}
|
|
1845
|
+
),
|
|
1846
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_core2.Divider, {}),
|
|
1847
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: "1rem" }, children: selectedThread.comments.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_core2.EmptyState, { children: [
|
|
1848
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_core2.Title, { headingLevel: "h4", size: "md", children: "No comments yet" }),
|
|
1849
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_core2.EmptyStateBody, { children: enableCommenting ? "Add a reply below to start the conversation." : "Enable commenting to add replies." })
|
|
1850
|
+
] }) : selectedThread.comments.map((comment, index) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_core2.Card, { isCompact: true, children: [
|
|
1851
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_core2.CardTitle, { children: [
|
|
1852
|
+
"Comment #",
|
|
1853
|
+
index + 1,
|
|
1854
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { fontSize: "0.75rem", color: "var(--pf-v6-global--Color--200)", fontWeight: "normal" }, children: [
|
|
1855
|
+
comment.author && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { style: { marginRight: "0.5rem" }, children: [
|
|
1856
|
+
"@",
|
|
1857
|
+
comment.author
|
|
1858
|
+
] }),
|
|
1859
|
+
formatDate(comment.createdAt)
|
|
1860
|
+
] })
|
|
1861
|
+
] }),
|
|
1862
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_core2.CardBody, { children: editingCommentId === comment.id ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
|
|
1863
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1864
|
+
import_react_core2.TextArea,
|
|
1865
|
+
{
|
|
1866
|
+
id: `edit-comment-${comment.id}`,
|
|
1867
|
+
value: editText,
|
|
1868
|
+
onChange: (_event, value) => setEditText(value),
|
|
1869
|
+
rows: 3,
|
|
1870
|
+
style: { marginBottom: "0.5rem" },
|
|
1871
|
+
onKeyDown: (e) => {
|
|
1872
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
1873
|
+
e.preventDefault();
|
|
1874
|
+
handleSave(selectedThread.id, comment.id);
|
|
1875
|
+
}
|
|
1876
|
+
if (e.key === "Escape") {
|
|
1877
|
+
setEditingCommentId(null);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
),
|
|
1882
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { display: "flex", gap: "0.5rem" }, children: [
|
|
1883
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1884
|
+
import_react_core2.Button,
|
|
1885
|
+
{
|
|
1886
|
+
id: `save-comment-${comment.id}`,
|
|
1887
|
+
variant: "primary",
|
|
1888
|
+
size: "sm",
|
|
1889
|
+
onClick: () => handleSave(selectedThread.id, comment.id),
|
|
1890
|
+
children: "Save"
|
|
1891
|
+
}
|
|
1892
|
+
),
|
|
1893
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1894
|
+
import_react_core2.Button,
|
|
1895
|
+
{
|
|
1896
|
+
id: `cancel-edit-${comment.id}`,
|
|
1897
|
+
variant: "link",
|
|
1898
|
+
size: "sm",
|
|
1899
|
+
onClick: () => setEditingCommentId(null),
|
|
1900
|
+
children: "Cancel"
|
|
1901
|
+
}
|
|
1902
|
+
)
|
|
1903
|
+
] })
|
|
1904
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
|
|
1905
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { marginBottom: "0.75rem", whiteSpace: "pre-wrap" }, children: comment.text || /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("em", { style: { color: "var(--pf-v6-global--Color--200)" }, children: "No text" }) }),
|
|
1906
|
+
enableCommenting && isUserAuthenticated && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { display: "flex", gap: "0.5rem" }, children: [
|
|
1907
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1908
|
+
import_react_core2.Button,
|
|
1909
|
+
{
|
|
1910
|
+
id: `edit-comment-btn-${comment.id}`,
|
|
1911
|
+
variant: "secondary",
|
|
1912
|
+
size: "sm",
|
|
1913
|
+
onClick: () => handleEdit(comment.id, comment.text),
|
|
1914
|
+
children: "Edit"
|
|
1915
|
+
}
|
|
1916
|
+
),
|
|
1917
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1918
|
+
import_react_core2.Button,
|
|
1919
|
+
{
|
|
1920
|
+
id: `delete-comment-btn-${comment.id}`,
|
|
1921
|
+
variant: "danger",
|
|
1922
|
+
size: "sm",
|
|
1923
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_icons2.TimesIcon, {}),
|
|
1924
|
+
onClick: () => handleDeleteComment(selectedThread.id, comment.id),
|
|
1925
|
+
children: "Delete"
|
|
1926
|
+
}
|
|
1927
|
+
)
|
|
1928
|
+
] })
|
|
1929
|
+
] }) })
|
|
1930
|
+
] }, comment.id)) }),
|
|
1931
|
+
enableCommenting && isUserAuthenticated && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
|
|
1932
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_core2.Divider, {}),
|
|
1933
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_core2.Card, { isCompact: true, children: [
|
|
1934
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_core2.CardTitle, { children: [
|
|
1935
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_icons2.PlusCircleIcon, { style: { marginRight: "0.5rem" } }),
|
|
1936
|
+
"Add Reply"
|
|
1937
|
+
] }),
|
|
1938
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_core2.CardBody, { children: [
|
|
1939
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1940
|
+
import_react_core2.TextArea,
|
|
1941
|
+
{
|
|
1942
|
+
ref: replyTextAreaRef,
|
|
1943
|
+
id: `reply-textarea-${selectedThread.id}`,
|
|
1944
|
+
value: replyText,
|
|
1945
|
+
onChange: (_event, value) => setReplyText(value),
|
|
1946
|
+
placeholder: "Enter your reply...",
|
|
1947
|
+
rows: 3,
|
|
1948
|
+
style: { marginBottom: "0.5rem" },
|
|
1949
|
+
onKeyDown: (e) => {
|
|
1950
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
1951
|
+
e.preventDefault();
|
|
1952
|
+
handleAddReply();
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
),
|
|
1957
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1958
|
+
import_react_core2.Button,
|
|
1959
|
+
{
|
|
1960
|
+
id: `add-reply-${selectedThread.id}`,
|
|
1961
|
+
variant: "primary",
|
|
1962
|
+
size: "sm",
|
|
1963
|
+
onClick: handleAddReply,
|
|
1964
|
+
isDisabled: !replyText.trim(),
|
|
1965
|
+
children: "Add Reply"
|
|
1966
|
+
}
|
|
1967
|
+
)
|
|
1968
|
+
] })
|
|
1969
|
+
] })
|
|
1970
|
+
] })
|
|
1971
|
+
] }) })
|
|
1972
|
+
] });
|
|
1973
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_core2.Drawer, { isExpanded: isDrawerOpen, isInline: true, position: "right", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_core2.DrawerContent, { panelContent, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_core2.DrawerContentBody, { children }) }) });
|
|
153
1974
|
};
|
|
154
|
-
|
|
155
|
-
|
|
1975
|
+
|
|
1976
|
+
// src/components/AIAssistant.tsx
|
|
1977
|
+
var import_chatbot2 = require("@patternfly/chatbot");
|
|
1978
|
+
|
|
1979
|
+
// src/components/AIChatPanel.tsx
|
|
1980
|
+
var React7 = __toESM(require("react"));
|
|
1981
|
+
var import_chatbot = require("@patternfly/chatbot");
|
|
1982
|
+
var import_react_core3 = require("@patternfly/react-core");
|
|
1983
|
+
var import_react_icons3 = require("@patternfly/react-icons");
|
|
1984
|
+
var import_react_router_dom3 = require("react-router-dom");
|
|
1985
|
+
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
1986
|
+
var AIChatPanel = () => {
|
|
1987
|
+
const { messages, isLoading, sendMessage, toggleChatbot, clearHistory } = useAIContext();
|
|
1988
|
+
const { currentVersion } = useVersion();
|
|
1989
|
+
const { threads } = useComments();
|
|
1990
|
+
const location = (0, import_react_router_dom3.useLocation)();
|
|
1991
|
+
const [inputValue, setInputValue] = React7.useState("");
|
|
1992
|
+
const commentCount = React7.useMemo(() => {
|
|
1993
|
+
return threads.filter((t) => t.version === currentVersion).reduce((acc, t) => acc + t.comments.length, 0);
|
|
1994
|
+
}, [threads, currentVersion]);
|
|
1995
|
+
const handleSendMessage = async () => {
|
|
1996
|
+
if (inputValue.trim() && !isLoading) {
|
|
1997
|
+
await sendMessage(inputValue, {
|
|
1998
|
+
threads,
|
|
1999
|
+
version: currentVersion || "unknown",
|
|
2000
|
+
route: location.pathname
|
|
2001
|
+
});
|
|
2002
|
+
setInputValue("");
|
|
2003
|
+
}
|
|
2004
|
+
};
|
|
2005
|
+
const handleKeyDown = (event) => {
|
|
2006
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
2007
|
+
event.preventDefault();
|
|
2008
|
+
handleSendMessage();
|
|
2009
|
+
}
|
|
2010
|
+
};
|
|
2011
|
+
const handleQuickAction = (action) => {
|
|
2012
|
+
setInputValue(action);
|
|
2013
|
+
};
|
|
2014
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
2015
|
+
"div",
|
|
2016
|
+
{
|
|
2017
|
+
style: {
|
|
2018
|
+
position: "fixed",
|
|
2019
|
+
bottom: "80px",
|
|
2020
|
+
right: "20px",
|
|
2021
|
+
width: "400px",
|
|
2022
|
+
maxHeight: "600px",
|
|
2023
|
+
zIndex: 2e3,
|
|
2024
|
+
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)"
|
|
2025
|
+
},
|
|
2026
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_chatbot.Chatbot, { displayMode: import_chatbot.ChatbotDisplayMode.default, children: [
|
|
2027
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_chatbot.ChatbotHeader, { children: [
|
|
2028
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_chatbot.ChatbotHeaderMain, { children: [
|
|
2029
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_chatbot.ChatbotHeaderTitle, { children: "\u{1F916} AI Feedback Assistant" }),
|
|
2030
|
+
currentVersion && commentCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { style: { marginLeft: "0.5rem" }, children: [
|
|
2031
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_core3.Label, { color: "blue", isCompact: true, children: [
|
|
2032
|
+
"Version ",
|
|
2033
|
+
currentVersion
|
|
2034
|
+
] }),
|
|
2035
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_core3.Label, { color: "grey", isCompact: true, style: { marginLeft: "0.25rem" }, children: [
|
|
2036
|
+
commentCount,
|
|
2037
|
+
" comments"
|
|
2038
|
+
] })
|
|
2039
|
+
] })
|
|
2040
|
+
] }),
|
|
2041
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_chatbot.ChatbotHeaderActions, { children: [
|
|
2042
|
+
messages.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
2043
|
+
import_react_core3.Button,
|
|
2044
|
+
{
|
|
2045
|
+
variant: "plain",
|
|
2046
|
+
onClick: clearHistory,
|
|
2047
|
+
"aria-label": "Clear chat history",
|
|
2048
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_icons3.TrashIcon, {})
|
|
2049
|
+
}
|
|
2050
|
+
),
|
|
2051
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
2052
|
+
import_react_core3.Button,
|
|
2053
|
+
{
|
|
2054
|
+
variant: "plain",
|
|
2055
|
+
onClick: toggleChatbot,
|
|
2056
|
+
"aria-label": "Close chatbot",
|
|
2057
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_icons3.TimesIcon, {})
|
|
2058
|
+
}
|
|
2059
|
+
)
|
|
2060
|
+
] })
|
|
2061
|
+
] }),
|
|
2062
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_chatbot.ChatbotContent, { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_chatbot.MessageBox, { children: messages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
2063
|
+
import_chatbot.ChatbotWelcomePrompt,
|
|
2064
|
+
{
|
|
2065
|
+
title: "Welcome to AI Feedback Assistant",
|
|
2066
|
+
description: "Ask me about comments across your prototype. I can help you:",
|
|
2067
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_core3.ActionList, { children: [
|
|
2068
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_core3.ActionListItem, { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
2069
|
+
import_react_core3.Button,
|
|
2070
|
+
{
|
|
2071
|
+
variant: "link",
|
|
2072
|
+
isInline: true,
|
|
2073
|
+
onClick: () => handleQuickAction("What feedback was left in the last week?"),
|
|
2074
|
+
children: "Summarize recent feedback"
|
|
2075
|
+
}
|
|
2076
|
+
) }),
|
|
2077
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_core3.ActionListItem, { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
2078
|
+
import_react_core3.Button,
|
|
2079
|
+
{
|
|
2080
|
+
variant: "link",
|
|
2081
|
+
isInline: true,
|
|
2082
|
+
onClick: () => handleQuickAction("Show me all accessibility issues"),
|
|
2083
|
+
children: "Find accessibility issues"
|
|
2084
|
+
}
|
|
2085
|
+
) }),
|
|
2086
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_core3.ActionListItem, { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
2087
|
+
import_react_core3.Button,
|
|
2088
|
+
{
|
|
2089
|
+
variant: "link",
|
|
2090
|
+
isInline: true,
|
|
2091
|
+
onClick: () => handleQuickAction("What are the main navigation problems?"),
|
|
2092
|
+
children: "Identify navigation problems"
|
|
2093
|
+
}
|
|
2094
|
+
) }),
|
|
2095
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_core3.ActionListItem, { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
2096
|
+
import_react_core3.Button,
|
|
2097
|
+
{
|
|
2098
|
+
variant: "link",
|
|
2099
|
+
isInline: true,
|
|
2100
|
+
onClick: () => handleQuickAction("Which page has the most comments?"),
|
|
2101
|
+
children: "Analyze comment distribution"
|
|
2102
|
+
}
|
|
2103
|
+
) })
|
|
2104
|
+
] })
|
|
2105
|
+
}
|
|
2106
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
|
|
2107
|
+
messages.map((msg) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
2108
|
+
import_chatbot.Message,
|
|
2109
|
+
{
|
|
2110
|
+
name: msg.role === "user" ? "You" : "AI Assistant",
|
|
2111
|
+
role: msg.role === "user" ? "user" : "bot",
|
|
2112
|
+
avatar: msg.role === "user" ? "/images/user_advatar.jpg" : "/images/halefavicon.png",
|
|
2113
|
+
content: msg.content,
|
|
2114
|
+
timestamp: new Date(msg.timestamp).toLocaleString(void 0, {
|
|
2115
|
+
month: "short",
|
|
2116
|
+
day: "numeric",
|
|
2117
|
+
hour: "2-digit",
|
|
2118
|
+
minute: "2-digit"
|
|
2119
|
+
})
|
|
2120
|
+
},
|
|
2121
|
+
msg.id
|
|
2122
|
+
)),
|
|
2123
|
+
isLoading && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
2124
|
+
import_chatbot.Message,
|
|
2125
|
+
{
|
|
2126
|
+
name: "AI Assistant",
|
|
2127
|
+
role: "bot",
|
|
2128
|
+
avatar: "/images/halefavicon.png",
|
|
2129
|
+
content: "\u23F3 Analyzing comments..."
|
|
2130
|
+
}
|
|
2131
|
+
)
|
|
2132
|
+
] }) }) }),
|
|
2133
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_chatbot.ChatbotFooter, { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { display: "flex", gap: "0.5rem", alignItems: "flex-end", width: "100%" }, children: [
|
|
2134
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
2135
|
+
import_react_core3.TextArea,
|
|
2136
|
+
{
|
|
2137
|
+
id: "ai-chat-input",
|
|
2138
|
+
value: inputValue,
|
|
2139
|
+
onChange: (_event, value) => setInputValue(value),
|
|
2140
|
+
onKeyDown: handleKeyDown,
|
|
2141
|
+
placeholder: "Ask about feedback...",
|
|
2142
|
+
"aria-label": "Message input",
|
|
2143
|
+
rows: 2,
|
|
2144
|
+
style: { flex: 1 },
|
|
2145
|
+
isDisabled: isLoading
|
|
2146
|
+
}
|
|
2147
|
+
),
|
|
2148
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
2149
|
+
import_react_core3.Button,
|
|
2150
|
+
{
|
|
2151
|
+
variant: "primary",
|
|
2152
|
+
onClick: handleSendMessage,
|
|
2153
|
+
isDisabled: !inputValue.trim() || isLoading,
|
|
2154
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_icons3.PaperPlaneIcon, {}),
|
|
2155
|
+
"aria-label": "Send message"
|
|
2156
|
+
}
|
|
2157
|
+
)
|
|
2158
|
+
] }) })
|
|
2159
|
+
] })
|
|
2160
|
+
}
|
|
2161
|
+
);
|
|
156
2162
|
};
|
|
157
2163
|
|
|
158
|
-
// src/
|
|
159
|
-
var
|
|
160
|
-
|
|
2164
|
+
// src/components/AIAssistant.tsx
|
|
2165
|
+
var import_jsx_runtime9 = require("react/jsx-runtime");
|
|
2166
|
+
var AIAssistant = () => {
|
|
2167
|
+
const { isChatbotVisible, toggleChatbot } = useAIContext();
|
|
2168
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
|
|
2169
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
2170
|
+
"div",
|
|
2171
|
+
{
|
|
2172
|
+
style: {
|
|
2173
|
+
position: "fixed",
|
|
2174
|
+
bottom: "20px",
|
|
2175
|
+
right: "20px",
|
|
2176
|
+
zIndex: 2e3
|
|
2177
|
+
},
|
|
2178
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
2179
|
+
import_chatbot2.ChatbotToggle,
|
|
2180
|
+
{
|
|
2181
|
+
id: "ai-chatbot-toggle",
|
|
2182
|
+
isChatbotVisible,
|
|
2183
|
+
onToggleChatbot: toggleChatbot,
|
|
2184
|
+
toggleButtonLabel: "AI Assistant",
|
|
2185
|
+
tooltipLabel: "Get AI help analyzing feedback",
|
|
2186
|
+
style: {
|
|
2187
|
+
backgroundColor: "#C9190B",
|
|
2188
|
+
borderRadius: "50%",
|
|
2189
|
+
width: "56px",
|
|
2190
|
+
height: "56px",
|
|
2191
|
+
border: "2px solid white",
|
|
2192
|
+
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.3)",
|
|
2193
|
+
color: "white"
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
)
|
|
2197
|
+
}
|
|
2198
|
+
),
|
|
2199
|
+
isChatbotVisible && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(AIChatPanel, {})
|
|
2200
|
+
] });
|
|
161
2201
|
};
|
|
162
|
-
|
|
163
|
-
|
|
2202
|
+
|
|
2203
|
+
// src/contexts/GitHubAuthContext.tsx
|
|
2204
|
+
var React8 = __toESM(require("react"));
|
|
2205
|
+
var import_jsx_runtime10 = require("react/jsx-runtime");
|
|
2206
|
+
var GitHubAuthContext = React8.createContext(void 0);
|
|
2207
|
+
var GitHubAuthProvider = ({ children }) => {
|
|
2208
|
+
const [user, setUser] = React8.useState(null);
|
|
2209
|
+
React8.useEffect(() => {
|
|
2210
|
+
const storedUser = getAuthenticatedUser();
|
|
2211
|
+
if (storedUser) {
|
|
2212
|
+
setUser(storedUser);
|
|
2213
|
+
}
|
|
2214
|
+
}, []);
|
|
2215
|
+
React8.useEffect(() => {
|
|
2216
|
+
const handleOAuthCallback = () => {
|
|
2217
|
+
const hash = window.location.hash;
|
|
2218
|
+
if (hash.includes("#/auth-callback")) {
|
|
2219
|
+
const params = new URLSearchParams(hash.split("?")[1]);
|
|
2220
|
+
const token = params.get("token");
|
|
2221
|
+
const login2 = params.get("login");
|
|
2222
|
+
const avatar = params.get("avatar");
|
|
2223
|
+
if (token && login2 && avatar) {
|
|
2224
|
+
storeGitHubAuth(token, login2, decodeURIComponent(avatar));
|
|
2225
|
+
setUser({ login: login2, avatar: decodeURIComponent(avatar) });
|
|
2226
|
+
window.location.hash = "/";
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
};
|
|
2230
|
+
handleOAuthCallback();
|
|
2231
|
+
}, []);
|
|
2232
|
+
const login = () => {
|
|
2233
|
+
const clientId = process.env.VITE_GITHUB_CLIENT_ID;
|
|
2234
|
+
if (!clientId) {
|
|
2235
|
+
alert("GitHub login is not configured (missing VITE_GITHUB_CLIENT_ID).");
|
|
2236
|
+
return;
|
|
2237
|
+
}
|
|
2238
|
+
const redirectUri = `${window.location.origin}/api/github-oauth-callback`;
|
|
2239
|
+
const scope = "public_repo";
|
|
2240
|
+
const githubAuthUrl = `https://github.com/login/oauth/authorize?client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}`;
|
|
2241
|
+
console.log("\u{1F50D} GitHub OAuth redirect:", { clientId: "present", redirectUri });
|
|
2242
|
+
window.location.href = githubAuthUrl;
|
|
2243
|
+
};
|
|
2244
|
+
const logout = () => {
|
|
2245
|
+
clearGitHubAuth();
|
|
2246
|
+
setUser(null);
|
|
2247
|
+
};
|
|
2248
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
|
|
2249
|
+
GitHubAuthContext.Provider,
|
|
2250
|
+
{
|
|
2251
|
+
value: {
|
|
2252
|
+
user,
|
|
2253
|
+
isAuthenticated: !!user,
|
|
2254
|
+
login,
|
|
2255
|
+
logout
|
|
2256
|
+
},
|
|
2257
|
+
children
|
|
2258
|
+
}
|
|
2259
|
+
);
|
|
2260
|
+
};
|
|
2261
|
+
var useGitHubAuth = () => {
|
|
2262
|
+
const context = React8.useContext(GitHubAuthContext);
|
|
2263
|
+
if (context === void 0) {
|
|
2264
|
+
throw new Error("useGitHubAuth must be used within a GitHubAuthProvider");
|
|
2265
|
+
}
|
|
2266
|
+
return context;
|
|
164
2267
|
};
|
|
165
2268
|
// Annotate the CommonJS export names for ESM import in node:
|
|
166
2269
|
0 && (module.exports = {
|