Skip to content

Commit e06bb15

Browse files
Marfuentofikwestclaudfuen
authored
feat(security-questionnaire): add AI-powered questionnaire parsing an… (#1751)
* feat(security-questionnaire): add AI-powered questionnaire parsing and auto-answering functionality * feat(frameworks): enhance FrameworksOverview with badge display and improve Security Questionnaire layout - Update FrameworksOverview to conditionally render badges or initials based on availability. - Refactor Security Questionnaire page to include a breadcrumb navigation and improve layout for better user experience. - Enhance QuestionnaireParser with new alert dialog for exit confirmation and streamline question answering process. - Improve UI components for better accessibility and responsiveness. * refactor(security-questionnaire): improve QuestionnaireParser layout and styling - Update search input styling for better visibility and responsiveness. - Adjust layout of command bar components for improved user experience. - Streamline button functionalities and ensure consistent styling across export options. * feat(security-questionnaire): enhance UI and functionality of Security Questionnaire page - Adjust padding and layout for improved responsiveness on the Security Questionnaire page. - Update header styles for better visibility and consistency. - Implement download functionality for questionnaire responses with enhanced user feedback. - Refactor question and answer display for better organization and accessibility across devices. - Improve button and input styling for a more cohesive user experience. * feat(security-questionnaire): enhance QuestionnaireParser UI and functionality - Update tab trigger styles for improved visibility and consistency. - Refactor file upload and URL input sections for better user experience. - Enhance dropzone component with clearer instructions and improved styling. - Streamline action button layout and functionality for better accessibility. * refactor(security-questionnaire): streamline action button layout in QuestionnaireParser - Reorganize action button section for improved clarity and consistency. - Maintain existing functionality while enhancing the overall UI structure. * feat(security-questionnaire): implement feature flag checks for questionnaire access - Add feature flag checks to control access to the AI vendor questionnaire. - Remove FeatureFlagWrapper component and directly use QuestionnaireParser. - Update header, sidebar, and mobile menu to conditionally render questionnaire options based on feature flag status. * refactor(api): simplify run status retrieval logic in task status route --------- Co-authored-by: Tofik Hasanov <annexcies@gmail.com> Co-authored-by: Claudio Fuentes <imclaudfuen@gmail.com>
1 parent ee92a16 commit e06bb15

30 files changed

+3276
-18
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ node_modules
55
.pnp
66
.pnp.js
77

8+
.idea/
89
# testing
910
coverage
1011

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
---
2+
description: Only the most important rules for writing basic Trigger.dev tasks
3+
globs: **/trigger/**/*.ts
4+
alwaysApply: false
5+
---
6+
# Trigger.dev Basic Tasks (v4)
7+
8+
**MUST use `@trigger.dev/sdk` (v4), NEVER `client.defineJob`**
9+
10+
## Basic Task
11+
12+
```ts
13+
import { task } from "@trigger.dev/sdk";
14+
15+
export const processData = task({
16+
id: "process-data",
17+
retry: {
18+
maxAttempts: 10,
19+
factor: 1.8,
20+
minTimeoutInMs: 500,
21+
maxTimeoutInMs: 30_000,
22+
randomize: false,
23+
},
24+
run: async (payload: { userId: string; data: any[] }) => {
25+
// Task logic - runs for long time, no timeouts
26+
console.log(`Processing ${payload.data.length} items for user ${payload.userId}`);
27+
return { processed: payload.data.length };
28+
},
29+
});
30+
```
31+
32+
## Schema Task (with validation)
33+
34+
```ts
35+
import { schemaTask } from "@trigger.dev/sdk";
36+
import { z } from "zod";
37+
38+
export const validatedTask = schemaTask({
39+
id: "validated-task",
40+
schema: z.object({
41+
name: z.string(),
42+
age: z.number(),
43+
email: z.string().email(),
44+
}),
45+
run: async (payload) => {
46+
// Payload is automatically validated and typed
47+
return { message: `Hello ${payload.name}, age ${payload.age}` };
48+
},
49+
});
50+
```
51+
52+
## Scheduled Task
53+
54+
```ts
55+
import { schedules } from "@trigger.dev/sdk";
56+
57+
const dailyReport = schedules.task({
58+
id: "daily-report",
59+
cron: "0 9 * * *", // Daily at 9:00 AM UTC
60+
// or with timezone: cron: { pattern: "0 9 * * *", timezone: "America/New_York" },
61+
run: async (payload) => {
62+
console.log("Scheduled run at:", payload.timestamp);
63+
console.log("Last run was:", payload.lastTimestamp);
64+
console.log("Next 5 runs:", payload.upcoming);
65+
66+
// Generate daily report logic
67+
return { reportGenerated: true, date: payload.timestamp };
68+
},
69+
});
70+
```
71+
72+
## Triggering Tasks
73+
74+
### From Backend Code
75+
76+
```ts
77+
import { tasks } from "@trigger.dev/sdk";
78+
import type { processData } from "./trigger/tasks";
79+
80+
// Single trigger
81+
const handle = await tasks.trigger<typeof processData>("process-data", {
82+
userId: "123",
83+
data: [{ id: 1 }, { id: 2 }],
84+
});
85+
86+
// Batch trigger
87+
const batchHandle = await tasks.batchTrigger<typeof processData>("process-data", [
88+
{ payload: { userId: "123", data: [{ id: 1 }] } },
89+
{ payload: { userId: "456", data: [{ id: 2 }] } },
90+
]);
91+
```
92+
93+
### From Inside Tasks (with Result handling)
94+
95+
```ts
96+
export const parentTask = task({
97+
id: "parent-task",
98+
run: async (payload) => {
99+
// Trigger and continue
100+
const handle = await childTask.trigger({ data: "value" });
101+
102+
// Trigger and wait - returns Result object, NOT task output
103+
const result = await childTask.triggerAndWait({ data: "value" });
104+
if (result.ok) {
105+
console.log("Task output:", result.output); // Actual task return value
106+
} else {
107+
console.error("Task failed:", result.error);
108+
}
109+
110+
// Quick unwrap (throws on error)
111+
const output = await childTask.triggerAndWait({ data: "value" }).unwrap();
112+
113+
// Batch trigger and wait
114+
const results = await childTask.batchTriggerAndWait([
115+
{ payload: { data: "item1" } },
116+
{ payload: { data: "item2" } },
117+
]);
118+
119+
for (const run of results) {
120+
if (run.ok) {
121+
console.log("Success:", run.output);
122+
} else {
123+
console.log("Failed:", run.error);
124+
}
125+
}
126+
},
127+
});
128+
129+
export const childTask = task({
130+
id: "child-task",
131+
run: async (payload: { data: string }) => {
132+
return { processed: payload.data };
133+
},
134+
});
135+
```
136+
137+
> Never wrap triggerAndWait or batchTriggerAndWait calls in a Promise.all or Promise.allSettled as this is not supported in Trigger.dev tasks.
138+
139+
## Waits
140+
141+
```ts
142+
import { task, wait } from "@trigger.dev/sdk";
143+
144+
export const taskWithWaits = task({
145+
id: "task-with-waits",
146+
run: async (payload) => {
147+
console.log("Starting task");
148+
149+
// Wait for specific duration
150+
await wait.for({ seconds: 30 });
151+
await wait.for({ minutes: 5 });
152+
await wait.for({ hours: 1 });
153+
await wait.for({ days: 1 });
154+
155+
// Wait until specific date
156+
await wait.until({ date: new Date("2024-12-25") });
157+
158+
// Wait for token (from external system)
159+
await wait.forToken({
160+
token: "user-approval-token",
161+
timeoutInSeconds: 3600, // 1 hour timeout
162+
});
163+
164+
console.log("All waits completed");
165+
return { status: "completed" };
166+
},
167+
});
168+
```
169+
170+
> Never wrap wait calls in a Promise.all or Promise.allSettled as this is not supported in Trigger.dev tasks.
171+
172+
## Key Points
173+
174+
- **Result vs Output**: `triggerAndWait()` returns a `Result` object with `ok`, `output`, `error` properties - NOT the direct task output
175+
- **Type safety**: Use `import type` for task references when triggering from backend
176+
- **Waits > 5 seconds**: Automatically checkpointed, don't count toward compute usage
177+
178+
## NEVER Use (v2 deprecated)
179+
180+
```ts
181+
// BREAKS APPLICATION
182+
client.defineJob({
183+
id: "job-id",
184+
run: async (payload, io) => {
185+
/* ... */
186+
},
187+
});
188+
```
189+
190+
Use v4 SDK (`@trigger.dev/sdk`), check `result.ok` before accessing `result.output`

apps/app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@
106106
"use-debounce": "^10.0.4",
107107
"use-long-press": "^3.3.0",
108108
"use-stick-to-bottom": "^1.1.1",
109+
"xlsx": "^0.18.5",
109110
"xml2js": "^0.6.2",
110111
"zaraz-ts": "^1.2.0",
111112
"zod": "^3.25.76",

apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworksOverview.tsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -98,19 +98,28 @@ export function FrameworksOverview({
9898
<div className="space-y-0 pr-4">
9999
{frameworksWithControls.map((framework, index) => {
100100
const complianceScore = complianceMap.get(framework.id) ?? 0;
101+
const badgeSrc = mapFrameworkToBadge(framework);
101102

102103
return (
103104
<div key={framework.id}>
104105
<div className="flex items-start justify-between py-4 px-1">
105106
<div className="flex items-start gap-3 flex-1 min-w-0">
106107
<div className="flex-shrink-0 mt-1">
107-
<Image
108-
src={mapFrameworkToBadge(framework) ?? ''}
109-
alt={framework.framework.name}
110-
width={400}
111-
height={400}
112-
className="rounded-full w-8 h-8"
113-
/>
108+
{badgeSrc ? (
109+
<Image
110+
src={badgeSrc}
111+
alt={framework.framework.name}
112+
width={400}
113+
height={400}
114+
className="rounded-full w-8 h-8"
115+
/>
116+
) : (
117+
<div className="rounded-full w-8 h-8 bg-muted flex items-center justify-center">
118+
<span className="text-xs text-muted-foreground">
119+
{framework.framework.name.charAt(0)}
120+
</span>
121+
</div>
122+
)}
114123
</div>
115124
<div className="flex flex-col flex-1 min-w-0">
116125
<span className="text-sm font-medium text-foreground flex flex-col">
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
'use server';
2+
3+
import { authActionClient } from '@/actions/safe-action';
4+
import { autoAnswerQuestionnaireTask } from '@/jobs/tasks/vendors/auto-answer-questionnaire';
5+
import { tasks } from '@trigger.dev/sdk';
6+
import { z } from 'zod';
7+
8+
const inputSchema = z.object({
9+
questionsAndAnswers: z.array(
10+
z.object({
11+
question: z.string(),
12+
answer: z.string().nullable(),
13+
}),
14+
),
15+
});
16+
17+
export const autoAnswerQuestionnaire = authActionClient
18+
.inputSchema(inputSchema)
19+
.metadata({
20+
name: 'auto-answer-questionnaire',
21+
track: {
22+
event: 'auto-answer-questionnaire',
23+
channel: 'server',
24+
},
25+
})
26+
.action(async ({ parsedInput, ctx }) => {
27+
const { questionsAndAnswers } = parsedInput;
28+
const { session } = ctx;
29+
30+
if (!session?.activeOrganizationId) {
31+
throw new Error('No active organization');
32+
}
33+
34+
const organizationId = session.activeOrganizationId;
35+
36+
try {
37+
// Trigger the root orchestrator task - it will handle batching internally
38+
const handle = await tasks.trigger<typeof autoAnswerQuestionnaireTask>(
39+
'auto-answer-questionnaire',
40+
{
41+
vendorId: `org_${organizationId}`,
42+
organizationId,
43+
questionsAndAnswers,
44+
},
45+
);
46+
47+
return {
48+
success: true,
49+
data: {
50+
taskId: handle.id, // Return orchestrator task ID for polling
51+
},
52+
};
53+
} catch (error) {
54+
throw error instanceof Error
55+
? error
56+
: new Error('Failed to trigger auto-answer questionnaire');
57+
}
58+
});
59+

0 commit comments

Comments
 (0)