From 9e0506edd19eef9d0c177097e3d1aae9a2f97391 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 14:15:04 -0500 Subject: [PATCH 1/5] fix(app): remove Demo button on booking-step page (#1822) Co-authored-by: chasprowebdev --- .../[orgId]/components/booking-step.tsx | 45 ++++--------------- .../src/app/(app)/upgrade/[orgId]/page.tsx | 22 +-------- 2 files changed, 9 insertions(+), 58 deletions(-) diff --git a/apps/app/src/app/(app)/upgrade/[orgId]/components/booking-step.tsx b/apps/app/src/app/(app)/upgrade/[orgId]/components/booking-step.tsx index 30ce09888..5d2f55660 100644 --- a/apps/app/src/app/(app)/upgrade/[orgId]/components/booking-step.tsx +++ b/apps/app/src/app/(app)/upgrade/[orgId]/components/booking-step.tsx @@ -1,26 +1,19 @@ 'use client'; -import { useState } from 'react'; -import { ArrowRight, Check, Copy } from 'lucide-react'; -import { toast } from 'sonner'; -import Link from 'next/link'; import { Button } from '@comp/ui/button'; import { Card } from '@comp/ui/card'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@comp/ui/tooltip'; +import { Check, Copy } from 'lucide-react'; +import { useState } from 'react'; +import { toast } from 'sonner'; export function BookingStep({ - email, - name, company, orgId, - complianceFrameworks, hasAccess, }: { - email: string; - name: string; company: string; orgId: string; - complianceFrameworks: string[]; hasAccess: boolean; }) { const [isCopied, setIsCopied] = useState(false); @@ -28,19 +21,17 @@ export function BookingStep({ const title = !hasAccess ? `Let's get ${company} approved` : 'Talk to us to upgrade'; const description = !hasAccess - ? `A quick 20-minute call with our team to understand your compliance needs and approve your organization for access.` + ? `Please copy and share the Org ID below in your with your Customer Success Rep in Slack` : `A quick 20-minute call with our team to understand your compliance needs and upgrade your plan.`; - const cta = !hasAccess ? 'Book Your Demo' : 'Book a Call'; - const handleCopyOrgId = async () => { if (isCopied) return; - + try { await navigator.clipboard.writeText(orgId); setIsCopied(true); toast.success('Org ID copied to clipboard'); - + // Reset after 3 seconds setTimeout(() => { setIsCopied(false); @@ -74,7 +65,7 @@ export function BookingStep({ variant="outline" className="text-xs rounded-tl-none rounded-bl-none" onClick={handleCopyOrgId} - aria-label={isCopied ? "Copied!" : "Copy Org ID"} + aria-label={isCopied ? 'Copied!' : 'Copy Org ID'} > {isCopied ? ( @@ -84,31 +75,11 @@ export function BookingStep({ -

{isCopied ? "Copied!" : "Copy Org ID"}

+

{isCopied ? 'Copied!' : 'Copy Org ID'}

- - {/* CTA Button */} -
- - - -
- - {/* Already spoke to us section */} -
-

- Already had a demo? Ask your point of contact to activate your account. -

-
diff --git a/apps/app/src/app/(app)/upgrade/[orgId]/page.tsx b/apps/app/src/app/(app)/upgrade/[orgId]/page.tsx index db8841713..067bd5381 100644 --- a/apps/app/src/app/(app)/upgrade/[orgId]/page.tsx +++ b/apps/app/src/app/(app)/upgrade/[orgId]/page.tsx @@ -51,31 +51,11 @@ export default async function UpgradePage({ params }: PageProps) { redirect(`/${orgId}`); } - const frameworkInstances = await db.frameworkInstance.findMany({ - where: { - organizationId: orgId, - }, - include: { - framework: true, - }, - }); - - const complianceFrameworks = frameworkInstances.map((framework) => - framework.framework.name.toLowerCase().replaceAll(' ', ''), - ); - return ( <>
- +
); From 739853a65b46e06d3db77c9f0fff049df3f814b8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 14:18:08 -0500 Subject: [PATCH 2/5] [dev] [tofikwest] tofik/security-questionnaire-UI (#1819) * fix(security-questionnaire): implement double-click protection and improve parsing state management * fix(file-uploader): remove unnecessary padding from uploader component * refactor(security-questionnaire): clean up auto-answer hook by removing debug logging and optimizing metadata handling * refactor(security-questionnaire): add parse process state management * feat(security-questionnaire): implement manual answer linking and update questionnaire components * fix(security-questionnaire): enable CTA button for navigating to policies page * feat(docs): create documentation (without video) * refactor(security-questionnaire): normalize results and update button states * refactor(parse-questionnaire): enhance chunk processing and question extraction logic --------- Co-authored-by: Tofik Hasanov Co-authored-by: Mariano Fuentes --- apps/app/src/actions/safe-action.ts | 26 +- .../components/QuestionnaireDetailClient.tsx | 103 ++-- .../[questionnaireId]/page.tsx | 2 - .../actions/save-answer.ts | 6 +- .../actions/update-questionnaire-answer.ts | 30 +- .../components/ManualAnswerLink.tsx | 34 ++ .../components/QuestionnaireParser.tsx | 116 ++-- .../components/QuestionnaireResults.tsx | 4 + .../components/QuestionnaireResultsCards.tsx | 18 + .../components/QuestionnaireResultsHeader.tsx | 10 +- .../components/QuestionnaireResultsTable.tsx | 19 + .../components/QuestionnaireUpload.tsx | 4 +- .../components/QuestionnaireView.tsx | 194 +++++++ .../SecurityQuestionnaireBreadcrumb.tsx | 19 - .../hooks/usePersistGeneratedAnswers.ts | 27 +- .../hooks/useQuestionnaireActions.ts | 93 ++- .../hooks/useQuestionnaireAutoAnswer.ts | 426 ++++++-------- .../hooks/useQuestionnaireDetail.ts | 529 ------------------ .../hooks/useQuestionnaireDetail/index.ts | 3 + .../hooks/useQuestionnaireDetail/types.ts | 23 + .../useQuestionnaireDetail.ts | 283 ++++++++++ .../useQuestionnaireDetailHandlers.ts | 310 ++++++++++ .../useQuestionnaireDetailState.ts | 161 ++++++ .../hooks/useQuestionnaireParse.ts | 46 +- .../hooks/useQuestionnaireParser.ts | 103 ++-- .../context/components/ContextSection.tsx | 43 +- .../components/ManualAnswersSection.tsx | 54 +- .../knowledge-base/page.tsx | 2 +- .../components/PublishedPoliciesSection.tsx | 43 +- .../[orgId]/security-questionnaire/page.tsx | 2 +- .../utils/deduplicate-sources.ts | 67 ++- apps/app/src/components/file-uploader.tsx | 2 +- .../tasks/vendors/answer-question-helpers.ts | 94 +++- .../src/jobs/tasks/vendors/answer-question.ts | 39 +- .../jobs/tasks/vendors/parse-questionnaire.ts | 244 +++++--- .../vendor-questionnaire-orchestrator.ts | 64 +-- apps/app/src/lib/vector/core/find-similar.ts | 2 + .../src/lib/vector/core/upsert-embedding.ts | 5 + .../src/lib/vector/sync/sync-manual-answer.ts | 1 + .../src/lib/vector/sync/sync-organization.ts | 1 + packages/docs/docs.json | 2 +- packages/docs/security-questionnaire.mdx | 128 +++++ 42 files changed, 2108 insertions(+), 1274 deletions(-) create mode 100644 apps/app/src/app/(app)/[orgId]/security-questionnaire/components/ManualAnswerLink.tsx create mode 100644 apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireView.tsx delete mode 100644 apps/app/src/app/(app)/[orgId]/security-questionnaire/components/SecurityQuestionnaireBreadcrumb.tsx delete mode 100644 apps/app/src/app/(app)/[orgId]/security-questionnaire/hooks/useQuestionnaireDetail.ts create mode 100644 apps/app/src/app/(app)/[orgId]/security-questionnaire/hooks/useQuestionnaireDetail/index.ts create mode 100644 apps/app/src/app/(app)/[orgId]/security-questionnaire/hooks/useQuestionnaireDetail/types.ts create mode 100644 apps/app/src/app/(app)/[orgId]/security-questionnaire/hooks/useQuestionnaireDetail/useQuestionnaireDetail.ts create mode 100644 apps/app/src/app/(app)/[orgId]/security-questionnaire/hooks/useQuestionnaireDetail/useQuestionnaireDetailHandlers.ts create mode 100644 apps/app/src/app/(app)/[orgId]/security-questionnaire/hooks/useQuestionnaireDetail/useQuestionnaireDetailState.ts create mode 100644 packages/docs/security-questionnaire.mdx diff --git a/apps/app/src/actions/safe-action.ts b/apps/app/src/actions/safe-action.ts index 49280a4ea..e64730d3e 100644 --- a/apps/app/src/actions/safe-action.ts +++ b/apps/app/src/actions/safe-action.ts @@ -83,7 +83,18 @@ export const authActionClient = actionClientWithMeta const headersList = await headers(); let remaining: number | undefined; - if (ratelimit) { + // Exclude answer saving actions from rate limiting + // These actions are user-initiated and should not be rate limited + const excludedActions = [ + 'save-questionnaire-answer', + 'update-questionnaire-answer', + 'save-manual-answer', + 'save-questionnaire-answers-batch', + ]; + + const shouldRateLimit = !excludedActions.includes(metadata.name); + + if (ratelimit && shouldRateLimit) { const { success, remaining: rateLimitRemaining } = await ratelimit.limit( `${headersList.get('x-forwarded-for')}-${metadata.name}`, ); @@ -283,7 +294,18 @@ export const authActionClientWithoutOrg = actionClientWithMeta const headersList = await headers(); let remaining: number | undefined; - if (ratelimit) { + // Exclude answer saving actions from rate limiting + // These actions are user-initiated and should not be rate limited + const excludedActions = [ + 'save-questionnaire-answer', + 'update-questionnaire-answer', + 'save-manual-answer', + 'save-questionnaire-answers-batch', + ]; + + const shouldRateLimit = !excludedActions.includes(metadata.name); + + if (ratelimit && shouldRateLimit) { const { success, remaining: rateLimitRemaining } = await ratelimit.limit( `${headersList.get('x-forwarded-for')}-${metadata.name}`, ); diff --git a/apps/app/src/app/(app)/[orgId]/security-questionnaire/[questionnaireId]/components/QuestionnaireDetailClient.tsx b/apps/app/src/app/(app)/[orgId]/security-questionnaire/[questionnaireId]/components/QuestionnaireDetailClient.tsx index 471f28808..94cf176d8 100644 --- a/apps/app/src/app/(app)/[orgId]/security-questionnaire/[questionnaireId]/components/QuestionnaireDetailClient.tsx +++ b/apps/app/src/app/(app)/[orgId]/security-questionnaire/[questionnaireId]/components/QuestionnaireDetailClient.tsx @@ -1,6 +1,6 @@ 'use client'; -import { QuestionnaireResults } from '../../components/QuestionnaireResults'; +import { QuestionnaireView } from '../../components/QuestionnaireView'; import { useQuestionnaireDetail } from '../../hooks/useQuestionnaireDetail'; interface QuestionnaireDetailClientProps { @@ -33,6 +33,7 @@ export function QuestionnaireDetailClient({ expandedSources, questionStatuses, answeringQuestionIndex, + answerQueue, hasClickedAutoAnswer, isLoading, isAutoAnswering, @@ -57,60 +58,52 @@ export function QuestionnaireDetailClient({ }); return ( -
-
-

{filename}

-

- Review and manage answers for this questionnaire -

-
- ({ - question: r.question, - answer: r.answer, - sources: r.sources, - failedToGenerate: (r as any).failedToGenerate ?? false, // Preserve failedToGenerate from result - status: (r as any).status ?? 'untouched', // Preserve status field for UI behavior - _originalIndex: (r as any).originalIndex ?? index, // Preserve originalIndex for reference, fallback to map index - }))} - filteredResults={filteredResults?.map((r, index) => ({ - question: r.question, - answer: r.answer, - sources: r.sources, - failedToGenerate: (r as any).failedToGenerate ?? false, // Preserve failedToGenerate from result - status: (r as any).status ?? 'untouched', // Preserve status field for UI behavior - _originalIndex: (r as any).originalIndex ?? index, // Preserve originalIndex for reference, fallback to map index - }))} - searchQuery={searchQuery} - onSearchChange={setSearchQuery} - editingIndex={editingIndex} - editingAnswer={editingAnswer} - onEditingAnswerChange={setEditingAnswer} - expandedSources={expandedSources} - questionStatuses={questionStatuses} - answeringQuestionIndex={answeringQuestionIndex} - hasClickedAutoAnswer={hasClickedAutoAnswer} - isLoading={isLoading} - isAutoAnswering={isAutoAnswering} - isExporting={isExporting} - isSaving={isSaving} - savingIndex={savingIndex} - showExitDialog={false} - onShowExitDialogChange={() => {}} - onExit={() => {}} - onAutoAnswer={handleAutoAnswer} - onAnswerSingleQuestion={handleAnswerSingleQuestion} - onEditAnswer={handleEditAnswer} - onSaveAnswer={handleSaveAnswer} - onCancelEdit={handleCancelEdit} - onExport={handleExport} - onToggleSource={handleToggleSource} - totalCount={totalCount} - answeredCount={answeredCount} - progressPercentage={progressPercentage} - /> -
+ ({ + question: r.question, + answer: r.answer, + sources: r.sources, + failedToGenerate: (r as any).failedToGenerate ?? false, + status: (r as any).status ?? 'untouched', + _originalIndex: (r as any).originalIndex ?? index, + }))} + filteredResults={filteredResults?.map((r, index) => ({ + question: r.question, + answer: r.answer, + sources: r.sources, + failedToGenerate: (r as any).failedToGenerate ?? false, + status: (r as any).status ?? 'untouched', + _originalIndex: (r as any).originalIndex ?? index, + }))} + searchQuery={searchQuery} + setSearchQuery={setSearchQuery} + editingIndex={editingIndex} + editingAnswer={editingAnswer} + setEditingAnswer={setEditingAnswer} + expandedSources={expandedSources} + questionStatuses={questionStatuses as Map} + answeringQuestionIndex={answeringQuestionIndex} + answerQueue={answerQueue} + hasClickedAutoAnswer={hasClickedAutoAnswer} + isLoading={isLoading} + isAutoAnswering={isAutoAnswering} + isExporting={isExporting} + isSaving={isSaving} + savingIndex={savingIndex} + totalCount={totalCount} + answeredCount={answeredCount} + progressPercentage={progressPercentage} + onAutoAnswer={handleAutoAnswer} + onAnswerSingleQuestion={handleAnswerSingleQuestion} + onEditAnswer={handleEditAnswer} + onSaveAnswer={handleSaveAnswer} + onCancelEdit={handleCancelEdit} + onExport={handleExport} + onToggleSource={handleToggleSource} + filename={filename} + description="Review and manage answers for this questionnaire" + /> ); } diff --git a/apps/app/src/app/(app)/[orgId]/security-questionnaire/[questionnaireId]/page.tsx b/apps/app/src/app/(app)/[orgId]/security-questionnaire/[questionnaireId]/page.tsx index 77808dd13..022f57ec7 100644 --- a/apps/app/src/app/(app)/[orgId]/security-questionnaire/[questionnaireId]/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/security-questionnaire/[questionnaireId]/page.tsx @@ -2,8 +2,6 @@ import PageWithBreadcrumb from '@/components/pages/PageWithBreadcrumb'; import { auth } from '@/utils/auth'; import { headers } from 'next/headers'; import { notFound } from 'next/navigation'; -import { QuestionnaireResults } from '../components/QuestionnaireResults'; -import { useQuestionnaireDetail } from '../hooks/useQuestionnaireDetail'; import { getQuestionnaireById } from './data/queries'; import { QuestionnaireDetailClient } from './components/QuestionnaireDetailClient'; diff --git a/apps/app/src/app/(app)/[orgId]/security-questionnaire/actions/save-answer.ts b/apps/app/src/app/(app)/[orgId]/security-questionnaire/actions/save-answer.ts index 0774151a9..08bb3256f 100644 --- a/apps/app/src/app/(app)/[orgId]/security-questionnaire/actions/save-answer.ts +++ b/apps/app/src/app/(app)/[orgId]/security-questionnaire/actions/save-answer.ts @@ -90,8 +90,10 @@ export const saveAnswerAction = authActionClient }, }); - // If status is manual and answer exists, also save to SecurityQuestionnaireManualAnswer - if (status === 'manual' && answer && answer.trim().length > 0 && existingQuestion.question) { + const shouldPersistManualAnswer = + status === 'manual' && answer && answer.trim().length > 0 && existingQuestion.question; + + if (shouldPersistManualAnswer) { try { const manualAnswer = await db.securityQuestionnaireManualAnswer.upsert({ where: { diff --git a/apps/app/src/app/(app)/[orgId]/security-questionnaire/actions/update-questionnaire-answer.ts b/apps/app/src/app/(app)/[orgId]/security-questionnaire/actions/update-questionnaire-answer.ts index ad55ed9b3..6589c3a10 100644 --- a/apps/app/src/app/(app)/[orgId]/security-questionnaire/actions/update-questionnaire-answer.ts +++ b/apps/app/src/app/(app)/[orgId]/security-questionnaire/actions/update-questionnaire-answer.ts @@ -12,6 +12,19 @@ const updateAnswerSchema = z.object({ questionnaireId: z.string(), questionAnswerId: z.string(), answer: z.string(), + status: z.enum(['generated', 'manual']).optional().default('manual'), + sources: z + .array( + z.object({ + sourceType: z.string(), + sourceName: z.string().optional(), + sourceId: z.string().optional(), + policyName: z.string().optional(), + documentName: z.string().optional(), + score: z.number(), + }), + ) + .optional(), }); export const updateQuestionnaireAnswer = authActionClient @@ -25,7 +38,7 @@ export const updateQuestionnaireAnswer = authActionClient }, }) .action(async ({ parsedInput, ctx }) => { - const { questionnaireId, questionAnswerId, answer } = parsedInput; + const { questionnaireId, questionAnswerId, answer, status, sources } = parsedInput; const { activeOrganizationId } = ctx.session; const userId = ctx.user.id; @@ -67,6 +80,9 @@ export const updateQuestionnaireAnswer = authActionClient }; } + // Store the previous status to determine if this was written from scratch + const previousStatus = questionAnswer.status; + // Update the answer await db.questionnaireQuestionAnswer.update({ where: { @@ -74,14 +90,18 @@ export const updateQuestionnaireAnswer = authActionClient }, data: { answer: answer.trim() || null, - status: 'manual', - updatedBy: userId || null, + status: status === 'generated' ? 'generated' : 'manual', + sources: sources ? (sources as any) : null, + generatedAt: status === 'generated' ? new Date() : null, + updatedBy: status === 'manual' ? userId || null : null, updatedAt: new Date(), }, }); - // Also save to SecurityQuestionnaireManualAnswer if answer exists - if (answer && answer.trim().length > 0 && questionAnswer.question) { + const shouldPersistManualAnswer = + status === 'manual' && answer && answer.trim().length > 0 && questionAnswer.question; + + if (shouldPersistManualAnswer) { try { const manualAnswer = await db.securityQuestionnaireManualAnswer.upsert({ where: { diff --git a/apps/app/src/app/(app)/[orgId]/security-questionnaire/components/ManualAnswerLink.tsx b/apps/app/src/app/(app)/[orgId]/security-questionnaire/components/ManualAnswerLink.tsx new file mode 100644 index 000000000..46727807f --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/security-questionnaire/components/ManualAnswerLink.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { LinkIcon } from 'lucide-react'; +import Link from 'next/link'; + +interface ManualAnswerLinkProps { + manualAnswerId: string; + sourceName: string; + orgId: string; + className?: string; +} + +export function ManualAnswerLink({ + manualAnswerId, + sourceName, + orgId, + className = 'font-medium text-primary hover:underline inline-flex items-center gap-1', +}: ManualAnswerLinkProps) { + // Link to knowledge base page with hash anchor to scroll to specific manual answer + const knowledgeBaseUrl = `/${orgId}/security-questionnaire/knowledge-base#manual-answer-${manualAnswerId}`; + + return ( + + {sourceName} + + + ); +} + diff --git a/apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireParser.tsx b/apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireParser.tsx index 831cf1b90..07581a8ee 100644 --- a/apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireParser.tsx +++ b/apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireParser.tsx @@ -1,9 +1,7 @@ 'use client'; import { useQuestionnaireParser } from '../hooks/useQuestionnaireParser'; -import { QuestionnaireResults } from './QuestionnaireResults'; -import { QuestionnaireSidebar } from './QuestionnaireSidebar'; -import { QuestionnaireUpload } from './QuestionnaireUpload'; +import { QuestionnaireView } from './QuestionnaireView'; export function QuestionnaireParser() { const { @@ -42,74 +40,54 @@ export function QuestionnaireParser() { handleToggleSource, } = useQuestionnaireParser(); - const hasResults = results && results.length > 0; + const normalizedResults = + results?.map((result) => ({ + ...result, + sources: result.sources ?? [], + })) ?? null; - if (!hasResults) { - return ( -
-
-

