Spaces:
Sleeping
Sleeping
| <script lang="ts"> | |
| import { questions, nextStep, currentStep } from '../stores/appStore' | |
| import { parseDocument } from '../utils/documentParser' | |
| import { onMount, onDestroy } from 'svelte' | |
| let dragActive = false | |
| let questionText = '' | |
| let isProcessing = false | |
| let processingStatus = '' | |
| let uploadedFiles: File[] = [] | |
| let currentMethod: 'paste' | 'upload' = 'upload' | |
| let extractedContent = '' | |
| let previousStep = $currentStep | |
| // Auto-clear functionality | |
| onMount(() => { | |
| // Clear questions when component mounts (page refresh) | |
| if ($questions.length > 0) { | |
| console.log('Clearing questions on component mount (page refresh)') | |
| clearAllQuestions() | |
| } | |
| }) | |
| // Watch for step changes | |
| $: if ($currentStep !== previousStep && previousStep !== undefined) { | |
| if ($currentStep !== 2) { // If we're leaving step 2 (Add Questions) | |
| console.log('Clearing questions due to step change') | |
| clearAllQuestions() | |
| } | |
| previousStep = $currentStep | |
| } | |
| async function handleFileUpload(event: Event) { | |
| const target = event.target as HTMLInputElement | |
| if (target.files) { | |
| await processFiles(Array.from(target.files)) | |
| } | |
| } | |
| async function processFiles(files: File[]) { | |
| isProcessing = true | |
| processingStatus = 'Processing RFP document...' | |
| uploadedFiles = [...uploadedFiles, ...files] | |
| try { | |
| for (const file of files) { | |
| processingStatus = `Extracting content from ${file.name}...` | |
| const content = await parseDocument(file) | |
| extractedContent = content | |
| processingStatus = 'Identifying questions in document...' | |
| // Enhanced question extraction | |
| const extractedQuestions = extractQuestions(content) | |
| questions.update(current => [...current, ...extractedQuestions]) | |
| processingStatus = `Found ${extractedQuestions.length} questions in RFP document` | |
| } | |
| } catch (error) { | |
| console.error('Error processing files:', error) | |
| processingStatus = 'Error processing document. Please check format and try again.' | |
| } finally { | |
| setTimeout(() => { | |
| isProcessing = false | |
| processingStatus = '' | |
| }, 2000) | |
| } | |
| } | |
| function extractQuestions(content: string): any[] { | |
| const questions = [] | |
| console.log('Original content:', content) | |
| // SIMPLE APPROACH: Just split by "number. " pattern | |
| // Remove any leading/trailing whitespace and normalize | |
| const cleanContent = content.trim() | |
| // Split using a simple approach - find all "number. " patterns | |
| const parts = cleanContent.split(/(?=\d+\.\s)/) | |
| console.log('Split parts:', parts) | |
| for (let i = 0; i < parts.length; i++) { | |
| const part = parts[i].trim() | |
| if (!part) continue | |
| // Extract the question number and text | |
| const match = part.match(/^(\d+)\.\s*(.+)$/) | |
| if (match) { | |
| const questionNumber = match[1] | |
| let questionText = match[2].trim() | |
| // Remove any trailing numbers that might be part of the next question | |
| questionText = questionText.replace(/\s+\d+\.\s*.*$/, '') | |
| console.log(`Extracted Q${questionNumber}: "${questionText}"`) | |
| if (questionText.length > 3) { | |
| questions.push({ | |
| id: `q-split-${Date.now()}-${i}`, | |
| text: questionText, | |
| selected: true, | |
| priority: 'high' as const, | |
| source: 'split' | |
| }) | |
| } | |
| } | |
| } | |
| // FALLBACK: If the above didn't work, try manual character-by-character parsing | |
| if (questions.length <= 1 && cleanContent.includes('1.') && cleanContent.includes('2.')) { | |
| console.log('Using fallback manual parsing...') | |
| questions.length = 0 // Clear any previous attempts | |
| const questionParts = [] | |
| let currentQuestion = '' | |
| let i = 0 | |
| while (i < cleanContent.length) { | |
| const char = cleanContent[i] | |
| const nextFew = cleanContent.substring(i, i + 10) | |
| // Check if we're at the start of a new question (digit followed by period and space) | |
| const questionStart = nextFew.match(/^(\d+)\.\s/) | |
| if (questionStart && currentQuestion.trim()) { | |
| // Save the previous question | |
| questionParts.push(currentQuestion.trim()) | |
| currentQuestion = '' | |
| } | |
| currentQuestion += char | |
| i++ | |
| } | |
| // Don't forget the last question | |
| if (currentQuestion.trim()) { | |
| questionParts.push(currentQuestion.trim()) | |
| } | |
| console.log('Manual parsing result:', questionParts) | |
| // Process each part | |
| questionParts.forEach((part, index) => { | |
| const match = part.match(/^(\d+)\.\s*(.+)$/) | |
| if (match) { | |
| const questionText = match[2].trim() | |
| console.log(`Manual Q${match[1]}: "${questionText}"`) | |
| questions.push({ | |
| id: `q-manual-${Date.now()}-${index}`, | |
| text: questionText, | |
| selected: true, | |
| priority: 'high' as const, | |
| source: 'manual' | |
| }) | |
| } | |
| }) | |
| } | |
| console.log(`Final result: ${questions.length} questions extracted`) | |
| return questions | |
| } | |
| function handleDrop(event: DragEvent) { | |
| event.preventDefault() | |
| dragActive = false | |
| if (event.dataTransfer?.files) { | |
| processFiles(Array.from(event.dataTransfer.files)) | |
| } | |
| } | |
| function handleDragOver(event: DragEvent) { | |
| event.preventDefault() | |
| dragActive = true | |
| } | |
| function handleDragLeave() { | |
| dragActive = false | |
| } | |
| function addManualQuestion() { | |
| if (questionText.trim()) { | |
| // Use the extraction function to split multiple questions | |
| const extractedQuestions = extractQuestions(questionText.trim()) | |
| if (extractedQuestions.length > 0) { | |
| // Add all extracted questions | |
| questions.update(current => [...current, ...extractedQuestions]) | |
| } else { | |
| // Fallback: add as single question if extraction fails | |
| questions.update(current => [...current, { | |
| id: `manual-${Date.now()}`, | |
| text: questionText.trim(), | |
| selected: true, | |
| priority: 'high' as const, | |
| source: 'manual' | |
| }]) | |
| } | |
| questionText = '' | |
| } | |
| } | |
| function removeFile(index: number) { | |
| uploadedFiles = uploadedFiles.filter((_, i) => i !== index) | |
| } | |
| function removeQuestion(id: string) { | |
| questions.update(current => current.filter(q => q.id !== id)) | |
| } | |
| function clearAllQuestions() { | |
| questions.set([]) | |
| uploadedFiles = [] | |
| extractedContent = '' | |
| questionText = '' | |
| } | |
| function proceedToAnswers() { | |
| if ($questions.length > 0) { | |
| // Skip selection step and go directly to AI Processing (step 3) | |
| nextStep() // This will go to step 3 (AI Processing) | |
| } | |
| } | |
| </script> | |
| <div class="max-w-4xl mx-auto space-y-6"> | |
| <!-- Method Selection --> | |
| <div class="bg-gray-800 border border-gray-600 rounded-lg p-6"> | |
| <h2 class="text-xl font-semibold text-white mb-4">Add RFP Questions</h2> | |
| <div class="flex gap-4 mb-6"> | |
| <button | |
| class="px-4 py-2 rounded border {currentMethod === 'upload' ? 'bg-blue-500/20 border-blue-500 text-blue-300' : 'bg-gray-700 border-gray-600 text-gray-300'}" | |
| on:click={() => currentMethod = 'upload'} | |
| > | |
| Upload Document | |
| </button> | |
| <button | |
| class="px-4 py-2 rounded border {currentMethod === 'paste' ? 'bg-blue-500/20 border-blue-500 text-blue-300' : 'bg-gray-700 border-gray-600 text-gray-300'}" | |
| on:click={() => currentMethod = 'paste'} | |
| > | |
| Manual Entry | |
| </button> | |
| </div> | |
| {#if currentMethod === 'upload'} | |
| <!-- File Upload Section --> | |
| <div class="space-y-4"> | |
| <div | |
| class="border-2 border-dashed border-gray-600 rounded-lg p-8 text-center transition-colors bg-gray-800 | |
| {dragActive ? 'border-blue-500 bg-blue-500/10' : 'hover:border-blue-500/50'} | |
| {isProcessing ? 'opacity-60' : ''}" | |
| on:drop={handleDrop} | |
| on:dragover={handleDragOver} | |
| on:dragleave={handleDragLeave} | |
| > | |
| {#if isProcessing} | |
| <div class="space-y-3"> | |
| <div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div> | |
| <p class="text-blue-400 font-medium">{processingStatus}</p> | |
| {#if processingStatus.includes('Found')} | |
| <div class="text-green-400 text-sm">✓ Processing complete</div> | |
| {/if} | |
| </div> | |
| {:else} | |
| <div class="space-y-3"> | |
| <svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48"> | |
| <path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> | |
| </svg> | |
| <div> | |
| <p class="text-gray-200 font-medium">Drop RFP documents here</p> | |
| <p class="text-gray-400 text-sm">or</p> | |
| <label class="inline-block px-4 py-2 bg-blue-600 text-white rounded cursor-pointer hover:bg-blue-700"> | |
| Browse Files | |
| <input type="file" class="hidden" accept=".pdf,.docx,.doc" multiple on:change={handleFileUpload}> | |
| </label> | |
| </div> | |
| <p class="text-xs text-gray-500">Supports PDF, DOCX, DOC files</p> | |
| </div> | |
| {/if} | |
| </div> | |
| <!-- Uploaded Files List --> | |
| {#if uploadedFiles.length > 0} | |
| <div class="bg-gray-700/50 rounded-lg p-4"> | |
| <h3 class="font-medium text-gray-200 mb-3">Uploaded Documents</h3> | |
| <div class="space-y-2"> | |
| {#each uploadedFiles as file, index} | |
| <div class="flex items-center justify-between bg-gray-800 p-3 rounded border border-gray-600"> | |
| <div class="flex items-center space-x-3"> | |
| <svg class="h-5 w-5 text-blue-400" fill="currentColor" viewBox="0 0 20 20"> | |
| <path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd" /> | |
| </svg> | |
| <div> | |
| <p class="font-medium text-gray-200">{file.name}</p> | |
| <p class="text-sm text-gray-400">{(file.size / 1024 / 1024).toFixed(2)} MB</p> | |
| </div> | |
| </div> | |
| <button | |
| class="text-red-400 hover:text-red-300" | |
| on:click={() => removeFile(index)} | |
| > | |
| Remove | |
| </button> | |
| </div> | |
| {/each} | |
| </div> | |
| </div> | |
| {/if} | |
| </div> | |
| {:else} | |
| <!-- Manual Entry Section --> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-200 mb-2"> | |
| Enter Questions (one per line or numbered) | |
| </label> | |
| <textarea | |
| bind:value={questionText} | |
| placeholder="Example: | |
| 1. What is your company's experience with similar projects? | |
| 2. How do you ensure data security and compliance? | |
| 3. What is your implementation timeline?" | |
| class="w-full h-32 p-3 border border-gray-600 bg-gray-800 text-gray-100 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none placeholder-gray-500" | |
| /> | |
| </div> | |
| <button | |
| on:click={addManualQuestion} | |
| disabled={!questionText.trim()} | |
| class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed" | |
| > | |
| Add Question | |
| </button> | |
| </div> | |
| {/if} | |
| </div> | |
| <!-- Questions Preview --> | |
| {#if $questions.length > 0} | |
| <div class="bg-gray-800 border border-gray-600 rounded-lg p-6"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <div> | |
| <h3 class="text-lg font-semibold text-white">Extracted Questions</h3> | |
| <p class="text-sm text-gray-400">{$questions.length} questions found</p> | |
| </div> | |
| <div class="flex items-center space-x-2"> | |
| <button | |
| on:click={clearAllQuestions} | |
| class="px-3 py-1 bg-red-600 text-white text-sm rounded hover:bg-red-700 transition-colors" | |
| title="Clear all questions" | |
| > | |
| Clear All | |
| </button> | |
| <button | |
| on:click={proceedToAnswers} | |
| class="px-6 py-2 bg-green-600 text-white rounded hover:bg-green-700" | |
| > | |
| Generate AI Answers | |
| </button> | |
| </div> | |
| </div> | |
| <div class="space-y-3 max-h-96 overflow-y-auto"> | |
| {#each $questions as question, index} | |
| <div class="border border-gray-600 bg-gray-700/50 rounded-lg p-4"> | |
| <div class="flex items-start justify-between"> | |
| <div class="flex-1"> | |
| <div class="flex items-center space-x-2 mb-2"> | |
| <span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-500/20 text-blue-300"> | |
| Q{index + 1} | |
| </span> | |
| {#if question.source} | |
| <span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-600 text-gray-300"> | |
| {question.source} | |
| </span> | |
| {/if} | |
| <span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium | |
| {question.priority === 'high' ? 'bg-red-500/20 text-red-300' : | |
| question.priority === 'medium' ? 'bg-blue-500/20 text-blue-300' : | |
| 'bg-green-500/20 text-green-300'}"> | |
| {question.priority} | |
| </span> | |
| </div> | |
| <p class="text-gray-200">{question.text}</p> | |
| </div> | |
| <button | |
| on:click={() => removeQuestion(question.id)} | |
| class="ml-4 text-red-400 hover:text-red-300" | |
| title="Remove question" | |
| > | |
| × | |
| </button> | |
| </div> | |
| </div> | |
| {/each} | |
| </div> | |
| </div> | |
| {/if} | |
| </div> | |