fh / src /components /QuestionsList.svelte
Varun10000's picture
Upload 57 files
d8635c9 verified
Raw
History Blame Contribute Delete
77.5 kB
<script lang="ts">
import { onMount } from 'svelte'
import { questions, rfpData, nextStep, prevStep, isLoading, referenceDocuments } from '../stores/appStore'
import { generateAnswer as generateAIAnswer } from '../utils/aiGenerator'
import { backendService } from '../utils/backendService'
import { suggestionEngine } from '../utils/suggestionEngine'
import { vectorStorage } from '../utils/vectorStorage'
import type { RFPAnswer } from '../stores/appStore'
import RFPAnalyzer from './RFPAnalyzer.svelte'
let selectedQuestions: Set<number> = new Set()
let generatedAnswers: RFPAnswer[] = []
let selectedCategory: string = 'all' // Default to all categories
let currentQuestionIndex: number = 0
let showAnalyzer: boolean = false // Toggle for RFP Analyzer view
// Answer type selection
let selectedAnswerType: string = 'ai' // 'ai' for AI answer, 'previous' for previous answer
$: questionsToProcess = $questions
$: allSelected = selectedQuestions.size === questionsToProcess.length
$: currentQuestion = questionsToProcess[currentQuestionIndex]
$: currentAnswerId = `answer-${currentQuestionIndex}`
$: currentAnswer = $rfpData.answers?.find(a => a.id === currentAnswerId)
// Track formatted answers
let formattedAnswers = new Map()
let formattingInProgress = new Set()
// Additional information for AI processing
let additionalInfo = ""
// Auto-generate answers when current question changes
$: if (currentQuestion && !currentAnswer && !$isLoading && $rfpData.extractedData?.length > 0) {
console.log(`🤖 Auto-generating answer for question ${currentQuestionIndex + 1}: "${currentQuestion.text.substring(0, 50)}..."`)
generateAnswer()
}
// Auto-select all questions when questions change
$: if (questionsToProcess.length > 0 && selectedQuestions.size === 0) {
selectedQuestions = new Set(questionsToProcess.map((_, index) => index))
}
// Get available categories from reference documents
$: availableCategories = [...new Set($referenceDocuments.map(doc => doc.category))].filter(Boolean)
function toggleQuestion(index: number) {
if (selectedQuestions.has(index)) {
selectedQuestions.delete(index)
} else {
selectedQuestions.add(index)
}
selectedQuestions = new Set(selectedQuestions)
}
function toggleAllQuestions() {
if (allSelected) {
selectedQuestions.clear()
} else {
selectedQuestions = new Set(questionsToProcess.map((_, index) => index))
}
selectedQuestions = new Set(selectedQuestions)
}
function nextQuestion() {
if (currentQuestionIndex < questionsToProcess.length - 1) {
currentQuestionIndex++
}
}
function prevQuestion() {
if (currentQuestionIndex > 0) {
currentQuestionIndex--
}
}
function goToQuestion(index: number) {
currentQuestionIndex = index
}
// Function to reprocess answer with additional information
async function reprocessAnswerWithAdditionalInfo(answer) {
if (!answer || !additionalInfo.trim()) return
isLoading.set(true)
console.log('🔄 Reprocessing answer with additional information:', additionalInfo.trim())
try {
// Combine original answer with additional information
const enhancedContext = `${answer.aiGeneratedAnswer || answer.finalAnswer}
Additional Requirements/Context:
${additionalInfo.trim()}`
// Format the enhanced answer using the AI API
const formatResult = await backendService.formatAnswer(
answer.question,
enhancedContext,
`Enhanced with user input: ${additionalInfo.trim().substring(0, 100)}...`
)
if (formatResult.success) {
// Update the answer with the enhanced version
rfpData.update(data => {
const answers = data.answers || []
const answerIndex = answers.findIndex(a => a.id === answer.id)
if (answerIndex >= 0) {
answers[answerIndex] = {
...answers[answerIndex],
finalAnswer: formatResult.formattedAnswer,
isFormatted: true,
hasAdditionalInfo: true,
additionalInfo: additionalInfo.trim()
}
}
return { ...data, answers }
})
// Clear the additional info input
additionalInfo = ""
console.log('✅ Answer reprocessed successfully with additional information')
} else {
console.warn('Failed to reprocess answer:', formatResult)
}
} catch (error) {
console.error('Error reprocessing answer with additional info:', error)
} finally {
isLoading.set(false)
}
}
// Function to format a previous answer using AI
async function formatPreviousAnswer(answer, questionText) {
const answerId = `${answer.docName}-${answer.question.substring(0, 50)}`
if (formattingInProgress.has(answerId)) {
console.log('⏳ Already formatting this answer:', answerId)
return // Already formatting this answer
}
if (formattedAnswers.has(answerId)) {
console.log('✅ Answer already formatted:', answerId)
return formattedAnswers.get(answerId) // Already formatted
}
console.log('🚀 Starting format for answer:', answerId)
formattingInProgress.add(answerId)
formattingInProgress = formattingInProgress // Trigger reactivity
try {
console.log('📝 Formatting previous answer:', answer.question.substring(0, 60) + '...')
console.log('📄 Answer text length:', answer.answer?.length || 0)
console.log('🔧 Question text:', questionText || answer.question)
const result = await backendService.formatAnswer(
questionText || answer.question,
answer.answer,
`Document: ${answer.docName}, Page: ${answer.pageNumber}`
)
console.log('📥 Format result received:', result)
if (result.success) {
const formatted = {
...answer,
formattedAnswer: result.formattedAnswer,
originalAnswer: result.originalAnswer
}
formattedAnswers.set(answerId, formatted)
formattedAnswers = formattedAnswers // Trigger reactivity
console.log('✅ Successfully formatted answer for:', answerId)
return formatted
} else {
console.warn('⚠️ Failed to format answer:', result)
return answer
}
} catch (error) {
console.error('❌ Error formatting previous answer:', error)
console.error('❌ Error details:', {
message: error.message,
stack: error.stack,
answerId,
questionText: questionText || answer.question
})
return answer
} finally {
console.log('🏁 Cleaning up format process for:', answerId)
formattingInProgress.delete(answerId)
formattingInProgress = formattingInProgress // Trigger reactivity
}
}
// Function to extract relevant previous answers from reference documents
function getPreviousAnswers(questionText: string) {
console.log('🔍 HYBRID SEARCH: Regex + Vector for question:', questionText)
if (!questionText || questionText.length < 10) {
console.log('❌ Question too short for search')
return []
}
const allMatches = []
// ============================================
// STAGE 1: REGEX PATTERN MATCHING (Fast & Precise)
// ============================================
console.log('📋 STAGE 1: Trying regex pattern matching...')
if (referenceDocsWithContent && referenceDocsWithContent.length > 0) {
for (const doc of referenceDocsWithContent) {
if (selectedCategory !== 'all' && doc.category !== selectedCategory) {
continue
}
if (!doc.content || doc.content.length < 50) continue
// Try to find exact question match using regex
const questionAnswerPairs = extractQuestionAnswerPairs(doc.content, doc.name, questionText)
for (const pair of questionAnswerPairs) {
// Check for exact or very high similarity match
const similarity = calculateSimpleTextSimilarity(questionText.toLowerCase(), pair.question.toLowerCase())
const percentage = Math.round(similarity * 100)
if (percentage >= 85) { // High confidence regex match
console.log(` ✅ REGEX MATCH: ${doc.name} - ${percentage}% - "${pair.question.substring(0, 60)}..."`)
allMatches.push({
docName: doc.name,
question: pair.question,
answer: pair.answer,
content: `Q: ${pair.question}\n\nA: ${pair.answer}`,
matchScore: similarity,
matchPercentage: percentage,
category: doc.category || 'General',
pageNumber: pair.pageNumber || 1,
sectionType: percentage >= 95 ? 'exact-match' : 'high-confidence-match',
source: 'regex-pattern',
isExactMatch: percentage >= 95,
extractionStrategy: pair.extractionStrategy || 'regex'
})
// If exact match (95%+), return immediately - no need for vector search
if (percentage >= 95) {
console.log('🎯 EXACT REGEX MATCH - Returning immediately')
return [allMatches[allMatches.length - 1]]
}
}
}
}
}
// ============================================
// STAGE 2: VECTOR SEARCH (Semantic & Fuzzy)
// ============================================
console.log('🧠 STAGE 2: Trying vector semantic search...')
try {
const searchOptions = {
topK: 10, // Increased from 5 to get more results
categoryFilter: selectedCategory === 'all' ? undefined : selectedCategory,
expandQuery: true
}
const searchResults = vectorStorage.semanticSearch(questionText, searchOptions)
console.log(` ✅ Found ${searchResults.length} vector matches`)
// Add vector results
searchResults.forEach(result => {
const score = Math.round(result.score * 100)
console.log(` 📄 ${result.chunk.metadata.source}: ${score}% match`)
console.log(` 🔑 Keywords: ${result.matchedKeywords.join(', ')}`)
allMatches.push({
docName: result.chunk.metadata.source,
question: 'Relevant Content', // Vector finds content, not Q&A
answer: result.chunk.content,
content: result.chunk.content,
matchScore: result.score,
matchPercentage: score,
category: result.chunk.metadata.category || 'General',
matchedKeywords: result.matchedKeywords,
pageNumber: result.chunk.metadata.pageNumber || 1,
chunkIndex: result.chunk.metadata.chunkIndex,
sectionType: 'vector-match',
source: 'vector-search',
isExactMatch: false
})
})
} catch (error) {
console.error('❌ Vector search failed:', error)
}
// ============================================
// COMBINE & RANK RESULTS
// ============================================
console.log(`📊 Total matches: ${allMatches.length} (regex + vector)`)
// Log all match scores for debugging
if (allMatches.length > 0) {
console.log('📈 All match scores:')
allMatches.forEach(m => {
console.log(` ${m.docName}: ${m.matchPercentage}% (${m.source})`)
})
}
// Filter weak matches - lowered to 10% to catch semantic matches
const strongMatches = allMatches.filter(match => match.matchPercentage >= 10)
if (strongMatches.length === 0) {
console.log('⚠️ No strong matches found (minimum 10% required)')
return []
}
console.log(`✅ ${strongMatches.length} matches above 10% threshold`)
// Prioritize regex matches, then vector matches
const sortedMatches = strongMatches.sort((a, b) => {
// Exact matches first
if (a.isExactMatch && !b.isExactMatch) return -1
if (!a.isExactMatch && b.isExactMatch) return 1
// Regex matches before vector matches
if (a.source === 'regex-pattern' && b.source === 'vector-search') return -1
if (a.source === 'vector-search' && b.source === 'regex-pattern') return 1
// Then by score
return b.matchPercentage - a.matchPercentage
})
const topMatches = sortedMatches.slice(0, 3)
console.log('🏆 Top 3 matches:')
topMatches.forEach(m => {
console.log(` ${m.source === 'regex-pattern' ? '📋' : '🧠'} ${m.docName}: ${m.matchPercentage}% (${m.source})`)
})
return topMatches
}
// Simple text similarity for regex results
function calculateSimpleTextSimilarity(text1: string, text2: string): number {
const words1 = text1.split(/\s+/).filter(w => w.length > 2)
const words2 = text2.split(/\s+/).filter(w => w.length > 2)
const set1 = new Set(words1)
const set2 = new Set(words2)
let matchCount = 0
set1.forEach(word => {
if (set2.has(word)) matchCount++
})
return matchCount / Math.max(set1.size, set2.size)
}
// Function to deduplicate similar answers
function deduplicateAnswers(matches: any[]) {
const uniqueMatches = []
const seenAnswers = new Set()
for (const match of matches) {
// Create a normalized version of the answer for comparison
const normalizedAnswer = match.answer?.toLowerCase()
.replace(/\s+/g, ' ')
.replace(/[^\w\s]/g, '')
.trim() || ''
// Skip if we've seen a very similar answer
let isDuplicate = false
for (const seenAnswer of seenAnswers) {
const similarity = calculateTextSimilarity(normalizedAnswer, seenAnswer)
if (similarity > 0.8) { // 80% similarity threshold
isDuplicate = true
break
}
}
if (!isDuplicate && normalizedAnswer.length > 50) {
uniqueMatches.push(match)
seenAnswers.add(normalizedAnswer)
}
}
return uniqueMatches
}
// Simple text similarity calculation
function calculateTextSimilarity(text1: string, text2: string): number {
const words1 = new Set(text1.split(' ').filter(w => w.length > 3))
const words2 = new Set(text2.split(' ').filter(w => w.length > 3))
const intersection = new Set([...words1].filter(x => words2.has(x)))
const union = new Set([...words1, ...words2])
return union.size > 0 ? intersection.size / union.size : 0
}
// Enhanced question-answer pair extraction
function extractQuestionAnswerPairs(content: string, docName: string, searchQuestion?: string) {
const pairs = []
console.log(`📄 Extracting Q&A pairs from ${docName}...`)
// FIX: Normalize PDF text that has spaces between every character
// Common OCR/PDF extraction issue: "E d g e M a r k e t" -> "EdgeMarket"
// Check if content has this pattern
const sampleText = content.substring(0, 500)
const spacedChars = (sampleText.match(/\b[A-Za-z]\s(?=[A-Za-z]\s)/g) || []).length
const totalChars = sampleText.replace(/\s/g, '').length
// If more than 20% of characters are spaced individually, fix it
if (spacedChars > totalChars * 0.2) {
console.log(` 🔧 Detected spaced character formatting (${Math.round(spacedChars/totalChars*100)}% spaced), fixing...`)
// Remove spaces between individual letters while preserving word boundaries
// "S h e l b y C o u n t y" -> "Shelby County"
content = content.replace(/([A-Za-z])\s+(?=[A-Za-z](?:\s[A-Za-z]|\b))/g, '$1')
// Clean up excessive whitespace
content = content.replace(/\s{3,}/g, ' ').replace(/\s{2}/g, ' ')
console.log(` ✅ Text normalized, sample: "${content.substring(0, 100)}"`)
}
// Strategy 1: Split by numbered questions (1., 2., 3., etc.)
const numberedSections = content.split(/(?=^\s*\d+\.)/m)
for (let i = 0; i < numberedSections.length; i++) {
const section = numberedSections[i].trim()
if (section.length < 20) continue
// Extract question from numbered format
const questionMatch = section.match(/^\s*(\d+)\.\s*(.+?)(?:\n\n|\n[A-Z]|\n\d+\.|\s{3,}|$)/s)
if (questionMatch) {
const questionNumber = questionMatch[1]
const questionText = questionMatch[2].trim()
// Extract answer (everything after the question)
let answerText = section.replace(questionMatch[0], '').trim()
// Clean up answer text
answerText = answerText.replace(/^\n+/, '').replace(/\n+$/, '')
if (questionText.length > 10 && answerText.length > 20) {
pairs.push({
questionNumber: parseInt(questionNumber),
question: questionText,
answer: answerText,
pageNumber: Math.max(1, Math.ceil(i / 300)) + 1, // More realistic page estimate
sectionIndex: i
})
console.log(` ✅ Q${questionNumber}: "${questionText.substring(0, 50)}..." -> ${answerText.length} chars`)
}
}
}
// Strategy 2: Look for Q: A: patterns
const qaMatches = content.match(/Q[:\s]+(.*?)(?:\n|\r\n)A[:\s]+(.*?)(?=Q[:\s]+|$)/gs)
if (qaMatches) {
qaMatches.forEach((match, index) => {
const parts = match.match(/Q[:\s]+(.*?)(?:\n|\r\n)A[:\s]+(.*)/s)
if (parts) {
pairs.push({
questionNumber: pairs.length + 1,
question: parts[1].trim(),
answer: parts[2].trim(),
pageNumber: Math.max(1, Math.ceil(index / 300)) + 1, // More realistic page estimate
sectionIndex: index
})
}
})
}
// Strategy 3: Look for section headers with content (for RFP documents)
// Split content into paragraphs and look for header-like text followed by content
const paragraphs = content.split(/\n\s*\n/).filter(p => p.trim().length > 0)
for (let i = 0; i < paragraphs.length - 1; i++) {
const currentPara = paragraphs[i].trim()
const nextPara = paragraphs[i + 1]?.trim()
// Check if current paragraph looks like a question/header
const isQuestionLike = (
currentPara.length > 10 && currentPara.length < 200 && // Reasonable length for question
(currentPara.includes('?') || // Contains question mark
currentPara.match(/^[A-Z][^.!?]*[A-Za-z]$/) || // Capitalized statement without ending punctuation
currentPara.match(/Company|Background|Experience|Profile|Founded|Describe|Provide|Explain/i)) && // Common RFP terms
!currentPara.match(/^\d+\./) && // Not already a numbered question
nextPara && nextPara.length > 50 // Has substantial content following
)
if (isQuestionLike && nextPara) {
// Collect answer content (current and potentially next few paragraphs)
let answerContent = nextPara
let j = i + 2
while (j < paragraphs.length && j < i + 5) { // Look ahead max 3 more paragraphs
const para = paragraphs[j]?.trim()
if (para && para.length > 20 && !para.match(/^[A-Z][^.!?]*[A-Za-z]$/)) {
answerContent += '\n\n' + para
j++
} else {
break
}
}
if (answerContent.length > 50) {
pairs.push({
questionNumber: pairs.length + 1,
question: currentPara,
answer: answerContent,
pageNumber: Math.max(1, Math.ceil(i / 250)) + 1, // More realistic page estimate
sectionIndex: i,
extractionStrategy: 'section-header'
})
console.log(` ✅ Header Q: "${currentPara.substring(0, 50)}..." -> ${answerContent.length} chars`)
}
}
}
// Strategy 4: Direct text matching - Look for the exact question in content
const currentQuestionText = searchQuestion?.trim()
if (currentQuestionText && currentQuestionText.length > 10) {
const searchText = currentQuestionText.toLowerCase()
let contentLower = content.toLowerCase()
// SKIP PDF FRONT MATTER - Find where real content starts
const executiveSummaryIndex = contentLower.indexOf('executive summar')
const tabCIndex = contentLower.indexOf('tab c:')
const contentStartIndex = Math.max(0, executiveSummaryIndex, tabCIndex)
if (contentStartIndex > 0) {
console.log(`📄 Skipping ${contentStartIndex} chars of front matter (cover page, TOC)`)
content = content.substring(contentStartIndex)
contentLower = content.toLowerCase()
}
const exactMatchIndex = contentLower.indexOf(searchText)
if (exactMatchIndex !== -1) {
// Find the end of the question text in the document
const questionEndIndex = exactMatchIndex + searchText.length
// Extract answer content starting after the question
let answerStartIndex = questionEndIndex
// Skip any immediate whitespace or formatting characters after the question
while (answerStartIndex < content.length &&
(content[answerStartIndex] === ' ' || content[answerStartIndex] === '\n' ||
content[answerStartIndex] === '\r' || content[answerStartIndex] === '\t')) {
answerStartIndex++
}
// Extract ONLY the immediate answer content below the question
let answerEndIndex = answerStartIndex
let sentenceCount = 0
let paragraphCount = 0
let foundContent = false
// Look for a focused answer - stop at logical boundaries
while (answerEndIndex < content.length) {
const currentChar = content[answerEndIndex]
const nextChars = content.substring(answerEndIndex, answerEndIndex + 100).toLowerCase()
const currentSection = content.substring(answerStartIndex, answerEndIndex)
// Count sentences and paragraphs to limit content
if (currentChar === '.' || currentChar === '!' || currentChar === '?') {
sentenceCount++
}
if (currentChar === '\n' && content[answerEndIndex + 1] === '\n') {
paragraphCount++
}
// CONSERVATIVE stopping criteria - extract ONLY direct answer
if (foundContent && (
// Stop at new questions or major sections
nextChars.match(/^\s*\d+\.\s*[A-Z]/) || // New numbered question/section
nextChars.match(/^[A-Z][A-Z\s&]{8,}:/) || // Section headers like "COMPANY DETAILS:"
nextChars.includes('question:') ||
nextChars.includes('response:') ||
nextChars.includes('answer:') ||
nextChars.match(/^\s*q\d+/i) || // Q1, Q2, etc.
nextChars.match(/^\s*section\s+[a-z\d]/i) || // Section markers
// Stop at metadata sections and tables
nextChars.includes('company details') ||
nextChars.includes('year founded') ||
nextChars.includes('total employees') ||
nextChars.includes('contact information') ||
nextChars.includes('business address') ||
nextChars.includes('incorporation') ||
nextChars.match(/entity\s+duration/i) || // Contract tables
nextChars.match(/here is a sample/i) || // Sample sections
nextChars.includes('entity') && nextChars.includes('duration') && nextChars.includes('value') ||
// MUCH MORE CONSERVATIVE - Stop after short focused answer
sentenceCount >= 3 || // Max 3 sentences only
paragraphCount >= 2 || // Max 2 paragraphs only
(answerEndIndex - answerStartIndex > 500) || // Max 500 chars (very focused!)
// Stop at table-like data or lists of specs
nextChars.match(/^\s*\|\s*\w+\s*\|/) || // Table rows
nextChars.match(/^\s*[•·-]\s*\w+:/) || // Spec lists
// Stop at new page indicators or RFP metadata
nextChars.includes('page ') ||
nextChars.includes('continued on') ||
nextChars.includes('tab ') ||
nextChars.match(/rfp #\d/i) ||
nextChars.includes('proposal due date')
)) {
break
}
if (currentChar.trim() !== '' && !currentChar.match(/[\r\n\t]/)) {
foundContent = true
}
answerEndIndex++
}
// Extract and format the answer content
let answerContent = content.substring(answerStartIndex, answerEndIndex)
// Basic cleanup first
answerContent = answerContent
.replace(/\s+/g, ' ') // Normalize whitespace
.replace(/\s([.,!?;:])/g, '$1') // Fix punctuation spacing
.replace(/([.!?])\s*([A-Z])/g, '$1\n\n$2') // Add paragraph breaks after sentences
.replace(/(\d+\.)\s*([A-Z])/g, '\n\n$1 $2') // Format numbered lists
.replace(/([·•])\s*/g, '\n• ') // Format bullet points
.replace(/\n\s*\n\s*\n+/g, '\n\n') // Clean up multiple line breaks
.trim()
// Enhanced AI formatting using Ollama (async - will be processed later)
const processAnswerWithAI = async (rawAnswer: string, question: string) => {
try {
console.log(`Formatting answer with AI for: "${question.substring(0, 50)}..."`);
const formatResult = await backendService.formatAnswer(question, rawAnswer, docName);
if (formatResult.success && formatResult.formattedAnswer !== rawAnswer) {
console.log(`AI formatting improved answer quality`);
// Update the stored answer with formatted version
const pairIndex = pairs.findIndex(p => p.question === question);
if (pairIndex !== -1) {
pairs[pairIndex].answer = formatResult.formattedAnswer;
pairs[pairIndex].aiFormatted = true;
pairs[pairIndex].originalAnswer = rawAnswer;
}
}
} catch (error) {
console.warn('AI formatting failed, using basic cleanup:', error);
}
};
// Estimate page number more accurately (assuming ~3000 chars per page)
const estimatedPage = Math.floor(exactMatchIndex / 3000) + 1
const answerPair = {
questionNumber: pairs.length + 1,
question: currentQuestionText,
answer: answerContent,
pageNumber: estimatedPage,
sectionIndex: pairs.length,
extractionStrategy: 'exact-match',
matchScore: 100,
documentSource: docName,
exactMatch: true,
aiFormatted: false,
originalAnswer: answerContent
};
pairs.push(answerPair);
// Process with AI formatting in the background
processAnswerWithAI(answerContent, currentQuestionText);
console.log(` ✅ EXACT MATCH: "${currentQuestionText.substring(0, 50)}..." -> 100% match found on page ${estimatedPage}`)
console.log(` 📄 Source: ${docName}`)
console.log(` 📝 Answer length: ${answerContent.length} characters`)
console.log(` 📝 Answer preview: "${answerContent.substring(0, 200)}..."`)
console.log(` 🤖 AI formatting queued for processing`)
// Return early if we found an exact match - no need to look for other strategies
return pairs
}
}
console.log(` 📊 Total extracted: ${pairs.length} Q&A pairs`)
return pairs
}
// Advanced similarity calculation
function calculateAdvancedSimilarity(q1: string, q2: string, pair?: any): number {
const clean1 = q1.toLowerCase().replace(/[^\w\s]/g, ' ').replace(/\s+/g, ' ').trim()
const clean2 = q2.toLowerCase().replace(/[^\w\s]/g, ' ').replace(/\s+/g, ' ').trim()
// If this pair was extracted with exact match strategy, give it 100%
if (pair?.extractionStrategy === 'exact-match' && pair?.matchScore === 100) {
return 1.0
}
// Exact match
if (clean1 === clean2) return 1.0
// Check if one contains the other (high similarity for partial matches)
if (clean1.includes(clean2) || clean2.includes(clean1)) {
const longer = clean1.length > clean2.length ? clean1 : clean2
const shorter = clean1.length > clean2.length ? clean2 : clean1
return Math.max(0.85, shorter.length / longer.length)
}
// Word-based similarity
const words1 = clean1.split(' ').filter(w => w.length > 2)
const words2 = clean2.split(' ').filter(w => w.length > 2)
if (words1.length === 0 || words2.length === 0) return 0
const commonWords = words1.filter(w => words2.includes(w))
const wordSimilarity = commonWords.length / Math.max(words1.length, words2.length)
// Substring similarity
const longer = clean1.length > clean2.length ? clean1 : clean2
const shorter = clean1.length > clean2.length ? clean2 : clean1
let substringScore = 0
if (longer.includes(shorter)) substringScore = 0.3
// Key term matching (boost for important business terms)
const keyTerms = ['company', 'background', 'experience', 'profile', 'corporate', 'founded', 'healthcare', 'education', 'staffing', 'management', 'services', 'approach', 'methodology', 'process']
const keyTermMatches = keyTerms.filter(term =>
clean1.includes(term) && clean2.includes(term)
)
const keyTermScore = keyTermMatches.length * 0.05
// Length similarity bonus
const lengthRatio = Math.min(clean1.length, clean2.length) / Math.max(clean1.length, clean2.length)
const lengthScore = lengthRatio * 0.1
const totalSimilarity = wordSimilarity + substringScore + keyTermScore + lengthScore
return Math.min(totalSimilarity, 1.0)
}
// Find matched words between questions
function findMatchedWords(q1: string, q2: string): string[] {
const words1 = q1.toLowerCase().split(/\s+/).filter(w => w.length > 2)
const words2 = q2.toLowerCase().split(/\s+/).filter(w => w.length > 2)
return words1.filter(w => words2.includes(w))
}
// Content-based matching fallback
function findContentMatches(doc: any, questionText: string) {
const matches = []
const questionLower = questionText.toLowerCase()
const keyWords = questionLower.split(/\s+/).filter(w => w.length > 3)
const sections = doc.content.split(/\n\s*\n+/)
for (let i = 0; i < sections.length; i++) {
const section = sections[i].trim()
if (section.length < 50) continue
const sectionLower = section.toLowerCase()
let matchCount = 0
const matchedWords = []
for (const word of keyWords) {
if (sectionLower.includes(word)) {
matchCount++
matchedWords.push(word)
}
}
// STRICTER THRESHOLD: Need at least 40% keyword match OR 5+ keywords
const matchPercentage = (matchCount / keyWords.length) * 100
if (matchCount >= 5 || matchPercentage >= 40) {
const percentage = Math.min(Math.round(matchPercentage), 95)
matches.push({
docName: doc.name,
question: 'Content Match',
answer: section,
content: section.substring(0, 600) + (section.length > 600 ? '...' : ''),
matchScore: matchCount / keyWords.length,
matchPercentage: percentage,
category: doc.category || 'General',
matchedWords: matchedWords,
pageNumber: Math.max(1, Math.ceil(i / 200)) + 1, // More realistic page estimate
sectionType: 'content-match',
source: 'content-fallback',
isExactMatch: false
})
}
}
return matches
}
// Get previous answers for current question
$: {
console.log('🔄 Reactive update triggered for previousAnswers')
console.log('Current question:', currentQuestion?.text)
console.log('Reference docs with content available:', referenceDocsWithContent?.length || 0)
if (referenceDocsWithContent && referenceDocsWithContent.length > 0) {
console.log('Content preview:', referenceDocsWithContent[0].content?.substring(0, 100))
}
}
$: previousAnswers = (currentQuestion && referenceDocsWithContent?.length > 0) ? getPreviousAnswers(currentQuestion.text) : []
// Load reference documents with content on component mount
let referenceDocsWithContent: Array<{
id: string
name: string
category: string
content: string
fileType: string
}> = []
async function loadReferenceDocuments() {
try {
console.log('📚 Loading reference documents with content...')
const docs = await backendService.getPreviousRFPs()
console.log('Found documents:', docs.map(d => d.name))
// Clear existing vector storage
vectorStorage.clear()
console.log('🗑️ Cleared vector storage')
const docsWithContent = []
for (const doc of docs) {
try {
const docWithContent = await backendService.getDocumentContent(doc.id)
console.log(`Loaded content for ${docWithContent.name}: ${docWithContent.content?.length || 0} characters`)
docsWithContent.push(docWithContent)
// Add document to vector storage
if (docWithContent.content && docWithContent.content.length > 100) {
await vectorStorage.addDocument(docWithContent.content, {
source: docWithContent.name,
category: docWithContent.category || 'General'
})
console.log(`✅ Added ${docWithContent.name} to vector storage`)
}
} catch (error) {
console.error(`Failed to load content for ${doc.name}:`, error)
}
}
referenceDocsWithContent = docsWithContent
// Show vector storage stats
const stats = vectorStorage.getStats()
console.log('📊 Vector Storage Stats:', stats)
// Trigger reactive update after documents are loaded
console.log('📡 Triggering reactive update after document loading')
if (currentQuestion) {
previousAnswers = getPreviousAnswers(currentQuestion.text)
}
} catch (error) {
console.error('Error loading reference documents:', error)
}
}
// Load documents on component mount
onMount(() => {
loadReferenceDocuments()
})
async function generateAnswer() {
if (!currentQuestion) return
isLoading.set(true)
const categoryFilter = selectedCategory === 'all' ? undefined : selectedCategory
console.log(`Generating answer for question ${currentQuestionIndex + 1} using ${categoryFilter ? `${categoryFilter} category only` : 'all categories'}`)
try {
// Start with basic answer generation
const aiResponse = await generateAIAnswer(currentQuestion.text, $rfpData.extractedData, undefined, categoryFilter)
const answer: RFPAnswer = {
id: `answer-${currentQuestionIndex}`,
question: currentQuestion.text,
aiGeneratedAnswer: aiResponse.generatedAnswer,
finalAnswer: aiResponse.generatedAnswer,
isFinalized: false,
citations: aiResponse.citations.map(c => `${c.source} (Page ${c.pageNumber})`),
source: aiResponse.citations[0]?.source,
pageNumber: aiResponse.citations[0]?.pageNumber
}
// Update the store with the initial answer
rfpData.update(data => {
const existingAnswers = data.answers || []
const existingIndex = existingAnswers.findIndex(a => a.id === answer.id)
if (existingIndex >= 0) {
existingAnswers[existingIndex] = answer
} else {
existingAnswers.push(answer)
}
return { ...data, answers: existingAnswers }
})
// Now format the answer in real-time using AI
console.log('Formatting generated answer in real-time...')
try {
const formatResult = await backendService.formatAnswer(
currentQuestion.text,
aiResponse.generatedAnswer,
`Reference from: ${aiResponse.citations.map(c => `${c.source} (Page ${c.pageNumber})`).join(', ')}`
)
if (formatResult.success) {
// Update the answer with formatted version
rfpData.update(data => {
const answers = data.answers || []
const answerIndex = answers.findIndex(a => a.id === answer.id)
if (answerIndex >= 0) {
answers[answerIndex] = {
...answers[answerIndex],
finalAnswer: formatResult.formattedAnswer,
aiGeneratedAnswer: formatResult.originalAnswer,
isFormatted: true
}
}
return { ...data, answers }
})
console.log('✅ Answer formatted successfully in real-time')
} else {
console.warn('Failed to format answer:', formatResult)
}
} catch (formatError) {
console.error('Error formatting answer:', formatError)
// Continue with unformatted answer
}
// Move to next question or complete
if (currentQuestionIndex < questionsToProcess.length - 1) {
nextQuestion()
} else {
// All questions processed, go to next step
nextStep()
}
} catch (error) {
console.error('Error generating answer:', error)
} finally {
isLoading.set(false)
}
}
// Function to use a previous answer directly
async function usePreviousAnswer(previousAnswer: any) {
if (!currentQuestion || !previousAnswer) return
isLoading.set(true)
try {
const answer: RFPAnswer = {
id: `answer-${currentQuestionIndex}`,
question: currentQuestion.text,
aiGeneratedAnswer: previousAnswer.answer,
finalAnswer: previousAnswer.answer,
isFinalized: false,
citations: [`${previousAnswer.docName} (Page ${previousAnswer.pageNumber})`],
source: previousAnswer.docName,
pageNumber: previousAnswer.pageNumber
}
// Update the store with the previous answer
rfpData.update(data => {
const existingAnswers = data.answers || []
const existingIndex = existingAnswers.findIndex(a => a.id === answer.id)
if (existingIndex >= 0) {
existingAnswers[existingIndex] = answer
} else {
existingAnswers.push(answer)
}
return { ...data, answers: existingAnswers }
})
console.log('✅ Previous answer used successfully')
// Move to next question or complete
if (currentQuestionIndex < questionsToProcess.length - 1) {
nextQuestion()
} else {
// All questions processed, go to next step
nextStep()
}
} catch (error) {
console.error('Error using previous answer:', error)
} finally {
isLoading.set(false)
}
}
</script>
<div class="max-w-6xl mx-auto">
<div class="bg-gray-800 rounded-lg shadow-lg p-8">
<!-- View Toggle Buttons -->
<div class="mb-6 flex gap-3">
<button
on:click={() => showAnalyzer = false}
class="px-4 py-2 rounded-lg font-medium transition-colors {!showAnalyzer ? 'bg-blue-600 text-white' : 'bg-gray-700 text-gray-300 hover:bg-gray-600'}"
>
📝 Standard Answer Generation
</button>
<button
on:click={() => showAnalyzer = true}
class="px-4 py-2 rounded-lg font-medium transition-colors {showAnalyzer ? 'bg-purple-600 text-white' : 'bg-gray-700 text-gray-300 hover:bg-gray-600'}"
>
🔍 Advanced RFP Analysis
</button>
</div>
{#if showAnalyzer}
<!-- RFP Analyzer View -->
<div class="bg-gray-700 rounded-lg p-6 border border-gray-600">
<h3 class="text-xl font-bold text-white mb-4">🔍 Advanced RFP Question Analysis</h3>
<p class="text-gray-300 mb-6">
Get comprehensive analysis with structured previous answers, AI-generated responses considering stress points, gap analysis, and actionable suggestions.
</p>
<RFPAnalyzer
question={currentQuestion?.text || ''}
currentRFPContext={$rfpData.extractedData?.map(d => d.text).join('\n') || ''}
/>
</div>
{:else}
<!-- Standard View (existing content) -->
<!-- Configuration -->
<div class="mb-8">
<!-- Reference Category Selection -->
{#if availableCategories.length > 0}
<div class="mb-6 p-4 bg-gray-700 rounded-lg border border-gray-600">
<h4 class="text-sm font-semibold text-white mb-3">
Reference Document Category
</h4>
<div class="flex flex-wrap gap-2">
<label class="flex items-center">
<input
type="radio"
name="category"
value="all"
bind:group={selectedCategory}
class="mr-2 text-blue-600"
/>
<span class="text-sm text-gray-200 px-2 py-1 bg-gray-800 rounded border border-gray-600">
All Categories ({$referenceDocuments.length} docs)
</span>
</label>
{#each availableCategories as category}
{@const categoryDocs = $referenceDocuments.filter(doc => doc.category === category)}
<label class="flex items-center">
<input
type="radio"
name="category"
value={category}
bind:group={selectedCategory}
class="mr-2 text-blue-600"
/>
<span class="text-sm text-gray-200 px-2 py-1 bg-gray-800 rounded border border-gray-600">
{category === 'it-staffing' ? 'IT Staffing' : category === 'ai-rfps' ? 'AI RFPs' : category} ({categoryDocs.length} docs)
</span>
</label>
{/each}
</div>
<p class="text-xs text-gray-300 mt-2">
AI will only reference documents from the selected category when generating answers.
</p>
</div>
{/if}
<!-- Current Question Display -->
<div class="mb-6">
<div class="flex items-center justify-between mb-4">
<h4 class="text-lg font-semibold text-white">
Question {currentQuestionIndex + 1} of {questionsToProcess.length}
</h4>
<div class="flex items-center space-x-2">
<button
on:click={prevQuestion}
disabled={currentQuestionIndex === 0}
class="px-3 py-1 bg-gray-600 text-white rounded text-sm hover:bg-gray-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
← Previous
</button>
<button
on:click={nextQuestion}
disabled={currentQuestionIndex >= questionsToProcess.length - 1}
class="px-3 py-1 bg-gray-600 text-white rounded text-sm hover:bg-gray-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next →
</button>
</div>
</div>
{#if currentQuestion}
<div class="p-4 border border-gray-600 rounded-lg bg-gray-700">
<div class="text-gray-200 mb-4">
{currentQuestion.text}
</div>
<!-- Answer Type Selection -->
<div class="mb-6 p-4 bg-gray-800 rounded-lg border border-gray-600">
<h4 class="text-sm font-semibold text-white mb-3">
Choose Answer Source
</h4>
<div class="flex gap-4">
<label class="flex items-center">
<input
type="radio"
name="answerType"
value="ai"
bind:group={selectedAnswerType}
class="mr-2 text-blue-600"
/>
<span class="text-sm text-gray-200 px-3 py-2 bg-gray-700 rounded border border-gray-600">
Generate AI Answer
</span>
</label>
<label class="flex items-center">
<input
type="radio"
name="answerType"
value="previous"
bind:group={selectedAnswerType}
class="mr-2 text-blue-600"
/>
<span class="text-sm text-gray-200 px-3 py-2 bg-gray-700 rounded border border-gray-600">
Use Previous Answer
</span>
</label>
</div>
</div>
<!-- Three-part answer display -->
<div class="space-y-4">
<!-- 1. Previous RFP Answers -->
<div class="bg-gray-800 rounded-lg p-4 border border-gray-600">
<h5 class="text-sm font-semibold text-gray-300 mb-3 flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Previous RFP Answers
</h5>
<div class="text-gray-400 text-sm">
{#if previousAnswers.length > 0}
<div class="space-y-3">
{#each previousAnswers as answer, index}
<div class="bg-gray-700 p-4 rounded border border-gray-600
{answer.isExactMatch ? 'border-green-500 bg-green-900/10' : ''}
{answer.matchPercentage >= 80 ? 'border-blue-500 bg-blue-900/10' : ''}
{answer.matchPercentage >= 60 ? 'border-yellow-500 bg-yellow-900/10' : ''}">
<!-- Header with match percentage -->
<div class="flex items-center justify-between mb-3">
<div class="flex items-center space-x-2">
<div class="font-medium text-gray-200 text-sm">
📄 {answer.documentSource || answer.docName}
</div>
<div class="flex items-center space-x-1">
<div class="text-lg font-bold
{answer.matchPercentage >= 95 ? 'text-green-400' : ''}
{answer.matchPercentage >= 80 && answer.matchPercentage < 95 ? 'text-blue-400' : ''}
{answer.matchPercentage >= 60 && answer.matchPercentage < 80 ? 'text-yellow-400' : ''}
{answer.matchPercentage < 60 ? 'text-orange-400' : ''}">
{answer.matchPercentage}%
</div>
<div class="text-xs text-gray-400">match</div>
</div>
</div>
<div class="flex items-center space-x-2">
<span class="text-xs
{answer.isExactMatch ? 'text-green-400 bg-green-900/20' : 'text-blue-400 bg-blue-900/20'}
px-2 py-1 rounded">
{answer.category || 'General'}
</span>
<span class="text-xs text-green-400 px-2 py-1 bg-green-900/20 rounded font-bold">
📍 Page {answer.pageNumber || 'N/A'}
</span>
{#if answer.source === 'regex-pattern'}
<span class="text-xs text-orange-400 px-2 py-1 bg-orange-900/20 rounded font-bold">
📋 Regex Match
</span>
{:else if answer.source === 'vector-search'}
<span class="text-xs text-purple-400 px-2 py-1 bg-purple-900/20 rounded font-bold">
🧠 Vector Match
</span>
{/if}
{#if answer.isExactMatch}
<span class="text-xs text-green-400 px-2 py-1 bg-green-900/20 rounded font-bold">
✅ EXACT MATCH
</span>
{/if}
{#if answer.extractionStrategy}
<span class="text-xs text-purple-400 px-2 py-1 bg-purple-900/20 rounded">
{answer.extractionStrategy}
</span>
{/if}
</div>
</div>
<!-- Question and Answer Content -->
{#if answer.question && answer.answer}
<div class="space-y-2">
<!-- Show matched keywords for vector search -->
{#if answer.source === 'vector-search' && answer.matchedKeywords && answer.matchedKeywords.length > 0}
<div class="flex items-center gap-2 flex-wrap">
<span class="text-xs text-gray-400">Matched keywords:</span>
{#each answer.matchedKeywords as keyword}
<span class="text-xs px-2 py-0.5 bg-purple-900/30 text-purple-300 rounded">
{keyword}
</span>
{/each}
</div>
{/if}
{#if answer.question !== 'Relevant Content'}
<div class="text-xs text-gray-400 font-medium">Original Question:</div>
<div class="text-gray-300 text-sm italic bg-gray-800 p-2 rounded border-l-4 border-blue-500">
"{answer.question}"
</div>
{/if}
<!-- Format Answer Button -->
<div class="flex items-center justify-between">
<div class="text-xs text-gray-400 font-medium mt-3">Complete Answer:</div>
<div class="flex gap-2">
{#if selectedAnswerType === 'previous'}
<button
class="text-xs px-3 py-1 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors duration-200 flex items-center space-x-1"
on:click={() => usePreviousAnswer(answer)}
disabled={$isLoading}
>
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
<span>Use This Answer</span>
</button>
{/if}
<button
class="text-xs px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors duration-200 flex items-center space-x-1"
on:click={() => formatPreviousAnswer(answer, currentQuestion?.text)}
disabled={formattingInProgress.has(`${answer.docName}-${answer.question.substring(0, 50)}`)}
>
{#if formattingInProgress.has(`${answer.docName}-${answer.question.substring(0, 50)}`)}
<svg class="w-3 h-3 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span>Formatting...</span>
{:else}
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
<span>Format with AI</span>
{/if}
</button>
</div>
</div>
<!-- Show formatted answer if available -->
{#if formattedAnswers.has(`${answer.docName}-${answer.question.substring(0, 50)}`)}
{@const formattedAnswer = formattedAnswers.get(`${answer.docName}-${answer.question.substring(0, 50)}`)}
<!-- Formatted Answer -->
<div class="text-gray-200 text-sm leading-relaxed bg-gradient-to-r from-green-900/20 to-blue-900/20 p-4 rounded border-l-4 border-green-400">
<div class="flex items-center mb-2">
<svg class="w-4 h-4 text-green-400 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
<span class="text-xs font-semibold text-green-400">AI-Formatted Answer</span>
</div>
<div class="prose prose-sm max-w-none text-gray-200">
{#each (formattedAnswer.formattedAnswer || formattedAnswer.answer).split('\n\n') as paragraph}
{#if paragraph.trim()}
<p class="mb-3 last:mb-0">{@html paragraph.trim().replace(/\n/g, '<br>')}</p>
{/if}
{/each}
</div>
</div>
<!-- Original Answer (collapsed by default) -->
<details class="text-gray-200 text-sm leading-relaxed">
<summary class="text-xs text-gray-400 cursor-pointer hover:text-gray-300 mb-2">View Original Answer</summary>
<div class="bg-gray-800 p-4 rounded border-l-4 border-gray-500">
<div class="prose prose-sm max-w-none text-gray-300">
{#each answer.answer.split('\n\n') as paragraph}
{#if paragraph.trim()}
<p class="mb-3 last:mb-0">{@html paragraph.trim().replace(/\n/g, '<br>')}</p>
{/if}
{/each}
</div>
</div>
</details>
{:else}
<!-- Original Answer (when not formatted) -->
<div class="text-gray-200 text-sm leading-relaxed bg-gray-800 p-4 rounded border-l-4 border-green-500">
<div class="prose prose-sm max-w-none text-gray-200">
{#each answer.answer.split('\n\n') as paragraph}
{#if paragraph.trim()}
<p class="mb-3 last:mb-0">{@html paragraph.trim().replace(/\n/g, '<br>')}</p>
{/if}
{/each}
</div>
</div>
{/if}
</div>
{:else}
<div class="text-gray-200 text-sm leading-relaxed">
{answer.content}
</div>
{/if}
<!-- Match Details -->
<div class="mt-3 pt-3 border-t border-gray-600 flex items-center justify-between">
<div class="text-xs text-gray-400">
<span class="font-medium">Matched words:</span>
{#if answer.matchedWords && answer.matchedWords.length > 0}
<span class="text-blue-300">{answer.matchedWords.slice(0, 5).join(', ')}</span>
{#if answer.matchedWords.length > 5}
<span class="text-gray-500"> +{answer.matchedWords.length - 5} more</span>
{/if}
{:else}
<span class="text-gray-500">Content-based match</span>
{/if}
</div>
<div class="text-xs text-gray-500">
Type: {answer.sectionType || 'content'} | Strategy: {answer.extractionStrategy || answer.source || 'extraction'}
{#if answer.documentSource}
| Source: {answer.documentSource}
{/if}
</div>
</div>
</div>
{/each}
<!-- Summary -->
<div class="mt-4 p-3 bg-gray-800 rounded border border-gray-600">
<div class="text-xs text-gray-400">
<strong>Search Summary:</strong> Found {previousAnswers.length} relevant matches
{#if previousAnswers.some(a => a.isExactMatch)}
<span class="text-green-400 font-bold"> • Contains exact question match!</span>
{/if}
{#if previousAnswers.filter(a => a.matchPercentage >= 80).length > 0}
<span class="text-blue-400"> • {previousAnswers.filter(a => a.matchPercentage >= 80).length} high-confidence matches</span>
{/if}
</div>
</div>
</div>
{:else}
<div class="text-gray-500 italic">
{#if referenceDocsWithContent.length === 0}
<div class="space-y-2">
<div>No reference documents loaded yet...</div>
<div class="text-xs">Documents available: {$referenceDocuments.length}</div>
{#if $referenceDocuments.length > 0}
<div class="text-xs">
{#each $referenceDocuments as doc}
<div>• {doc.name} ({doc.fileType || 'unknown type'})</div>
{/each}
</div>
{/if}
</div>
{:else}
<div class="space-y-2">
<div>No relevant previous answers found for this question</div>
<div class="text-xs">Searched {referenceDocsWithContent.length} documents with content:</div>
<div class="text-xs">
{#each referenceDocsWithContent as doc}
<div>• {doc.name}: {doc.content?.length || 0} characters</div>
{/each}
</div>
<div class="text-xs text-blue-400 mt-2">Check browser console for detailed search logs</div>
</div>
{/if}
</div>
{/if}
</div>
</div>
<!-- 2. AI Generated Answer -->
<div class="bg-gray-800 rounded-lg p-4 border border-gray-600">
<h5 class="text-sm font-semibold text-gray-300 mb-3 flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
AI Generated Answer
{#if currentAnswer && currentAnswer.isFormatted}
<span class="ml-2 text-xs bg-green-600 text-white px-2 py-1 rounded-full">AI-Formatted</span>
{/if}
{#if currentAnswer && currentAnswer.hasAdditionalInfo}
<span class="ml-2 text-xs bg-purple-600 text-white px-2 py-1 rounded-full">Enhanced</span>
{/if}
</h5>
<div class="text-gray-400 text-sm">
{#if currentAnswer && (currentAnswer.finalAnswer || currentAnswer.aiGeneratedAnswer)}
<div class="bg-gray-700 p-3 rounded border border-gray-600">
{#if currentAnswer.isFormatted && currentAnswer.finalAnswer}
<!-- Show formatted answer with enhanced styling -->
<div class="bg-gradient-to-r from-green-900/20 to-blue-900/20 p-4 rounded border-l-4 border-green-400 mb-3">
<div class="flex items-center mb-2">
<svg class="w-4 h-4 text-green-400 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
<span class="text-xs font-semibold text-green-400">AI-Enhanced Answer</span>
</div>
<div class="text-gray-200 leading-relaxed whitespace-pre-wrap">{currentAnswer.finalAnswer}</div>
</div>
<!-- Show original answer in collapsible section -->
<details class="mt-3">
<summary class="text-xs text-gray-400 cursor-pointer hover:text-gray-300 mb-2">View Original Generated Answer</summary>
<div class="bg-gray-800 p-3 rounded border border-gray-500">
<div class="text-gray-300 leading-relaxed whitespace-pre-wrap">{currentAnswer.aiGeneratedAnswer}</div>
</div>
</details>
{:else}
<!-- Show regular generated answer -->
<div class="text-gray-200 leading-relaxed whitespace-pre-wrap">{currentAnswer.finalAnswer || currentAnswer.aiGeneratedAnswer}</div>
{/if}
{#if currentAnswer.citations && currentAnswer.citations.length > 0}
<div class="mt-3 pt-3 border-t border-gray-600">
<div class="text-xs text-blue-400 font-medium mb-1">Citations:</div>
<div class="space-y-1">
{#each currentAnswer.citations as citation}
<div class="text-xs text-blue-300">• {citation}</div>
{/each}
</div>
</div>
{/if}
<!-- Additional Information Input -->
<div class="mt-4 pt-3 border-t border-gray-600">
<div class="text-xs text-gray-400 font-medium mb-2">Add Additional Information for AI Processing:</div>
<div class="space-y-2">
<textarea
bind:value={additionalInfo}
placeholder="Enter any additional context, requirements, or specific details you want the AI to include when processing this answer..."
class="w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded-md text-gray-200 text-sm placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
rows="3"
></textarea>
<button
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-md transition-colors duration-200 flex items-center space-x-2"
on:click={() => reprocessAnswerWithAdditionalInfo(currentAnswer)}
disabled={$isLoading || !additionalInfo.trim()}
>
{#if $isLoading}
<svg class="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span>Processing...</span>
{:else}
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span>Reprocess with Additional Info</span>
{/if}
</button>
</div>
</div>
</div>
{:else if $isLoading}
<div class="bg-gray-700 p-3 rounded border border-gray-600">
<div class="flex items-center space-x-2">
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-500"></div>
<div class="text-gray-300">Generating and formatting AI answer in real-time...</div>
</div>
</div>
{:else}
<div class="bg-gray-700 p-3 rounded border border-gray-600">
<div class="text-gray-400">AI-generated answer will appear here after clicking "Generate Answer"...</div>
</div>
{/if}
</div>
</div>
<!-- 3. Suggestions -->
<div class="bg-gray-800 rounded-lg p-4 border border-gray-600">
<h5 class="text-sm font-semibold text-gray-300 mb-3 flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
Suggestions
</h5>
<div class="text-gray-400 text-sm">
<div class="space-y-2">
<div class="bg-gray-700 p-3 rounded border border-gray-600">
<div class="text-gray-300 text-xs">• Include specific metrics and KPIs</div>
</div>
<div class="bg-gray-700 p-3 rounded border border-gray-600">
<div class="text-gray-300 text-xs">• Add relevant case studies or examples</div>
</div>
<div class="bg-gray-700 p-3 rounded border border-gray-600">
<div class="text-gray-300 text-xs">• Mention compliance and security standards</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/if}
<!-- Question Navigation -->
<div class="mt-4">
<div class="flex flex-wrap gap-2">
{#each questionsToProcess as question, index}
<button
on:click={() => goToQuestion(index)}
class="px-3 py-1 text-sm rounded {index === currentQuestionIndex ? 'bg-blue-600 text-white' : 'bg-gray-600 text-gray-300 hover:bg-gray-500'}"
>
Q{index + 1}
</button>
{/each}
</div>
</div>
</div>
</div>
<!-- AI Generation Info -->
<div class="mb-8 p-6 bg-gray-800 rounded-lg border border-gray-600">
<h4 class="text-lg font-semibold text-white mb-3">
AI Answer Generation
</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-gray-200">
<div class="flex items-center">
<svg class="w-4 h-4 mr-2 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Searches previous RFP responses for relevant content
</div>
<div class="flex items-center">
<svg class="w-4 h-4 mr-2 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Provides citations with source and page numbers
</div>
<div class="flex items-center">
<svg class="w-4 h-4 mr-2 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Suggests improvements based on new RFP context
</div>
<div class="flex items-center">
<svg class="w-4 h-4 mr-2 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Allows full editing before finalization
</div>
</div>
</div>
<!-- Loading State -->
{#if $isLoading}
<div class="flex flex-col items-center justify-center p-8 space-y-4">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
<div class="text-center">
<div class="text-lg font-medium text-white">Generating AI Answers</div>
<div class="text-gray-300">This may take a few moments...</div>
</div>
<div class="w-full max-w-md bg-gray-700 rounded-full h-2">
<div class="bg-blue-600 h-2 rounded-full animate-pulse" style="width: 60%"></div>
</div>
</div>
{/if}
<!-- Navigation Buttons -->
<div class="flex justify-between">
<button
class="px-6 py-3 bg-gray-700 text-gray-300 rounded-lg font-medium hover:bg-gray-600 transition-colors"
on:click={prevStep}
disabled={$isLoading}
>
<svg class="w-5 h-5 mr-2 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
Back to Upload
</button>
<button
class="px-6 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
disabled={!currentQuestion || $isLoading || (selectedAnswerType === 'previous' && previousAnswers.length === 0)}
on:click={generateAnswer}
>
{#if selectedAnswerType === 'ai'}
Generate AI Answer for Q{currentQuestionIndex + 1}
{#if selectedCategory !== 'all'}
<span class="text-xs">({selectedCategory === 'it-staffing' ? 'IT Staffing' : selectedCategory === 'ai-rfps' ? 'AI RFPs' : selectedCategory})</span>
{/if}
<svg class="w-5 h-5 ml-2 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
{:else}
{#if previousAnswers.length > 0}
Select Previous Answer Above ↑
<svg class="w-5 h-5 ml-2 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
{:else}
No Previous Answers Available
<svg class="w-5 h-5 ml-2 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 15.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
{/if}
{/if}
</button>
</div>
{/if}
<!-- End of Standard View -->
</div>
</div>