- Security Questionnaire -

-

- Automatically analyze and answer questionnaires using AI. Upload questionnaires from - vendors, and our system will extract questions and generate answers based on your - organization's policies and documentation. -

-
-
-
- setSelectedFile(null)} - onParse={handleParse} - isLoading={isLoading} - parseStatus={parseStatus} - orgId={orgId} - /> -
-
- -
-
-
- ); - } + const normalizedFilteredResults = + filteredResults?.map((result) => ({ + ...result, + sources: result.sources ?? [], + })) ?? null; return ( -
-

Security Questionnaire

- -
+ setSelectedFile(null)} + onParse={handleParse} + parseStatus={parseStatus} + showExitDialog={showExitDialog} + onShowExitDialogChange={setShowExitDialog} + onExit={confirmReset} + /> ); } diff --git a/apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireResults.tsx b/apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireResults.tsx index 3f276b371..8850ea3d9 100644 --- a/apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireResults.tsx +++ b/apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireResults.tsx @@ -19,6 +19,7 @@ interface QuestionnaireResultsProps { expandedSources: Set; questionStatuses: Map; answeringQuestionIndex: number | null; + answerQueue?: number[]; hasClickedAutoAnswer: boolean; isLoading: boolean; isAutoAnswering: boolean; @@ -52,6 +53,7 @@ export function QuestionnaireResults({ expandedSources, questionStatuses, answeringQuestionIndex, + answerQueue = [], hasClickedAutoAnswer, isLoading, isAutoAnswering, @@ -111,6 +113,7 @@ export function QuestionnaireResults({ expandedSources={expandedSources} questionStatuses={questionStatuses} answeringQuestionIndex={answeringQuestionIndex} + answerQueue={answerQueue} isAutoAnswering={isAutoAnswering} hasClickedAutoAnswer={hasClickedAutoAnswer} isSaving={isSaving} @@ -133,6 +136,7 @@ export function QuestionnaireResults({ expandedSources={expandedSources} questionStatuses={questionStatuses} answeringQuestionIndex={answeringQuestionIndex} + answerQueue={answerQueue} isAutoAnswering={isAutoAnswering} hasClickedAutoAnswer={hasClickedAutoAnswer} isSaving={isSaving} diff --git a/apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireResultsCards.tsx b/apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireResultsCards.tsx index ced669304..2d5aa88b9 100644 --- a/apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireResultsCards.tsx +++ b/apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireResultsCards.tsx @@ -7,6 +7,7 @@ import Link from 'next/link'; import type { QuestionAnswer } from './types'; import { deduplicateSources } from '../utils/deduplicate-sources'; import { KnowledgeBaseDocumentLink } from './KnowledgeBaseDocumentLink'; +import { ManualAnswerLink } from './ManualAnswerLink'; interface QuestionnaireResultsCardsProps { orgId: string; @@ -18,6 +19,7 @@ interface QuestionnaireResultsCardsProps { expandedSources: Set; questionStatuses: Map; answeringQuestionIndex: number | null; + answerQueue?: number[]; isAutoAnswering: boolean; hasClickedAutoAnswer: boolean; isSaving?: boolean; @@ -39,6 +41,7 @@ export function QuestionnaireResultsCards({ expandedSources, questionStatuses, answeringQuestionIndex, + answerQueue = [], isAutoAnswering, hasClickedAutoAnswer, isSaving, @@ -63,6 +66,8 @@ export function QuestionnaireResultsCards({ const uniqueSources = qa.sources ? deduplicateSources(qa.sources) : []; const isEditing = editingIndex === safeIndex; const questionStatus = questionStatuses.get(safeIndex); + // Check if question is in queue (waiting to be processed) + const isQueued = answerQueue.includes(safeIndex); // Determine if this question is being processed // It's processing if: // 1. Status is explicitly 'processing' @@ -138,6 +143,11 @@ export function QuestionnaireResultsCards({ Finding answer... + ) : isQueued ? ( +
+ + Finding answer... +
) : (
{!qa.failedToGenerate && ( @@ -204,6 +214,8 @@ export function QuestionnaireResultsCards({ const isPolicy = source.sourceType === 'policy' && source.sourceId; const isKnowledgeBaseDocument = source.sourceType === 'knowledge_base_document' && source.sourceId; + const isManualAnswer = + source.sourceType === 'manual_answer' && source.sourceId; const sourceContent = source.sourceName || source.sourceType; return ( @@ -225,6 +237,12 @@ export function QuestionnaireResultsCards({ sourceName={sourceContent} orgId={orgId} /> + ) : isManualAnswer && source.sourceId ? ( + ) : ( {sourceContent} diff --git a/apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireResultsHeader.tsx b/apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireResultsHeader.tsx index d14ca9e34..96ada8255 100644 --- a/apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireResultsHeader.tsx +++ b/apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireResultsHeader.tsx @@ -154,30 +154,30 @@ export function QuestionnaireResultsHeader({ - - + onExport('xlsx')} - disabled={isExporting || isLoading} + disabled={isExporting} > Excel onExport('csv')} - disabled={isExporting || isLoading} + disabled={isExporting} > CSV onExport('pdf')} - disabled={isExporting || isLoading} + disabled={isExporting} > PDF diff --git a/apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireResultsTable.tsx b/apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireResultsTable.tsx index 8e3e877d2..0c827dbc4 100644 --- a/apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireResultsTable.tsx +++ b/apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireResultsTable.tsx @@ -8,6 +8,7 @@ import Link from 'next/link'; import type { QuestionAnswer } from './types'; import { deduplicateSources } from '../utils/deduplicate-sources'; import { KnowledgeBaseDocumentLink } from './KnowledgeBaseDocumentLink'; +import { ManualAnswerLink } from './ManualAnswerLink'; interface QuestionnaireResultsTableProps { orgId: string; @@ -19,6 +20,7 @@ interface QuestionnaireResultsTableProps { expandedSources: Set; questionStatuses: Map; answeringQuestionIndex: number | null; + answerQueue?: number[]; isAutoAnswering: boolean; hasClickedAutoAnswer: boolean; isSaving?: boolean; @@ -40,6 +42,7 @@ export function QuestionnaireResultsTable({ expandedSources, questionStatuses, answeringQuestionIndex, + answerQueue = [], isAutoAnswering, hasClickedAutoAnswer, isSaving, @@ -70,6 +73,8 @@ export function QuestionnaireResultsTable({ const safeIndex = originalIndex >= 0 ? originalIndex : index; const isEditing = editingIndex === safeIndex; const questionStatus = questionStatuses.get(safeIndex); + // Check if question is in queue (waiting to be processed) + const isQueued = answerQueue.includes(safeIndex); // Determine if this question is being processed // It's processing if: // 1. Status is explicitly 'processing' @@ -145,6 +150,11 @@ export function QuestionnaireResultsTable({ Finding answer...
+ ) : isQueued ? ( +
+ + Finding answer... +
) : qa.failedToGenerate ? (

@@ -217,6 +227,8 @@ export function QuestionnaireResultsTable({ const isPolicy = source.sourceType === 'policy' && source.sourceId; const isKnowledgeBaseDocument = source.sourceType === 'knowledge_base_document' && source.sourceId; + const isManualAnswer = + source.sourceType === 'manual_answer' && source.sourceId; const sourceContent = source.sourceName || source.sourceType; return ( @@ -241,6 +253,13 @@ export function QuestionnaireResultsTable({ orgId={orgId} className="text-primary hover:underline flex items-center gap-1 disabled:opacity-50 disabled:cursor-not-allowed" /> + ) : isManualAnswer && source.sourceId ? ( + ) : ( {sourceContent} )} diff --git a/apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireUpload.tsx b/apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireUpload.tsx index 2ed367dfe..a1656f500 100644 --- a/apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireUpload.tsx +++ b/apps/app/src/app/(app)/[orgId]/security-questionnaire/components/QuestionnaireUpload.tsx @@ -19,6 +19,7 @@ interface QuestionnaireUploadProps { isLoading: boolean; parseStatus: 'uploading' | 'starting' | 'queued' | 'analyzing' | 'processing' | null; orgId: string; + hasResults?: boolean; } export function QuestionnaireUpload({ @@ -28,6 +29,7 @@ export function QuestionnaireUpload({ onParse, isLoading, parseStatus, + hasResults = false, }: QuestionnaireUploadProps) { return (

@@ -99,7 +101,7 @@ export function QuestionnaireUpload({