fh / src /components /RFPAnalyzer.svelte
Varun10000's picture
Upload 57 files
d8635c9 verified
Raw
History Blame Contribute Delete
22.3 kB
<script lang="ts">
import { analyzeRFPQuestion, type AnalyzedAnswer, type StructuredPreviousAnswer, type MissingPoint } from '../utils/rfpAnalyzer';
import { onMount } from 'svelte';
export let question: string = '';
export let currentRFPContext: string = '';
let analyzing = false;
let analyzedResult: AnalyzedAnswer | null = null;
let error: string = '';
let selectedTab: 'previous' | 'ai' | 'gaps' | 'suggestions' = 'previous';
async function analyzeQuestion() {
if (!question.trim()) {
error = 'Please enter a question to analyze';
return;
}
analyzing = true;
error = '';
analyzedResult = null;
try {
console.log('🔍 Analyzing RFP question...');
const result = await analyzeRFPQuestion(question, currentRFPContext);
analyzedResult = result;
console.log('✅ Analysis complete:', result);
} catch (err) {
console.error('❌ Analysis failed:', err);
error = err instanceof Error ? err.message : 'Analysis failed';
} finally {
analyzing = false;
}
}
function getImportanceColor(importance: string): string {
const colors = {
critical: 'text-red-600 bg-red-50',
high: 'text-orange-600 bg-orange-50',
medium: 'text-yellow-600 bg-yellow-50',
low: 'text-blue-600 bg-blue-50'
};
return colors[importance as keyof typeof colors] || 'text-gray-600 bg-gray-50';
}
function getCoverageColor(coverage: number): string {
if (coverage >= 0.8) return 'text-green-600';
if (coverage >= 0.6) return 'text-yellow-600';
return 'text-red-600';
}
function copyToClipboard(text: string) {
navigator.clipboard.writeText(text);
alert('Copied to clipboard!');
}
</script>
<div class="rfp-analyzer-container">
<!-- Analysis Trigger -->
<div class="mb-6">
<div class="flex gap-3">
<button
on:click={analyzeQuestion}
disabled={analyzing || !question.trim()}
class="btn-primary"
>
{#if analyzing}
<span class="spinner"></span>
Analyzing...
{:else}
🔍 Analyze RFP Question
{/if}
</button>
{#if analyzedResult}
<div class="confidence-badge">
Confidence: <strong>{Math.round(analyzedResult.confidence * 100)}%</strong>
</div>
{/if}
</div>
{#if error}
<div class="error-message mt-3">
❌ {error}
</div>
{/if}
</div>
{#if analyzedResult}
<!-- Tabs -->
<div class="tabs-container">
<button
class:active={selectedTab === 'previous'}
on:click={() => selectedTab = 'previous'}
>
📚 Previous Answers ({analyzedResult.previousAnswers.length})
</button>
<button
class:active={selectedTab === 'ai'}
on:click={() => selectedTab = 'ai'}
>
🤖 AI Generated Answer
</button>
<button
class:active={selectedTab === 'gaps'}
on:click={() => selectedTab = 'gaps'}
>
📊 Gap Analysis ({analyzedResult.gapAnalysis.missingPoints.length})
</button>
<button
class:active={selectedTab === 'suggestions'}
on:click={() => selectedTab = 'suggestions'}
>
💡 Suggestions ({analyzedResult.suggestions.length})
</button>
</div>
<!-- Tab Content -->
<div class="tab-content">
<!-- Previous Answers Tab -->
{#if selectedTab === 'previous'}
<div class="previous-answers-section">
<h3>📚 Structured Previous Answers</h3>
<p class="text-sm text-gray-600 mb-4">
Found {analyzedResult.previousAnswers.length} relevant previous RFP responses
</p>
{#each analyzedResult.previousAnswers as answer, idx}
<div class="answer-card">
<div class="answer-header">
<div>
<h4>{answer.source}</h4>
<span class="category-badge">{answer.category}</span>
<span class="relevance-score">
Relevance: {Math.round(answer.relevanceScore * 100)}%
</span>
</div>
<button
class="btn-copy"
on:click={() => copyToClipboard(answer.content)}
title="Copy content"
>
📋 Copy
</button>
</div>
<!-- Key Points -->
{#if answer.keyPoints.length > 0}
<div class="key-points">
<strong>🎯 Key Points:</strong>
<ul>
{#each answer.keyPoints.slice(0, 5) as point}
<li>{point}</li>
{/each}
</ul>
</div>
{/if}
<!-- Metrics -->
{#if answer.metrics.length > 0}
<div class="metrics-section">
<strong>📊 Metrics Found:</strong>
<div class="metrics-grid">
{#each answer.metrics.slice(0, 6) as metric}
<div class="metric-item">
<span class="metric-value">{metric.value}</span>
<span class="metric-context">{metric.context.substring(0, 80)}...</span>
</div>
{/each}
</div>
</div>
{/if}
<!-- Full Content (collapsed by default) -->
<details class="content-details">
<summary>View Full Content ({answer.content.length} characters)</summary>
<div class="content-body">
{answer.content}
</div>
</details>
</div>
{/each}
</div>
{/if}
<!-- AI Generated Answer Tab -->
{#if selectedTab === 'ai'}
<div class="ai-answer-section">
<div class="answer-stats">
<div class="stat-item">
<span class="stat-label">Word Count:</span>
<span class="stat-value">{analyzedResult.aiGeneratedAnswer.wordCount}</span>
</div>
<div class="stat-item">
<span class="stat-label">Tone:</span>
<span class="stat-value">{analyzedResult.aiGeneratedAnswer.tone}</span>
</div>
<div class="stat-item">
<span class="stat-label">Comprehensiveness:</span>
<span class="stat-value {getCoverageColor(analyzedResult.aiGeneratedAnswer.comprehensiveness)}">
{Math.round(analyzedResult.aiGeneratedAnswer.comprehensiveness * 100)}%
</span>
</div>
<button
class="btn-copy"
on:click={() => copyToClipboard(analyzedResult.aiGeneratedAnswer.answer)}
>
📋 Copy Answer
</button>
</div>
<!-- Addressed Points -->
{#if analyzedResult.aiGeneratedAnswer.addressedPoints.length > 0}
<div class="addressed-points">
<strong>✅ Addressed Requirements:</strong>
<div class="points-grid">
{#each analyzedResult.aiGeneratedAnswer.addressedPoints as point}
<span class="point-badge">{point}</span>
{/each}
</div>
</div>
{/if}
<!-- Strengths -->
{#if analyzedResult.aiGeneratedAnswer.strengths.length > 0}
<div class="strengths-section">
<strong>💪 Answer Strengths:</strong>
<ul>
{#each analyzedResult.aiGeneratedAnswer.strengths as strength}
<li>✓ {strength}</li>
{/each}
</ul>
</div>
{/if}
<!-- Generated Answer -->
<div class="generated-answer">
<h4>🤖 Generated RFP Response:</h4>
<div class="answer-content">
{@html analyzedResult.aiGeneratedAnswer.answer.replace(/\n/g, '<br/>')}
</div>
</div>
<!-- Citations -->
{#if analyzedResult.aiGeneratedAnswer.citations.length > 0}
<div class="citations-section">
<strong>📚 Citations:</strong>
<ul>
{#each analyzedResult.aiGeneratedAnswer.citations as citation}
<li>{citation}</li>
{/each}
</ul>
</div>
{/if}
</div>
{/if}
<!-- Gap Analysis Tab -->
{#if selectedTab === 'gaps'}
<div class="gap-analysis-section">
<div class="coverage-summary">
<h3>Overall Coverage:
<span class={getCoverageColor(analyzedResult.gapAnalysis.overallCoverage)}>
{Math.round(analyzedResult.gapAnalysis.overallCoverage * 100)}%
</span>
</h3>
<div class="progress-bar">
<div
class="progress-fill"
style="width: {analyzedResult.gapAnalysis.overallCoverage * 100}%"
></div>
</div>
</div>
<!-- Covered Points -->
{#if analyzedResult.gapAnalysis.coveredPoints.length > 0}
<div class="covered-points">
<h4>✅ Covered Requirements ({analyzedResult.gapAnalysis.coveredPoints.length})</h4>
{#each analyzedResult.gapAnalysis.coveredPoints as point}
<div class="covered-point-item">
<div class="point-header">
<strong>{point.point}</strong>
<span class="confidence-badge small">
{Math.round(point.confidence * 100)}% confidence
</span>
</div>
<div class="point-source">Source: {point.source}</div>
{#if point.evidence}
<div class="point-evidence">"{point.evidence}..."</div>
{/if}
</div>
{/each}
</div>
{/if}
<!-- Missing Points -->
{#if analyzedResult.gapAnalysis.missingPoints.length > 0}
<div class="missing-points">
<h4>⚠️ Missing Requirements ({analyzedResult.gapAnalysis.missingPoints.length})</h4>
{#each analyzedResult.gapAnalysis.missingPoints as point}
<div class="missing-point-item">
<div class="point-header">
<strong>{point.point}</strong>
<span class="importance-badge {getImportanceColor(point.importance)}">
{point.importance.toUpperCase()}
</span>
</div>
<div class="point-suggestion">
<strong>Suggestion:</strong> {point.suggestion}
</div>
{#if point.examples && point.examples.length > 0}
<details class="examples-details">
<summary>View Examples</summary>
<ul>
{#each point.examples as example}
<li>{example}</li>
{/each}
</ul>
</details>
{/if}
</div>
{/each}
</div>
{/if}
<!-- Weak Points -->
{#if analyzedResult.gapAnalysis.weakPoints.length > 0}
<div class="weak-points">
<h4>🔧 Areas Needing Improvement ({analyzedResult.gapAnalysis.weakPoints.length})</h4>
{#each analyzedResult.gapAnalysis.weakPoints as point}
<div class="weak-point-item">
<strong>{point.point}</strong>
<div class="current-coverage">
<em>Current:</em> {point.currentCoverage}
</div>
<div class="improvement-needed">
<em>Needed:</em> {point.improvementNeeded}
</div>
{#if point.suggestions.length > 0}
<ul class="improvement-suggestions">
{#each point.suggestions as suggestion}
<li>→ {suggestion}</li>
{/each}
</ul>
{/if}
</div>
{/each}
</div>
{/if}
<!-- Recommendations -->
{#if analyzedResult.gapAnalysis.recommendations.length > 0}
<div class="recommendations">
<h4>📋 Recommendations</h4>
<ul>
{#each analyzedResult.gapAnalysis.recommendations as recommendation}
<li>{recommendation}</li>
{/each}
</ul>
</div>
{/if}
</div>
{/if}
<!-- Suggestions Tab -->
{#if selectedTab === 'suggestions'}
<div class="suggestions-section">
<h3>💡 Actionable Suggestions</h3>
<p class="text-sm text-gray-600 mb-4">
Follow these suggestions to improve your RFP response
</p>
<div class="suggestions-list">
{#each analyzedResult.suggestions as suggestion, idx}
<div class="suggestion-item">
<div class="suggestion-number">{idx + 1}</div>
<div class="suggestion-content">{suggestion}</div>
</div>
{/each}
</div>
{#if analyzedResult.suggestions.length === 0}
<div class="no-suggestions">
✅ Your response is comprehensive! No additional suggestions needed.
</div>
{/if}
</div>
{/if}
</div>
{/if}
</div>
<style>
.rfp-analyzer-container {
padding: 1.5rem;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 600;
border: none;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
transition: transform 0.2s;
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-2px);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.spinner {
display: inline-block;
width: 1rem;
height: 1rem;
border: 2px solid rgba(255,255,255,0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.confidence-badge {
padding: 0.75rem 1.5rem;
background: #f0f9ff;
border: 2px solid #0ea5e9;
border-radius: 0.5rem;
color: #0c4a6e;
font-size: 0.95rem;
}
.error-message {
padding: 1rem;
background: #fef2f2;
border-left: 4px solid #ef4444;
color: #991b1b;
border-radius: 0.5rem;
}
.tabs-container {
display: flex;
gap: 0.5rem;
border-bottom: 2px solid #e5e7eb;
margin-bottom: 1.5rem;
}
.tabs-container button {
padding: 0.75rem 1.25rem;
background: none;
border: none;
border-bottom: 3px solid transparent;
cursor: pointer;
font-weight: 500;
color: #6b7280;
transition: all 0.2s;
}
.tabs-container button:hover {
color: #374151;
background: #f9fafb;
}
.tabs-container button.active {
color: #667eea;
border-bottom-color: #667eea;
}
.tab-content {
min-height: 400px;
}
.answer-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.answer-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 1rem;
}
.answer-header h4 {
margin: 0 0 0.5rem 0;
color: #1f2937;
}
.category-badge {
display: inline-block;
padding: 0.25rem 0.75rem;
background: #dbeafe;
color: #1e40af;
border-radius: 1rem;
font-size: 0.75rem;
font-weight: 600;
margin-right: 0.5rem;
}
.relevance-score {
color: #059669;
font-size: 0.875rem;
font-weight: 600;
}
.btn-copy {
padding: 0.5rem 1rem;
background: #f3f4f6;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
cursor: pointer;
font-size: 0.875rem;
transition: all 0.2s;
}
.btn-copy:hover {
background: #e5e7eb;
}
.key-points {
margin-bottom: 1rem;
padding: 1rem;
background: #f9fafb;
border-radius: 0.5rem;
}
.key-points ul {
margin: 0.5rem 0 0 0;
padding-left: 1.5rem;
}
.key-points li {
margin: 0.5rem 0;
color: #374151;
}
.metrics-section {
margin-bottom: 1rem;
padding: 1rem;
background: #fef3c7;
border-radius: 0.5rem;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 0.75rem;
margin-top: 0.75rem;
}
.metric-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.metric-value {
font-weight: 700;
color: #92400e;
}
.metric-context {
font-size: 0.75rem;
color: #78350f;
}
.content-details {
margin-top: 1rem;
}
.content-details summary {
cursor: pointer;
color: #667eea;
font-weight: 600;
padding: 0.5rem;
background: #f5f7ff;
border-radius: 0.375rem;
}
.content-details summary:hover {
background: #eef2ff;
}
.content-body {
margin-top: 0.75rem;
padding: 1rem;
background: white;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
max-height: 400px;
overflow-y: auto;
white-space: pre-wrap;
font-size: 0.875rem;
line-height: 1.6;
}
.answer-stats {
display: flex;
gap: 1.5rem;
padding: 1rem;
background: #f9fafb;
border-radius: 0.75rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
align-items: center;
}
.stat-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.stat-label {
font-size: 0.75rem;
color: #6b7280;
text-transform: uppercase;
font-weight: 600;
}
.stat-value {
font-size: 1.25rem;
font-weight: 700;
color: #1f2937;
}
.addressed-points, .strengths-section {
margin-bottom: 1.5rem;
padding: 1rem;
background: #ecfdf5;
border-radius: 0.5rem;
}
.points-grid {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.75rem;
}
.point-badge {
padding: 0.5rem 1rem;
background: #059669;
color: white;
border-radius: 1rem;
font-size: 0.875rem;
font-weight: 600;
}
.strengths-section ul {
margin: 0.75rem 0 0 0;
padding-left: 1.5rem;
}
.strengths-section li {
margin: 0.5rem 0;
color: #065f46;
}
.generated-answer {
margin-bottom: 1.5rem;
}
.generated-answer h4 {
margin-bottom: 1rem;
color: #1f2937;
}
.answer-content {
padding: 1.5rem;
background: white;
border: 2px solid #e5e7eb;
border-radius: 0.75rem;
line-height: 1.8;
max-height: 600px;
overflow-y: auto;
}
.coverage-summary {
margin-bottom: 2rem;
}
.coverage-summary h3 {
margin-bottom: 1rem;
}
.progress-bar {
height: 2rem;
background: #e5e7eb;
border-radius: 1rem;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #10b981 0%, #059669 100%);
transition: width 0.3s ease;
}
.covered-points, .missing-points, .weak-points, .recommendations {
margin-bottom: 2rem;
padding: 1.5rem;
border-radius: 0.75rem;
}
.covered-points {
background: #f0fdf4;
border: 2px solid #86efac;
}
.missing-points {
background: #fef2f2;
border: 2px solid #fca5a5;
}
.weak-points {
background: #fefce8;
border: 2px solid #fde047;
}
.recommendations {
background: #eff6ff;
border: 2px solid #93c5fd;
}
.covered-point-item, .missing-point-item, .weak-point-item {
padding: 1rem;
background: white;
border-radius: 0.5rem;
margin-bottom: 1rem;
}
.point-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.importance-badge {
padding: 0.25rem 0.75rem;
border-radius: 1rem;
font-size: 0.75rem;
font-weight: 700;
}
.point-source, .point-evidence {
font-size: 0.875rem;
margin-top: 0.5rem;
}
.point-source {
color: #6b7280;
}
.point-evidence {
font-style: italic;
color: #374151;
padding-left: 1rem;
border-left: 3px solid #d1d5db;
}
.suggestions-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.suggestion-item {
display: flex;
gap: 1rem;
padding: 1rem;
background: white;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
align-items: start;
}
.suggestion-number {
flex-shrink: 0;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
background: #667eea;
color: white;
border-radius: 50%;
font-weight: 700;
}
.suggestion-content {
flex: 1;
line-height: 1.6;
color: #374151;
}
.no-suggestions {
padding: 3rem;
text-align: center;
font-size: 1.25rem;
color: #059669;
font-weight: 600;
}
</style>