Spaces:
Sleeping
Sleeping
| <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> | |