@memberjunction/server 5.27.1 → 5.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.d.ts +151 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +15 -0
- package/dist/config.js.map +1 -1
- package/dist/generated/generated.d.ts +959 -5
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +4639 -280
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +14 -0
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +37 -3
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RestoreContextInput.d.ts +27 -0
- package/dist/generic/RestoreContextInput.d.ts.map +1 -0
- package/dist/generic/RestoreContextInput.js +39 -0
- package/dist/generic/RestoreContextInput.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -4
- package/dist/index.js.map +1 -1
- package/dist/resolvers/FeedbackResolver.d.ts +150 -0
- package/dist/resolvers/FeedbackResolver.d.ts.map +1 -0
- package/dist/resolvers/FeedbackResolver.js +876 -0
- package/dist/resolvers/FeedbackResolver.js.map +1 -0
- package/dist/resolvers/FileResolver.d.ts +27 -0
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +32 -3
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +18 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +247 -22
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/MCPResolver.d.ts +77 -0
- package/dist/resolvers/MCPResolver.d.ts.map +1 -1
- package/dist/resolvers/MCPResolver.js +300 -1
- package/dist/resolvers/MCPResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +87 -32
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/package.json +68 -66
- package/src/config.ts +19 -0
- package/src/generated/generated.ts +3430 -281
- package/src/generic/ResolverBase.ts +41 -4
- package/src/generic/RestoreContextInput.ts +32 -0
- package/src/index.ts +22 -5
- package/src/resolvers/FeedbackResolver.ts +940 -0
- package/src/resolvers/FileResolver.ts +33 -4
- package/src/resolvers/IntegrationDiscoveryResolver.ts +224 -20
- package/src/resolvers/MCPResolver.ts +297 -1
- package/src/resolvers/RunAIAgentResolver.ts +89 -32
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
|
+
};
|
|
13
|
+
import { Resolver, Mutation, Query, Arg, Ctx, Field, InputType, ObjectType } from 'type-graphql';
|
|
14
|
+
import { Octokit } from '@octokit/rest';
|
|
15
|
+
import { createAppAuth } from '@octokit/auth-app';
|
|
16
|
+
import { LogError, LogStatus } from '@memberjunction/core';
|
|
17
|
+
import { configInfo } from '../config.js';
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
import { ChatParams, ChatMessageRole, GetAIAPIKey, BaseLLM } from '@memberjunction/ai';
|
|
20
|
+
import { AIEngine } from '@memberjunction/aiengine';
|
|
21
|
+
import { MJGlobal } from '@memberjunction/global';
|
|
22
|
+
import { readFileSync } from 'fs';
|
|
23
|
+
import { resolve } from 'path';
|
|
24
|
+
import { homedir } from 'os';
|
|
25
|
+
// Module-level cache — survives across resolver instances (type-graphql creates a new instance per request)
|
|
26
|
+
let cachedPrivateKey; // undefined = not yet read
|
|
27
|
+
let cachedOctokit = null;
|
|
28
|
+
let cachedAuthType = null;
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// GraphQL Input/Output Types
|
|
31
|
+
// ============================================================================
|
|
32
|
+
/**
|
|
33
|
+
* Input type for feedback submission
|
|
34
|
+
*/
|
|
35
|
+
let SubmitFeedbackInput = class SubmitFeedbackInput {
|
|
36
|
+
};
|
|
37
|
+
__decorate([
|
|
38
|
+
Field(),
|
|
39
|
+
__metadata("design:type", String)
|
|
40
|
+
], SubmitFeedbackInput.prototype, "Title", void 0);
|
|
41
|
+
__decorate([
|
|
42
|
+
Field(),
|
|
43
|
+
__metadata("design:type", String)
|
|
44
|
+
], SubmitFeedbackInput.prototype, "Description", void 0);
|
|
45
|
+
__decorate([
|
|
46
|
+
Field(),
|
|
47
|
+
__metadata("design:type", String)
|
|
48
|
+
], SubmitFeedbackInput.prototype, "Category", void 0);
|
|
49
|
+
__decorate([
|
|
50
|
+
Field({ nullable: true }),
|
|
51
|
+
__metadata("design:type", String)
|
|
52
|
+
], SubmitFeedbackInput.prototype, "StepsToReproduce", void 0);
|
|
53
|
+
__decorate([
|
|
54
|
+
Field({ nullable: true }),
|
|
55
|
+
__metadata("design:type", String)
|
|
56
|
+
], SubmitFeedbackInput.prototype, "ExpectedBehavior", void 0);
|
|
57
|
+
__decorate([
|
|
58
|
+
Field({ nullable: true }),
|
|
59
|
+
__metadata("design:type", String)
|
|
60
|
+
], SubmitFeedbackInput.prototype, "ActualBehavior", void 0);
|
|
61
|
+
__decorate([
|
|
62
|
+
Field({ nullable: true }),
|
|
63
|
+
__metadata("design:type", String)
|
|
64
|
+
], SubmitFeedbackInput.prototype, "Severity", void 0);
|
|
65
|
+
__decorate([
|
|
66
|
+
Field({ nullable: true }),
|
|
67
|
+
__metadata("design:type", String)
|
|
68
|
+
], SubmitFeedbackInput.prototype, "UseCase", void 0);
|
|
69
|
+
__decorate([
|
|
70
|
+
Field({ nullable: true }),
|
|
71
|
+
__metadata("design:type", String)
|
|
72
|
+
], SubmitFeedbackInput.prototype, "ProposedSolution", void 0);
|
|
73
|
+
__decorate([
|
|
74
|
+
Field({ nullable: true }),
|
|
75
|
+
__metadata("design:type", String)
|
|
76
|
+
], SubmitFeedbackInput.prototype, "Email", void 0);
|
|
77
|
+
__decorate([
|
|
78
|
+
Field({ nullable: true }),
|
|
79
|
+
__metadata("design:type", String)
|
|
80
|
+
], SubmitFeedbackInput.prototype, "Name", void 0);
|
|
81
|
+
__decorate([
|
|
82
|
+
Field({ nullable: true }),
|
|
83
|
+
__metadata("design:type", String)
|
|
84
|
+
], SubmitFeedbackInput.prototype, "Environment", void 0);
|
|
85
|
+
__decorate([
|
|
86
|
+
Field({ nullable: true }),
|
|
87
|
+
__metadata("design:type", String)
|
|
88
|
+
], SubmitFeedbackInput.prototype, "AffectedArea", void 0);
|
|
89
|
+
__decorate([
|
|
90
|
+
Field({ nullable: true }),
|
|
91
|
+
__metadata("design:type", String)
|
|
92
|
+
], SubmitFeedbackInput.prototype, "CurrentPage", void 0);
|
|
93
|
+
__decorate([
|
|
94
|
+
Field({ nullable: true }),
|
|
95
|
+
__metadata("design:type", String)
|
|
96
|
+
], SubmitFeedbackInput.prototype, "UserAgent", void 0);
|
|
97
|
+
__decorate([
|
|
98
|
+
Field({ nullable: true }),
|
|
99
|
+
__metadata("design:type", String)
|
|
100
|
+
], SubmitFeedbackInput.prototype, "ScreenSize", void 0);
|
|
101
|
+
__decorate([
|
|
102
|
+
Field({ nullable: true }),
|
|
103
|
+
__metadata("design:type", String)
|
|
104
|
+
], SubmitFeedbackInput.prototype, "AppName", void 0);
|
|
105
|
+
__decorate([
|
|
106
|
+
Field({ nullable: true }),
|
|
107
|
+
__metadata("design:type", String)
|
|
108
|
+
], SubmitFeedbackInput.prototype, "AppVersion", void 0);
|
|
109
|
+
__decorate([
|
|
110
|
+
Field({ nullable: true }),
|
|
111
|
+
__metadata("design:type", String)
|
|
112
|
+
], SubmitFeedbackInput.prototype, "UserId", void 0);
|
|
113
|
+
__decorate([
|
|
114
|
+
Field({ nullable: true }),
|
|
115
|
+
__metadata("design:type", String)
|
|
116
|
+
], SubmitFeedbackInput.prototype, "Timestamp", void 0);
|
|
117
|
+
__decorate([
|
|
118
|
+
Field({ nullable: true }),
|
|
119
|
+
__metadata("design:type", String)
|
|
120
|
+
], SubmitFeedbackInput.prototype, "Metadata", void 0);
|
|
121
|
+
SubmitFeedbackInput = __decorate([
|
|
122
|
+
InputType()
|
|
123
|
+
], SubmitFeedbackInput);
|
|
124
|
+
export { SubmitFeedbackInput };
|
|
125
|
+
/**
|
|
126
|
+
* Response type for feedback submission
|
|
127
|
+
*/
|
|
128
|
+
let FeedbackResponseType = class FeedbackResponseType {
|
|
129
|
+
};
|
|
130
|
+
__decorate([
|
|
131
|
+
Field(),
|
|
132
|
+
__metadata("design:type", Boolean)
|
|
133
|
+
], FeedbackResponseType.prototype, "Success", void 0);
|
|
134
|
+
__decorate([
|
|
135
|
+
Field({ nullable: true }),
|
|
136
|
+
__metadata("design:type", Number)
|
|
137
|
+
], FeedbackResponseType.prototype, "IssueNumber", void 0);
|
|
138
|
+
__decorate([
|
|
139
|
+
Field({ nullable: true }),
|
|
140
|
+
__metadata("design:type", String)
|
|
141
|
+
], FeedbackResponseType.prototype, "IssueUrl", void 0);
|
|
142
|
+
__decorate([
|
|
143
|
+
Field({ nullable: true }),
|
|
144
|
+
__metadata("design:type", String)
|
|
145
|
+
], FeedbackResponseType.prototype, "Error", void 0);
|
|
146
|
+
FeedbackResponseType = __decorate([
|
|
147
|
+
ObjectType()
|
|
148
|
+
], FeedbackResponseType);
|
|
149
|
+
export { FeedbackResponseType };
|
|
150
|
+
/**
|
|
151
|
+
* Input type for feedback classification
|
|
152
|
+
*/
|
|
153
|
+
let ClassifyFeedbackInput = class ClassifyFeedbackInput {
|
|
154
|
+
};
|
|
155
|
+
__decorate([
|
|
156
|
+
Field(),
|
|
157
|
+
__metadata("design:type", String)
|
|
158
|
+
], ClassifyFeedbackInput.prototype, "Title", void 0);
|
|
159
|
+
__decorate([
|
|
160
|
+
Field(),
|
|
161
|
+
__metadata("design:type", String)
|
|
162
|
+
], ClassifyFeedbackInput.prototype, "Description", void 0);
|
|
163
|
+
ClassifyFeedbackInput = __decorate([
|
|
164
|
+
InputType()
|
|
165
|
+
], ClassifyFeedbackInput);
|
|
166
|
+
export { ClassifyFeedbackInput };
|
|
167
|
+
/**
|
|
168
|
+
* Response type for feedback classification
|
|
169
|
+
*/
|
|
170
|
+
let FeedbackClassificationResult = class FeedbackClassificationResult {
|
|
171
|
+
};
|
|
172
|
+
__decorate([
|
|
173
|
+
Field(),
|
|
174
|
+
__metadata("design:type", Boolean)
|
|
175
|
+
], FeedbackClassificationResult.prototype, "Success", void 0);
|
|
176
|
+
__decorate([
|
|
177
|
+
Field({ nullable: true }),
|
|
178
|
+
__metadata("design:type", String)
|
|
179
|
+
], FeedbackClassificationResult.prototype, "Category", void 0);
|
|
180
|
+
__decorate([
|
|
181
|
+
Field({ nullable: true }),
|
|
182
|
+
__metadata("design:type", String)
|
|
183
|
+
], FeedbackClassificationResult.prototype, "Severity", void 0);
|
|
184
|
+
__decorate([
|
|
185
|
+
Field({ nullable: true }),
|
|
186
|
+
__metadata("design:type", String)
|
|
187
|
+
], FeedbackClassificationResult.prototype, "Error", void 0);
|
|
188
|
+
FeedbackClassificationResult = __decorate([
|
|
189
|
+
ObjectType()
|
|
190
|
+
], FeedbackClassificationResult);
|
|
191
|
+
export { FeedbackClassificationResult };
|
|
192
|
+
// ============================================================================
|
|
193
|
+
// Validation Schema
|
|
194
|
+
// ============================================================================
|
|
195
|
+
const FeedbackSubmissionSchema = z.object({
|
|
196
|
+
title: z
|
|
197
|
+
.string()
|
|
198
|
+
.min(5, 'Title must be at least 5 characters')
|
|
199
|
+
.max(256, 'Title must not exceed 256 characters'),
|
|
200
|
+
description: z
|
|
201
|
+
.string()
|
|
202
|
+
.min(20, 'Description must be at least 20 characters')
|
|
203
|
+
.max(10000, 'Description must not exceed 10000 characters'),
|
|
204
|
+
category: z.union([
|
|
205
|
+
z.literal('bug'),
|
|
206
|
+
z.literal('feature'),
|
|
207
|
+
z.literal('question'),
|
|
208
|
+
z.literal('other'),
|
|
209
|
+
z.string().max(50),
|
|
210
|
+
]),
|
|
211
|
+
stepsToReproduce: z.string().max(5000).optional(),
|
|
212
|
+
expectedBehavior: z.string().max(5000).optional(),
|
|
213
|
+
actualBehavior: z.string().max(5000).optional(),
|
|
214
|
+
severity: z.enum(['critical', 'major', 'minor', 'trivial']).optional(),
|
|
215
|
+
useCase: z.string().max(5000).optional(),
|
|
216
|
+
proposedSolution: z.string().max(5000).optional(),
|
|
217
|
+
email: z.string().email().optional().or(z.literal('')),
|
|
218
|
+
name: z.string().max(100).optional(),
|
|
219
|
+
environment: z.enum(['production', 'staging', 'development', 'local']).optional(),
|
|
220
|
+
affectedArea: z.string().max(100).optional(),
|
|
221
|
+
currentPage: z.string().max(200).optional(),
|
|
222
|
+
userAgent: z.string().max(500).optional(),
|
|
223
|
+
screenSize: z.string().max(20).optional(),
|
|
224
|
+
appName: z.string().max(100).optional(),
|
|
225
|
+
appVersion: z.string().max(50).optional(),
|
|
226
|
+
userId: z.string().max(100).optional(),
|
|
227
|
+
timestamp: z.string().optional(),
|
|
228
|
+
metadata: z.record(z.unknown()).optional(),
|
|
229
|
+
});
|
|
230
|
+
// ============================================================================
|
|
231
|
+
// Display Labels
|
|
232
|
+
// ============================================================================
|
|
233
|
+
const categoryLabels = {
|
|
234
|
+
bug: 'Bug Report',
|
|
235
|
+
feature: 'Feature Request',
|
|
236
|
+
question: 'Question',
|
|
237
|
+
other: 'Other',
|
|
238
|
+
};
|
|
239
|
+
const severityLabels = {
|
|
240
|
+
critical: 'Critical',
|
|
241
|
+
major: 'Major',
|
|
242
|
+
minor: 'Minor',
|
|
243
|
+
trivial: 'Trivial',
|
|
244
|
+
};
|
|
245
|
+
const environmentLabels = {
|
|
246
|
+
production: 'Production',
|
|
247
|
+
staging: 'Staging',
|
|
248
|
+
development: 'Development',
|
|
249
|
+
local: 'Local',
|
|
250
|
+
};
|
|
251
|
+
// ============================================================================
|
|
252
|
+
// Resolver
|
|
253
|
+
// ============================================================================
|
|
254
|
+
let FeedbackResolver = class FeedbackResolver {
|
|
255
|
+
/**
|
|
256
|
+
* Submit feedback and create a GitHub issue
|
|
257
|
+
*/
|
|
258
|
+
/**
|
|
259
|
+
* Check if the feedback feature is enabled for this org.
|
|
260
|
+
*/
|
|
261
|
+
FeedbackEnabled() {
|
|
262
|
+
return this.isFeedbackEnabled();
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Feedback is considered enabled only when both (a) the org kill switch
|
|
266
|
+
* is not explicitly off and (b) GitHub credentials are configured. Without
|
|
267
|
+
* (b) the feature cannot create issues, so we hide it rather than letting
|
|
268
|
+
* users submit into a broken pipe.
|
|
269
|
+
*/
|
|
270
|
+
isFeedbackEnabled() {
|
|
271
|
+
if (configInfo.feedbackSettings.enabled === false) {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
return this.resolveAuth() !== null;
|
|
275
|
+
}
|
|
276
|
+
async SubmitFeedback(input, ctx) {
|
|
277
|
+
try {
|
|
278
|
+
if (!this.isFeedbackEnabled()) {
|
|
279
|
+
return { Success: false, Error: 'Feedback is disabled for this organization.' };
|
|
280
|
+
}
|
|
281
|
+
// Get configuration
|
|
282
|
+
const config = this.getConfig();
|
|
283
|
+
if (!config) {
|
|
284
|
+
return {
|
|
285
|
+
Success: false,
|
|
286
|
+
Error: 'Feedback system is not configured. GITHUB_PAT environment variable is required.',
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
// Convert input to internal format
|
|
290
|
+
const submission = this.convertInputToSubmission(input, ctx);
|
|
291
|
+
// Validate submission
|
|
292
|
+
const validationResult = FeedbackSubmissionSchema.safeParse(submission);
|
|
293
|
+
if (!validationResult.success) {
|
|
294
|
+
const errorMessages = validationResult.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`);
|
|
295
|
+
return {
|
|
296
|
+
Success: false,
|
|
297
|
+
Error: `Validation failed: ${errorMessages.join('; ')}`,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
// Create GitHub issue
|
|
301
|
+
const issue = await this.createGitHubIssue(submission, config);
|
|
302
|
+
LogStatus(`Feedback submitted: Issue #${issue.number} created`);
|
|
303
|
+
return {
|
|
304
|
+
Success: true,
|
|
305
|
+
IssueNumber: issue.number,
|
|
306
|
+
IssueUrl: issue.html_url,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
LogError('Error submitting feedback', undefined, error);
|
|
311
|
+
const message = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
312
|
+
return {
|
|
313
|
+
Success: false,
|
|
314
|
+
Error: message,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Classify feedback using an LLM to suggest category and severity.
|
|
320
|
+
*/
|
|
321
|
+
async ClassifyFeedback(input, ctx) {
|
|
322
|
+
try {
|
|
323
|
+
if (!this.isFeedbackEnabled()) {
|
|
324
|
+
return { Success: false, Error: 'Feedback is disabled for this organization.' };
|
|
325
|
+
}
|
|
326
|
+
// Get the current context user for AIEngine config
|
|
327
|
+
const contextUser = ctx.userPayload?.userRecord;
|
|
328
|
+
await AIEngine.Instance.Config(false, contextUser);
|
|
329
|
+
// Get active LLM models with valid API keys (guard against null DriverClass)
|
|
330
|
+
const allModels = AIEngine.Instance.Models.filter(m => m.AIModelType?.trim().toLowerCase() === 'llm' && m.IsActive && m.DriverClass);
|
|
331
|
+
const models = allModels.filter(m => {
|
|
332
|
+
try {
|
|
333
|
+
const key = GetAIAPIKey(m.DriverClass);
|
|
334
|
+
return key && key.trim().length > 0;
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
if (models.length === 0) {
|
|
341
|
+
return { Success: false, Error: 'No AI models configured' };
|
|
342
|
+
}
|
|
343
|
+
// Pick a fast, cheap model — prefer Groq, then OpenAI, then any available
|
|
344
|
+
const model = models.find(m => m.DriverClass === 'GroqLLM')
|
|
345
|
+
|| models.find(m => m.DriverClass === 'OpenAILLM')
|
|
346
|
+
|| models[0];
|
|
347
|
+
const apiKey = GetAIAPIKey(model.DriverClass);
|
|
348
|
+
const llm = MJGlobal.Instance.ClassFactory.CreateInstance(BaseLLM, model.DriverClass, apiKey);
|
|
349
|
+
if (!llm) {
|
|
350
|
+
return { Success: false, Error: 'Failed to create LLM instance' };
|
|
351
|
+
}
|
|
352
|
+
const chatParams = new ChatParams();
|
|
353
|
+
chatParams.model = model.APIName;
|
|
354
|
+
chatParams.messages = [
|
|
355
|
+
{
|
|
356
|
+
role: ChatMessageRole.system,
|
|
357
|
+
content: `Classify this user feedback. Respond with ONLY a JSON object, no other text:
|
|
358
|
+
{"category": "bug"|"feature"|"question"|"other", "severity": "critical"|"major"|"minor"|"trivial"}
|
|
359
|
+
|
|
360
|
+
Step 1 — CATEGORY. Choose ONE. Read the intent, not just keywords:
|
|
361
|
+
- "question" = user is SEEKING HELP or INFORMATION. Clues: "how do I", "where is", "can I", "is it possible", "I can't find", "where did ___ go", "used to see ___ but now", "help me". Even if they sound frustrated, if they're looking for guidance it's a question. A user who can't FIND something is asking a question, not reporting a bug.
|
|
362
|
+
- "feature" = user WANTS something NEW: "it would be nice", "can you add", "I wish", "please add", "would love to see"
|
|
363
|
+
- "bug" = something is OBJECTIVELY BROKEN: "error message", "crash", "500 error", "white screen", "data lost", "button does nothing when clicked", "stops responding". The system is malfunctioning, not just confusing.
|
|
364
|
+
- "other" = doesn't fit the above
|
|
365
|
+
|
|
366
|
+
Step 2 — SEVERITY. Strictly follow these rules:
|
|
367
|
+
- If category is "question" → severity MUST be "trivial"
|
|
368
|
+
- If category is "feature" → severity MUST be "minor"
|
|
369
|
+
- If category is "other" → severity MUST be "minor"
|
|
370
|
+
- If category is "bug":
|
|
371
|
+
- "critical" = data loss, security vulnerability, entire system unusable for ALL users
|
|
372
|
+
- "major" = user EXPLICITLY says words like "blocking me", "preventing me from", "can't complete my work", "unable to do my job". Without these exact phrases, do NOT use major.
|
|
373
|
+
- "trivial" = purely visual: typo, misspelling, alignment, spacing, colors, dark mode, theme, font, styling, cosmetic appearance
|
|
374
|
+
- "minor" = everything else. This is the DEFAULT for bugs. If unsure between trivial and minor, pick trivial. If unsure between minor and major, pick minor.`
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
role: ChatMessageRole.user,
|
|
378
|
+
content: `Title: ${input.Title}\nDescription: ${input.Description}`
|
|
379
|
+
}
|
|
380
|
+
];
|
|
381
|
+
const result = await llm.ChatCompletion(chatParams);
|
|
382
|
+
const content = result?.data?.choices?.[0]?.message?.content?.trim();
|
|
383
|
+
if (!content) {
|
|
384
|
+
return { Success: false, Error: 'Empty response from AI' };
|
|
385
|
+
}
|
|
386
|
+
// Parse JSON from response (handle potential markdown code fences)
|
|
387
|
+
const jsonStr = content.replace(/```json?\s*/g, '').replace(/```/g, '').trim();
|
|
388
|
+
const parsed = JSON.parse(jsonStr);
|
|
389
|
+
return {
|
|
390
|
+
Success: true,
|
|
391
|
+
Category: parsed.category,
|
|
392
|
+
Severity: parsed.severity
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
LogError('Error classifying feedback', undefined, error);
|
|
397
|
+
return { Success: false, Error: 'Classification failed' };
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
// ============================================================================
|
|
401
|
+
// Private Helper Methods
|
|
402
|
+
// ============================================================================
|
|
403
|
+
/**
|
|
404
|
+
* Get feedback configuration from environment/config.
|
|
405
|
+
* Supports GitHub App auth (preferred) or PAT fallback.
|
|
406
|
+
*/
|
|
407
|
+
getConfig() {
|
|
408
|
+
const auth = this.resolveAuth();
|
|
409
|
+
if (!auth) {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
const githubSettings = configInfo.feedbackSettings?.github;
|
|
413
|
+
return {
|
|
414
|
+
owner: githubSettings?.owner || process.env.GITHUB_FEEDBACK_OWNER || 'MemberJunction',
|
|
415
|
+
repo: githubSettings?.repo || process.env.GITHUB_FEEDBACK_REPO || 'MJ',
|
|
416
|
+
auth,
|
|
417
|
+
defaultLabels: githubSettings?.defaultLabels || ['user-submitted'],
|
|
418
|
+
categoryLabels: githubSettings?.categoryLabels || {
|
|
419
|
+
bug: 'bug',
|
|
420
|
+
feature: 'enhancement',
|
|
421
|
+
question: 'question',
|
|
422
|
+
other: 'triage',
|
|
423
|
+
},
|
|
424
|
+
severityLabels: githubSettings?.severityLabels || {
|
|
425
|
+
critical: 'priority: critical',
|
|
426
|
+
major: 'priority: high',
|
|
427
|
+
minor: 'priority: medium',
|
|
428
|
+
trivial: 'priority: low',
|
|
429
|
+
},
|
|
430
|
+
assignees: githubSettings?.assignees,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Resolve authentication — prefers GitHub App, falls back to PAT.
|
|
435
|
+
*/
|
|
436
|
+
resolveAuth() {
|
|
437
|
+
// Try GitHub App first
|
|
438
|
+
const appId = process.env.GITHUB_APP_ID;
|
|
439
|
+
const installationId = process.env.GITHUB_APP_INSTALLATION_ID;
|
|
440
|
+
const privateKey = this.readPrivateKey();
|
|
441
|
+
if (appId && installationId && privateKey) {
|
|
442
|
+
return {
|
|
443
|
+
type: 'app',
|
|
444
|
+
appId: Number(appId),
|
|
445
|
+
installationId: Number(installationId),
|
|
446
|
+
privateKey
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
// Fall back to PAT
|
|
450
|
+
const token = process.env.GITHUB_PAT;
|
|
451
|
+
if (token) {
|
|
452
|
+
return { type: 'pat', token };
|
|
453
|
+
}
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Read the GitHub App private key from file path or inline env var.
|
|
458
|
+
* Cached at module level — the key file doesn't change at runtime.
|
|
459
|
+
*/
|
|
460
|
+
readPrivateKey() {
|
|
461
|
+
if (cachedPrivateKey !== undefined) {
|
|
462
|
+
return cachedPrivateKey;
|
|
463
|
+
}
|
|
464
|
+
// Option 1: File path
|
|
465
|
+
const keyPath = process.env.GITHUB_APP_PRIVATE_KEY_PATH;
|
|
466
|
+
if (keyPath) {
|
|
467
|
+
try {
|
|
468
|
+
const resolved = keyPath.startsWith('~/')
|
|
469
|
+
? resolve(homedir(), keyPath.slice(2))
|
|
470
|
+
: resolve(keyPath);
|
|
471
|
+
cachedPrivateKey = readFileSync(resolved, 'utf-8');
|
|
472
|
+
return cachedPrivateKey;
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
LogError('Failed to read GitHub App private key file', undefined, error);
|
|
476
|
+
cachedPrivateKey = null;
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
// Option 2: Inline key (with \n replaced)
|
|
481
|
+
const inlineKey = process.env.GITHUB_APP_PRIVATE_KEY;
|
|
482
|
+
if (inlineKey) {
|
|
483
|
+
cachedPrivateKey = inlineKey.replace(/\\n/g, '\n');
|
|
484
|
+
return cachedPrivateKey;
|
|
485
|
+
}
|
|
486
|
+
cachedPrivateKey = null;
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Convert GraphQL input to internal submission format
|
|
491
|
+
*/
|
|
492
|
+
convertInputToSubmission(input, ctx) {
|
|
493
|
+
const submission = {
|
|
494
|
+
title: input.Title,
|
|
495
|
+
description: input.Description,
|
|
496
|
+
category: input.Category,
|
|
497
|
+
stepsToReproduce: input.StepsToReproduce,
|
|
498
|
+
expectedBehavior: input.ExpectedBehavior,
|
|
499
|
+
actualBehavior: input.ActualBehavior,
|
|
500
|
+
severity: input.Severity,
|
|
501
|
+
useCase: input.UseCase,
|
|
502
|
+
proposedSolution: input.ProposedSolution,
|
|
503
|
+
email: input.Email,
|
|
504
|
+
name: input.Name,
|
|
505
|
+
environment: input.Environment,
|
|
506
|
+
affectedArea: input.AffectedArea,
|
|
507
|
+
currentPage: input.CurrentPage,
|
|
508
|
+
userAgent: input.UserAgent,
|
|
509
|
+
screenSize: input.ScreenSize,
|
|
510
|
+
appName: input.AppName,
|
|
511
|
+
appVersion: input.AppVersion,
|
|
512
|
+
userId: input.UserId || ctx.userPayload?.userRecord?.ID,
|
|
513
|
+
timestamp: input.Timestamp || new Date().toISOString(),
|
|
514
|
+
};
|
|
515
|
+
// Parse metadata if provided
|
|
516
|
+
if (input.Metadata) {
|
|
517
|
+
try {
|
|
518
|
+
submission.metadata = JSON.parse(input.Metadata);
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
// Ignore invalid JSON
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return submission;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Get or create an authenticated Octokit instance.
|
|
528
|
+
* Cached at module level — createAppAuth handles token refresh internally.
|
|
529
|
+
*/
|
|
530
|
+
getOctokit(config) {
|
|
531
|
+
if (cachedOctokit && cachedAuthType === config.auth.type) {
|
|
532
|
+
return cachedOctokit;
|
|
533
|
+
}
|
|
534
|
+
if (config.auth.type === 'app') {
|
|
535
|
+
cachedOctokit = new Octokit({
|
|
536
|
+
authStrategy: createAppAuth,
|
|
537
|
+
auth: {
|
|
538
|
+
appId: config.auth.appId,
|
|
539
|
+
privateKey: config.auth.privateKey,
|
|
540
|
+
installationId: config.auth.installationId,
|
|
541
|
+
},
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
cachedOctokit = new Octokit({ auth: config.auth.token });
|
|
546
|
+
}
|
|
547
|
+
cachedAuthType = config.auth.type;
|
|
548
|
+
return cachedOctokit;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Create a GitHub issue from the feedback submission.
|
|
552
|
+
* If a screenshot is present, uploads it to the repo and embeds it in the issue.
|
|
553
|
+
*/
|
|
554
|
+
async createGitHubIssue(submission, config) {
|
|
555
|
+
const octokit = this.getOctokit(config);
|
|
556
|
+
// Extract screenshot before formatting (formatScreenshotSection removes it from metadata)
|
|
557
|
+
const screenshotDataUrl = submission.metadata?.screenshot;
|
|
558
|
+
const labels = this.buildLabels(submission, config);
|
|
559
|
+
const body = this.formatIssueBody(submission);
|
|
560
|
+
// Create the issue first
|
|
561
|
+
const response = await octokit.issues.create({
|
|
562
|
+
owner: config.owner,
|
|
563
|
+
repo: config.repo,
|
|
564
|
+
title: submission.title,
|
|
565
|
+
body,
|
|
566
|
+
labels,
|
|
567
|
+
assignees: config.assignees,
|
|
568
|
+
});
|
|
569
|
+
const issueNumber = response.data.number;
|
|
570
|
+
// Upload screenshot and update issue body if present
|
|
571
|
+
if (screenshotDataUrl && typeof screenshotDataUrl === 'string') {
|
|
572
|
+
try {
|
|
573
|
+
const imageUrl = await this.uploadScreenshot(octokit, config.owner, config.repo, screenshotDataUrl, issueNumber);
|
|
574
|
+
if (imageUrl) {
|
|
575
|
+
const screenshotMarkdown = `\n## Screenshot\n\n\n`;
|
|
576
|
+
await octokit.issues.update({
|
|
577
|
+
owner: config.owner,
|
|
578
|
+
repo: config.repo,
|
|
579
|
+
issue_number: issueNumber,
|
|
580
|
+
body: body + screenshotMarkdown,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
catch (error) {
|
|
585
|
+
// Screenshot upload is best-effort — don't fail the whole submission
|
|
586
|
+
LogError('Failed to upload feedback screenshot', undefined, error);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
number: issueNumber,
|
|
591
|
+
html_url: response.data.html_url,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Upload a base64 screenshot to the repo and return the raw content URL.
|
|
596
|
+
*/
|
|
597
|
+
async uploadScreenshot(octokit, owner, repo, dataUrl, issueNumber) {
|
|
598
|
+
// Strip the data URL prefix (e.g., "data:image/jpeg;base64,")
|
|
599
|
+
const base64Match = dataUrl.match(/^data:image\/(\w+);base64,(.+)$/);
|
|
600
|
+
if (!base64Match)
|
|
601
|
+
return null;
|
|
602
|
+
const extension = base64Match[1] === 'jpeg' ? 'jpg' : base64Match[1];
|
|
603
|
+
const base64Content = base64Match[2];
|
|
604
|
+
const filename = `${issueNumber}-${Date.now()}.${extension}`;
|
|
605
|
+
const path = `.github/feedback-screenshots/${filename}`;
|
|
606
|
+
await octokit.repos.createOrUpdateFileContents({
|
|
607
|
+
owner,
|
|
608
|
+
repo,
|
|
609
|
+
path,
|
|
610
|
+
message: `feedback: add screenshot for issue #${issueNumber}`,
|
|
611
|
+
content: base64Content,
|
|
612
|
+
});
|
|
613
|
+
return `https://raw.githubusercontent.com/${owner}/${repo}/next/${path}`;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Build the labels array for the GitHub issue
|
|
617
|
+
*/
|
|
618
|
+
buildLabels(submission, config) {
|
|
619
|
+
const labels = [];
|
|
620
|
+
if (config.defaultLabels) {
|
|
621
|
+
labels.push(...config.defaultLabels);
|
|
622
|
+
}
|
|
623
|
+
if (config.categoryLabels && submission.category in config.categoryLabels) {
|
|
624
|
+
labels.push(config.categoryLabels[submission.category]);
|
|
625
|
+
}
|
|
626
|
+
if (submission.severity && config.severityLabels && submission.severity in config.severityLabels) {
|
|
627
|
+
labels.push(config.severityLabels[submission.severity]);
|
|
628
|
+
}
|
|
629
|
+
return labels;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Format the feedback submission as a GitHub issue body in markdown.
|
|
633
|
+
* Orchestrates section-specific formatters and joins them.
|
|
634
|
+
*/
|
|
635
|
+
formatIssueBody(submission) {
|
|
636
|
+
const sections = [
|
|
637
|
+
this.formatDescriptionSection(submission),
|
|
638
|
+
this.formatScreenshotSection(submission),
|
|
639
|
+
'---',
|
|
640
|
+
'',
|
|
641
|
+
this.formatDetailsTable(submission),
|
|
642
|
+
this.formatContactSection(submission),
|
|
643
|
+
this.formatTechnicalDetails(submission),
|
|
644
|
+
this.formatMetadataSection(submission),
|
|
645
|
+
];
|
|
646
|
+
return sections.filter(s => s.length > 0).join('\n');
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Format the description and category-specific sections (bug/feature)
|
|
650
|
+
*/
|
|
651
|
+
formatDescriptionSection(submission) {
|
|
652
|
+
const lines = [];
|
|
653
|
+
lines.push('## Description', '', this.sanitizeMarkdown(submission.description), '');
|
|
654
|
+
if (submission.category === 'bug') {
|
|
655
|
+
if (submission.stepsToReproduce) {
|
|
656
|
+
lines.push('## Steps to Reproduce', '', this.sanitizeMarkdown(submission.stepsToReproduce), '');
|
|
657
|
+
}
|
|
658
|
+
if (submission.expectedBehavior) {
|
|
659
|
+
lines.push('## Expected Behavior', '', this.sanitizeMarkdown(submission.expectedBehavior), '');
|
|
660
|
+
}
|
|
661
|
+
if (submission.actualBehavior) {
|
|
662
|
+
lines.push('## Actual Behavior', '', this.sanitizeMarkdown(submission.actualBehavior), '');
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (submission.category === 'feature') {
|
|
666
|
+
if (submission.useCase) {
|
|
667
|
+
lines.push('## Use Case', '', this.sanitizeMarkdown(submission.useCase), '');
|
|
668
|
+
}
|
|
669
|
+
if (submission.proposedSolution) {
|
|
670
|
+
lines.push('## Proposed Solution', '', this.sanitizeMarkdown(submission.proposedSolution), '');
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return lines.join('\n');
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Format the submission details markdown table
|
|
677
|
+
*/
|
|
678
|
+
formatDetailsTable(submission) {
|
|
679
|
+
const rows = [];
|
|
680
|
+
rows.push('### Submission Details', '', '| Field | Value |', '|-------|-------|');
|
|
681
|
+
if (submission.appName) {
|
|
682
|
+
const appInfo = submission.appVersion
|
|
683
|
+
? `${this.sanitizeMarkdown(submission.appName)} v${this.sanitizeMarkdown(submission.appVersion)}`
|
|
684
|
+
: this.sanitizeMarkdown(submission.appName);
|
|
685
|
+
rows.push(`| **App** | ${appInfo} |`);
|
|
686
|
+
}
|
|
687
|
+
const categoryDisplay = categoryLabels[submission.category] || submission.category;
|
|
688
|
+
rows.push(`| **Category** | ${this.sanitizeMarkdown(categoryDisplay)} |`);
|
|
689
|
+
if (submission.severity) {
|
|
690
|
+
const severityDisplay = severityLabels[submission.severity] || submission.severity;
|
|
691
|
+
rows.push(`| **Severity** | ${this.sanitizeMarkdown(severityDisplay)} |`);
|
|
692
|
+
}
|
|
693
|
+
if (submission.environment) {
|
|
694
|
+
const envDisplay = environmentLabels[submission.environment] || submission.environment;
|
|
695
|
+
rows.push(`| **Environment** | ${this.sanitizeMarkdown(envDisplay)} |`);
|
|
696
|
+
}
|
|
697
|
+
if (submission.affectedArea) {
|
|
698
|
+
rows.push(`| **Affected Area** | ${this.sanitizeMarkdown(submission.affectedArea)} |`);
|
|
699
|
+
}
|
|
700
|
+
if (submission.timestamp) {
|
|
701
|
+
rows.push(`| **Submitted** | ${this.sanitizeMarkdown(submission.timestamp)} |`);
|
|
702
|
+
}
|
|
703
|
+
rows.push('');
|
|
704
|
+
return rows.join('\n');
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Format the contact information section
|
|
708
|
+
*/
|
|
709
|
+
formatContactSection(submission) {
|
|
710
|
+
if (!submission.name && !submission.email) {
|
|
711
|
+
return '';
|
|
712
|
+
}
|
|
713
|
+
const contactParts = [];
|
|
714
|
+
if (submission.name) {
|
|
715
|
+
contactParts.push(this.sanitizeMarkdown(submission.name));
|
|
716
|
+
}
|
|
717
|
+
if (submission.email) {
|
|
718
|
+
contactParts.push(this.sanitizeMarkdown(submission.email));
|
|
719
|
+
}
|
|
720
|
+
return ['### Contact', '', contactParts.join(' — '), ''].join('\n');
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Format the technical details table (page, browser, screen, user)
|
|
724
|
+
*/
|
|
725
|
+
formatTechnicalDetails(submission) {
|
|
726
|
+
if (!submission.currentPage && !submission.userAgent && !submission.screenSize && !submission.userId) {
|
|
727
|
+
return '';
|
|
728
|
+
}
|
|
729
|
+
const rows = [];
|
|
730
|
+
rows.push('### Technical Details', '', '| Field | Value |', '|-------|-------|');
|
|
731
|
+
if (submission.currentPage) {
|
|
732
|
+
rows.push(`| **Page/View** | ${this.sanitizeMarkdown(submission.currentPage)} |`);
|
|
733
|
+
}
|
|
734
|
+
if (submission.userAgent) {
|
|
735
|
+
const browserInfo = this.parseUserAgent(submission.userAgent);
|
|
736
|
+
rows.push(`| **Browser** | ${this.sanitizeMarkdown(browserInfo)} |`);
|
|
737
|
+
}
|
|
738
|
+
if (submission.screenSize) {
|
|
739
|
+
rows.push(`| **Screen Size** | ${this.sanitizeMarkdown(submission.screenSize)} |`);
|
|
740
|
+
}
|
|
741
|
+
if (submission.userId) {
|
|
742
|
+
rows.push(`| **User ID** | ${this.sanitizeMarkdown(submission.userId)} |`);
|
|
743
|
+
}
|
|
744
|
+
rows.push('');
|
|
745
|
+
return rows.join('\n');
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Format the screenshot section. Embeds the base64 data in a collapsed block
|
|
749
|
+
* that AI agents can parse, even though GitHub won't render it visually.
|
|
750
|
+
* If the file upload succeeds (in createGitHubIssue), the issue body gets
|
|
751
|
+
* updated with a rendered image above this block.
|
|
752
|
+
*/
|
|
753
|
+
formatScreenshotSection(submission) {
|
|
754
|
+
const screenshot = submission.metadata?.screenshot;
|
|
755
|
+
if (!screenshot || typeof screenshot !== 'string') {
|
|
756
|
+
return '';
|
|
757
|
+
}
|
|
758
|
+
// Remove from metadata so it doesn't also appear in the JSON section
|
|
759
|
+
delete submission.metadata.screenshot;
|
|
760
|
+
return [
|
|
761
|
+
'<details>',
|
|
762
|
+
'<summary>📷 Screenshot (for AI agents — not rendered by GitHub)</summary>',
|
|
763
|
+
'',
|
|
764
|
+
``,
|
|
765
|
+
'',
|
|
766
|
+
'</details>',
|
|
767
|
+
'',
|
|
768
|
+
].join('\n');
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Format the collapsible metadata JSON section
|
|
772
|
+
*/
|
|
773
|
+
formatMetadataSection(submission) {
|
|
774
|
+
if (!submission.metadata || Object.keys(submission.metadata).length === 0) {
|
|
775
|
+
return '';
|
|
776
|
+
}
|
|
777
|
+
return [
|
|
778
|
+
'<details>',
|
|
779
|
+
'<summary>Additional Metadata</summary>',
|
|
780
|
+
'',
|
|
781
|
+
'```json',
|
|
782
|
+
JSON.stringify(submission.metadata, null, 2),
|
|
783
|
+
'```',
|
|
784
|
+
'</details>',
|
|
785
|
+
].join('\n');
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Sanitize a string for safe inclusion in markdown
|
|
789
|
+
*/
|
|
790
|
+
sanitizeMarkdown(text) {
|
|
791
|
+
if (!text)
|
|
792
|
+
return '';
|
|
793
|
+
return text
|
|
794
|
+
.replace(/\\/g, '\\\\') // backslashes first (before other escapes add more)
|
|
795
|
+
.replace(/&/g, '&')
|
|
796
|
+
.replace(/</g, '<')
|
|
797
|
+
.replace(/>/g, '>')
|
|
798
|
+
.replace(/`/g, '\\`') // inline code injection
|
|
799
|
+
.replace(/\[/g, '\\[')
|
|
800
|
+
.replace(/\]/g, '\\]')
|
|
801
|
+
.replace(/!\[/g, '!\\[')
|
|
802
|
+
.replace(/\(/g, '\\(') // link URL injection
|
|
803
|
+
.replace(/\)/g, '\\)')
|
|
804
|
+
.replace(/\|/g, '\\|');
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Parse user agent string into a human-readable format
|
|
808
|
+
*/
|
|
809
|
+
parseUserAgent(userAgent) {
|
|
810
|
+
let browser = 'Unknown Browser';
|
|
811
|
+
let os = 'Unknown OS';
|
|
812
|
+
if (userAgent.includes('Firefox/')) {
|
|
813
|
+
const match = userAgent.match(/Firefox\/(\d+)/);
|
|
814
|
+
browser = match ? `Firefox ${match[1]}` : 'Firefox';
|
|
815
|
+
}
|
|
816
|
+
else if (userAgent.includes('Edg/')) {
|
|
817
|
+
const match = userAgent.match(/Edg\/(\d+)/);
|
|
818
|
+
browser = match ? `Edge ${match[1]}` : 'Edge';
|
|
819
|
+
}
|
|
820
|
+
else if (userAgent.includes('Chrome/')) {
|
|
821
|
+
const match = userAgent.match(/Chrome\/(\d+)/);
|
|
822
|
+
browser = match ? `Chrome ${match[1]}` : 'Chrome';
|
|
823
|
+
}
|
|
824
|
+
else if (userAgent.includes('Safari/') && !userAgent.includes('Chrome')) {
|
|
825
|
+
const match = userAgent.match(/Version\/(\d+)/);
|
|
826
|
+
browser = match ? `Safari ${match[1]}` : 'Safari';
|
|
827
|
+
}
|
|
828
|
+
if (userAgent.includes('Windows NT 10')) {
|
|
829
|
+
os = 'Windows 10/11';
|
|
830
|
+
}
|
|
831
|
+
else if (userAgent.includes('Windows NT')) {
|
|
832
|
+
os = 'Windows';
|
|
833
|
+
}
|
|
834
|
+
else if (userAgent.includes('Mac OS X')) {
|
|
835
|
+
const match = userAgent.match(/Mac OS X (\d+[._]\d+)/);
|
|
836
|
+
os = match ? `macOS ${match[1].replace('_', '.')}` : 'macOS';
|
|
837
|
+
}
|
|
838
|
+
else if (userAgent.includes('Linux')) {
|
|
839
|
+
os = 'Linux';
|
|
840
|
+
}
|
|
841
|
+
else if (userAgent.includes('Android')) {
|
|
842
|
+
os = 'Android';
|
|
843
|
+
}
|
|
844
|
+
else if (userAgent.includes('iOS') || userAgent.includes('iPhone') || userAgent.includes('iPad')) {
|
|
845
|
+
os = 'iOS';
|
|
846
|
+
}
|
|
847
|
+
return `${browser} on ${os}`;
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
__decorate([
|
|
851
|
+
Query(() => Boolean),
|
|
852
|
+
__metadata("design:type", Function),
|
|
853
|
+
__metadata("design:paramtypes", []),
|
|
854
|
+
__metadata("design:returntype", Boolean)
|
|
855
|
+
], FeedbackResolver.prototype, "FeedbackEnabled", null);
|
|
856
|
+
__decorate([
|
|
857
|
+
Mutation(() => FeedbackResponseType),
|
|
858
|
+
__param(0, Arg('input')),
|
|
859
|
+
__param(1, Ctx()),
|
|
860
|
+
__metadata("design:type", Function),
|
|
861
|
+
__metadata("design:paramtypes", [SubmitFeedbackInput, Object]),
|
|
862
|
+
__metadata("design:returntype", Promise)
|
|
863
|
+
], FeedbackResolver.prototype, "SubmitFeedback", null);
|
|
864
|
+
__decorate([
|
|
865
|
+
Mutation(() => FeedbackClassificationResult),
|
|
866
|
+
__param(0, Arg('input')),
|
|
867
|
+
__param(1, Ctx()),
|
|
868
|
+
__metadata("design:type", Function),
|
|
869
|
+
__metadata("design:paramtypes", [ClassifyFeedbackInput, Object]),
|
|
870
|
+
__metadata("design:returntype", Promise)
|
|
871
|
+
], FeedbackResolver.prototype, "ClassifyFeedback", null);
|
|
872
|
+
FeedbackResolver = __decorate([
|
|
873
|
+
Resolver()
|
|
874
|
+
], FeedbackResolver);
|
|
875
|
+
export { FeedbackResolver };
|
|
876
|
+
//# sourceMappingURL=FeedbackResolver.js.map
|