Skip to content

Commit 2b9659a

Browse files
[dev] [Marfuen] mariano/security-questionnaire-ui (#1762)
* refactor(security-questionnaire): reorganize imports and update header text * chore(security-questionnaire): enhance auto-answer button and add error handling for unanswered questions --------- Co-authored-by: Mariano Fuentes <marfuen98@gmail.com>
1 parent a3db984 commit 2b9659a

File tree

4 files changed

+89
-91
lines changed

4 files changed

+89
-91
lines changed

apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireParser.tsx

Lines changed: 39 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
'use client';
22

3+
import { useQuestionnaireParser } from '../hooks/useQuestionnaireParser';
34
import { QuestionnaireResults } from './QuestionnaireResults';
4-
import { QuestionnaireUpload } from './QuestionnaireUpload';
55
import { QuestionnaireSidebar } from './QuestionnaireSidebar';
6-
import { useQuestionnaireParser } from '../hooks/useQuestionnaireParser';
6+
import { QuestionnaireUpload } from './QuestionnaireUpload';
77

88
export function QuestionnaireParser() {
99
const {
@@ -60,14 +60,14 @@ export function QuestionnaireParser() {
6060
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 lg:gap-12">
6161
<div className="lg:col-span-2">
6262
<QuestionnaireUpload
63-
selectedFile={selectedFile}
64-
onFileSelect={handleFileSelect}
65-
onFileRemove={() => setSelectedFile(null)}
66-
onParse={handleParse}
67-
isLoading={isLoading}
68-
parseStatus={parseStatus}
69-
orgId={orgId}
70-
/>
63+
selectedFile={selectedFile}
64+
onFileSelect={handleFileSelect}
65+
onFileRemove={() => setSelectedFile(null)}
66+
onParse={handleParse}
67+
isLoading={isLoading}
68+
parseStatus={parseStatus}
69+
orgId={orgId}
70+
/>
7171
</div>
7272
<div className="lg:col-span-1">
7373
<QuestionnaireSidebar />
@@ -79,38 +79,36 @@ export function QuestionnaireParser() {
7979

8080
return (
8181
<div className="flex flex-col gap-6">
82-
<h1 className="text-xl lg:text-2xl font-semibold text-foreground">
83-
Vendor Questionnaire
84-
</h1>
82+
<h1 className="text-xl lg:text-2xl font-semibold text-foreground">Security Questionnaire</h1>
8583
<QuestionnaireResults
86-
orgId={orgId}
87-
results={results}
88-
filteredResults={filteredResults}
89-
searchQuery={searchQuery}
90-
onSearchChange={setSearchQuery}
91-
editingIndex={editingIndex}
92-
editingAnswer={editingAnswer}
93-
onEditingAnswerChange={setEditingAnswer}
94-
expandedSources={expandedSources}
95-
questionStatuses={questionStatuses}
96-
answeringQuestionIndex={answeringQuestionIndex}
97-
hasClickedAutoAnswer={hasClickedAutoAnswer}
98-
isLoading={isLoading}
99-
isAutoAnswering={isAutoAnswering}
100-
isExporting={isExporting}
101-
showExitDialog={showExitDialog}
102-
onShowExitDialogChange={setShowExitDialog}
103-
onExit={confirmReset}
104-
onAutoAnswer={handleAutoAnswer}
105-
onAnswerSingleQuestion={handleAnswerSingleQuestion}
106-
onEditAnswer={handleEditAnswer}
107-
onSaveAnswer={handleSaveAnswer}
108-
onCancelEdit={handleCancelEdit}
109-
onExport={handleExport}
110-
onToggleSource={handleToggleSource}
111-
totalCount={totalCount}
112-
answeredCount={answeredCount}
113-
progressPercentage={progressPercentage}
84+
orgId={orgId}
85+
results={results}
86+
filteredResults={filteredResults}
87+
searchQuery={searchQuery}
88+
onSearchChange={setSearchQuery}
89+
editingIndex={editingIndex}
90+
editingAnswer={editingAnswer}
91+
onEditingAnswerChange={setEditingAnswer}
92+
expandedSources={expandedSources}
93+
questionStatuses={questionStatuses}
94+
answeringQuestionIndex={answeringQuestionIndex}
95+
hasClickedAutoAnswer={hasClickedAutoAnswer}
96+
isLoading={isLoading}
97+
isAutoAnswering={isAutoAnswering}
98+
isExporting={isExporting}
99+
showExitDialog={showExitDialog}
100+
onShowExitDialogChange={setShowExitDialog}
101+
onExit={confirmReset}
102+
onAutoAnswer={handleAutoAnswer}
103+
onAnswerSingleQuestion={handleAnswerSingleQuestion}
104+
onEditAnswer={handleEditAnswer}
105+
onSaveAnswer={handleSaveAnswer}
106+
onCancelEdit={handleCancelEdit}
107+
onExport={handleExport}
108+
onToggleSource={handleToggleSource}
109+
totalCount={totalCount}
110+
answeredCount={answeredCount}
111+
progressPercentage={progressPercentage}
114112
/>
115113
</div>
116114
);

apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireResultsHeader.tsx

Lines changed: 14 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -135,43 +135,23 @@ export function QuestionnaireResultsHeader({
135135
</div>
136136

137137
<div className="flex items-center gap-3">
138-
<div className="relative">
139-
{!hasClickedAutoAnswer && results.some((qa) => !qa.answer) && (
138+
<Button
139+
onClick={(e) => {
140+
e.stopPropagation();
141+
onAutoAnswer();
142+
}}
143+
disabled={isAutoAnswering || isLoading}
144+
size="default"
145+
>
146+
{isAutoAnswering ? (
147+
<Loader2 className="size-4 animate-spin" />
148+
) : (
140149
<>
141-
<style
142-
dangerouslySetInnerHTML={{
143-
__html: `
144-
@keyframes ping-subtle {
145-
0%, 100% { transform: scale(1); opacity: 0.8; }
146-
60% { transform: scale(1.05); opacity: 0.5; }
147-
}
148-
`,
149-
}}
150-
/>
151-
<span
152-
className="absolute -inset-0.5 rounded-xs bg-primary/10"
153-
style={{ animation: 'ping-subtle 2.5s ease-in-out infinite' }}
154-
/>
150+
<Zap className="size-4" />
151+
Auto-Fill All
155152
</>
156153
)}
157-
<Button
158-
onClick={(e) => {
159-
e.stopPropagation();
160-
onAutoAnswer();
161-
}}
162-
disabled={isAutoAnswering || isLoading}
163-
size="default"
164-
>
165-
{isAutoAnswering ? (
166-
<Loader2 className="size-4 animate-spin" />
167-
) : (
168-
<>
169-
<Zap className="size-4" />
170-
Auto-Fill All
171-
</>
172-
)}
173-
</Button>
174-
</div>
154+
</Button>
175155
<DropdownMenu>
176156
<DropdownMenuTrigger asChild>
177157
<Button variant="outline" size="default" disabled={isExporting || isLoading}>

apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireResultsTable.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,22 @@ export function QuestionnaireResultsTable({
100100
<Loader2 className="h-4 w-4 animate-spin text-primary" />
101101
<span className="text-sm text-muted-foreground">Finding answer...</span>
102102
</div>
103+
) : qa.failedToGenerate ? (
104+
<div className="flex items-center justify-between gap-4">
105+
<p className="text-sm text-muted-foreground italic">
106+
Could not find an answer
107+
</p>
108+
<Button
109+
onClick={(e) => {
110+
e.stopPropagation();
111+
onEditAnswer(originalIndex);
112+
}}
113+
variant="outline"
114+
size="sm"
115+
>
116+
Write Answer
117+
</Button>
118+
</div>
103119
) : (
104120
<div className="flex gap-2 justify-end">
105121
<Button

apps/app/src/jobs/tasks/vendors/answer-question-helpers.ts

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { findSimilarContent } from '@/lib/vector';
2-
import { logger } from '@trigger.dev/sdk';
32
import { openai } from '@ai-sdk/openai';
3+
import { logger } from '@trigger.dev/sdk';
44
import { generateText } from 'ai';
55

66
export interface AnswerWithSources {
@@ -37,13 +37,16 @@ export async function generateAnswerWithRAG(
3737

3838
// Extract sources information and deduplicate by sourceName
3939
// Multiple chunks from the same source (same policy/context) should appear as a single source
40-
const sourceMap = new Map<string, {
41-
sourceType: string;
42-
sourceName?: string;
43-
sourceId: string;
44-
policyName?: string;
45-
score: number;
46-
}>();
40+
const sourceMap = new Map<
41+
string,
42+
{
43+
sourceType: string;
44+
sourceName?: string;
45+
sourceId: string;
46+
policyName?: string;
47+
score: number;
48+
}
49+
>();
4750

4851
for (const result of similarContent) {
4952
// Generate sourceName first to use as deduplication key
@@ -55,12 +58,12 @@ export async function generateAnswerWithRAG(
5558
} else if (result.contextQuestion) {
5659
sourceName = 'Context Q&A';
5760
}
58-
61+
5962
// Use sourceName as the unique key to prevent duplicates
6063
// For policies: same policy name = same source
6164
// For context: all context entries = single "Context Q&A" source
6265
const key = sourceName || result.sourceId;
63-
66+
6467
// If we haven't seen this source, or this chunk has a higher score, use it
6568
const existing = sourceMap.get(key);
6669
if (!existing || result.score > existing.score) {
@@ -114,11 +117,13 @@ Your task is to answer questions based ONLY on the provided context from the org
114117
CRITICAL RULES:
115118
1. Answer based ONLY on the provided context. Do not make up facts or use general knowledge.
116119
2. If the context does not contain enough information to answer the question, respond with exactly: "N/A - no evidence found"
117-
3. Be concise and direct. Use enterprise-ready language.
118-
4. If multiple sources provide information, synthesize them into a coherent answer.
119-
5. Do not include disclaimers or notes about the source unless specifically relevant.
120-
6. Format your answer as a clear, professional response suitable for a vendor questionnaire.
121-
7. Always write in first person plural (we, our, us) as if speaking on behalf of the organization.`,
120+
3. BE CONCISE. Give SHORT, direct answers. Do NOT provide detailed explanations or elaborate unnecessarily.
121+
4. Use enterprise-ready language appropriate for vendor questionnaires.
122+
5. If multiple sources provide information, synthesize them into ONE concise answer.
123+
6. Do not include disclaimers or notes about the source unless specifically relevant.
124+
7. Format your answer as a clear, professional response suitable for a vendor questionnaire.
125+
8. Always write in first person plural (we, our, us) as if speaking on behalf of the organization.
126+
9. Keep answers to 1-3 sentences maximum unless the question explicitly requires more detail.`,
122127
prompt: `Based on the following context from our organization's policies and documentation, answer this question:
123128
124129
Question: ${question}
@@ -149,4 +154,3 @@ Answer the question based ONLY on the provided context, using first person plura
149154
return { answer: null, sources: [] };
150155
}
151156
}
152-

0 commit comments

Comments
 (0)