Spaces:
Sleeping
Sleeping
| <script lang="ts"> | |
| import { | |
| currentStep, | |
| nextStep, | |
| prevStep, | |
| referenceDocuments, | |
| loadReferenceDocuments, | |
| uploadReferenceDocument, | |
| deleteReferenceDocument, | |
| getDocumentContent, | |
| isLoading, | |
| error | |
| } from '../stores/appStore' | |
| import { onMount, onDestroy } from 'svelte' | |
| let hasStoredRfps = false | |
| let selectedCategory = 'it-staffing' | |
| let showCategoryDropdown = false | |
| let isUploading = false | |
| const categories = [ | |
| { value: 'it-staffing', label: 'IT Staffing' }, | |
| { value: 'ai-rfps', label: 'AI RFPs' } | |
| ] | |
| onMount(async () => { | |
| // Load reference documents from backend on component mount | |
| try { | |
| await loadReferenceDocuments() | |
| hasStoredRfps = $referenceDocuments.length > 0 | |
| console.log(`β Loaded ${$referenceDocuments.length} documents from backend on page load`) | |
| } catch (err) { | |
| console.warn('Failed to load reference documents from backend:', err) | |
| // Continue without backend - app should still work | |
| hasStoredRfps = false | |
| } | |
| }) | |
| // Auto-refresh documents every 30 seconds to keep in sync | |
| let refreshInterval: number; | |
| onMount(() => { | |
| refreshInterval = setInterval(async () => { | |
| try { | |
| await loadReferenceDocuments() | |
| } catch (err) { | |
| console.warn('Background refresh failed:', err) | |
| } | |
| }, 30000) // 30 seconds | |
| }) | |
| onDestroy(() => { | |
| if (refreshInterval) clearInterval(refreshInterval) | |
| }) | |
| // Subscribe to reference documents changes | |
| $: hasStoredRfps = $referenceDocuments.length > 0 | |
| async function handleReferenceUpload(event: Event) { | |
| const target = event.target as HTMLInputElement | |
| if (target.files) { | |
| const files = Array.from(target.files) | |
| isUploading = true | |
| try { | |
| // Upload each file to backend | |
| for (const file of files) { | |
| await uploadReferenceDocument(file, selectedCategory) | |
| } | |
| // Immediately refresh the list to show new documents | |
| await loadReferenceDocuments() | |
| // Reset file input | |
| target.value = '' | |
| console.log(`β Successfully uploaded ${files.length} file(s) to backend storage`) | |
| } catch (err) { | |
| console.error('β Upload failed:', err) | |
| } finally { | |
| isUploading = false | |
| } | |
| } | |
| } | |
| async function handleDeleteDocument(id: string) { | |
| if (confirm('Are you sure you want to delete this document?')) { | |
| try { | |
| await deleteReferenceDocument(id) | |
| console.log('β Document deleted successfully') | |
| } catch (err) { | |
| console.error('β Delete failed:', err) | |
| } | |
| } | |
| } | |
| async function viewDocumentContent(id: string) { | |
| try { | |
| const docWithContent = await getDocumentContent(id) | |
| console.log('π Document Content:', docWithContent) | |
| alert(`Document: ${docWithContent.name}\n\nContent Preview:\n${docWithContent.content.substring(0, 500)}${docWithContent.content.length > 500 ? '...' : ''}`) | |
| } catch (err) { | |
| console.error('β Failed to load content:', err) | |
| } | |
| } | |
| function clearAllReference() { | |
| if (confirm('Clear all reference documents from backend storage?')) { | |
| // Delete all documents one by one | |
| $referenceDocuments.forEach(async (doc) => { | |
| try { | |
| await deleteReferenceDocument(doc.id) | |
| } catch (err) { | |
| console.error('Failed to delete document:', err) | |
| } | |
| }) | |
| } | |
| } | |
| function getDocumentsByCategory(category: string) { | |
| return $referenceDocuments.filter(doc => doc.category === category) | |
| } | |
| function formatDate(dateString: string): string { | |
| if (!dateString) return 'Unknown date'; | |
| try { | |
| const date = new Date(dateString); | |
| if (isNaN(date.getTime())) return 'Unknown date'; | |
| return date.toLocaleDateString(); | |
| } catch { | |
| return 'Unknown date'; | |
| } | |
| } | |
| const steps = [ | |
| { | |
| id: 1, | |
| title: 'Upload Current RFP', | |
| description: 'Upload the RFP document you are responding to' | |
| }, | |
| { | |
| id: 2, | |
| title: 'Add Questions', | |
| description: 'Add questions manually or upload document' | |
| }, | |
| { | |
| id: 3, | |
| title: 'AI Processing', | |
| description: 'AI creates responses with citations' | |
| }, | |
| { | |
| id: 4, | |
| title: 'Review & Edit', | |
| description: 'Review and edit AI-generated answers' | |
| }, | |
| { | |
| id: 5, | |
| title: 'Export Document', | |
| description: 'Export professional Word document' | |
| } | |
| ] | |
| function goToStep(stepId: number) { | |
| if (stepId <= $currentStep + 1) { | |
| currentStep.set(stepId) | |
| } | |
| } | |
| </script> | |
| <div class="w-80 bg-gray-900 border-r border-gray-700 flex flex-col"> | |
| <!-- Header --> | |
| <div class="p-6 border-b border-gray-700"> | |
| <h1 class="text-xl font-semibold text-white mb-1">RFP Management</h1> | |
| <p class="text-sm text-gray-400">AI-Powered Response Generation</p> | |
| </div> | |
| <!-- Steps Navigation --> | |
| <div class="flex-1 p-6"> | |
| <h3 class="text-sm font-medium text-gray-300 mb-4">Workflow Steps</h3> | |
| <div class="space-y-2"> | |
| {#each steps as step} | |
| <button | |
| on:click={() => goToStep(step.id)} | |
| class="w-full text-left p-4 rounded-lg border transition-colors { | |
| $currentStep === step.id | |
| ? 'bg-blue-500/20 border-blue-500/40 text-blue-300' | |
| : $currentStep > step.id | |
| ? 'bg-green-500/20 border-green-500/40 text-green-300 hover:bg-green-500/30' | |
| : 'bg-gray-800 border-gray-700 text-gray-400 hover:bg-gray-700' | |
| }" | |
| disabled={step.id > $currentStep + 1} | |
| > | |
| <div class="flex items-start justify-between"> | |
| <div class="flex-1 min-w-0"> | |
| <div class="flex items-center space-x-3 mb-1"> | |
| <div class="flex items-center justify-center w-6 h-6 rounded-full text-xs font-medium { | |
| $currentStep === step.id | |
| ? 'bg-blue-500 text-white' | |
| : $currentStep > step.id | |
| ? 'bg-green-500 text-white' | |
| : 'bg-gray-600 text-gray-400' | |
| }"> | |
| {#if $currentStep > step.id} | |
| β | |
| {:else} | |
| {step.id} | |
| {/if} | |
| </div> | |
| <h4 class="font-medium"> | |
| {step.title} | |
| </h4> | |
| </div> | |
| <p class="text-sm { | |
| $currentStep === step.id | |
| ? 'text-blue-200' | |
| : $currentStep > step.id | |
| ? 'text-green-200' | |
| : 'text-gray-500' | |
| }"> | |
| {step.description} | |
| </p> | |
| </div> | |
| </div> | |
| </button> | |
| {/each} | |
| </div> | |
| </div> | |
| <!-- Progress --> | |
| <div class="p-6 border-t border-gray-700"> | |
| <div class="flex justify-between text-sm mb-2"> | |
| <span class="text-gray-400">Progress</span> | |
| <span class="font-medium text-gray-300">{Math.round(($currentStep / steps.length) * 100)}%</span> | |
| </div> | |
| <div class="w-full bg-gray-700 rounded-full h-2"> | |
| <div | |
| class="bg-gradient-to-r from-blue-500 to-blue-600 h-2 rounded-full transition-all duration-300" | |
| style="width: {($currentStep / steps.length) * 100}%" | |
| ></div> | |
| </div> | |
| <p class="text-xs text-gray-500 mt-2"> | |
| Complete each step to build your professional RFP response. | |
| </p> | |
| </div> | |
| <!-- Reference Documents Section --> | |
| <div class="p-6 border-t border-gray-700 bg-gray-900/50"> | |
| <div class="mb-4"> | |
| <div class="flex items-center justify-between mb-3"> | |
| <h3 class="text-sm font-medium text-gray-300">Reference Documents</h3> | |
| <div class="flex items-center space-x-2"> | |
| {#if hasStoredRfps} | |
| <span class="text-xs text-blue-400">{$referenceDocuments.length} stored</span> | |
| {/if} | |
| <button | |
| on:click={loadReferenceDocuments} | |
| class="text-xs text-gray-400 hover:text-blue-400 px-2 py-1 rounded border border-gray-600 hover:border-blue-500 transition-colors" | |
| disabled={$isLoading} | |
| title="Refresh documents" | |
| > | |
| {#if $isLoading} | |
| β³ | |
| {:else} | |
| π | |
| {/if} | |
| </button> | |
| </div> | |
| </div> | |
| <p class="text-xs text-gray-500 mb-3"> | |
| Upload previous RFPs for reference (stored in backend) | |
| </p> | |
| </div> | |
| <!-- Backend Status --> | |
| {#if $error} | |
| <div class="bg-red-500/20 border border-red-500/40 rounded px-2 py-1 mb-3"> | |
| <p class="text-xs text-red-300">β οΈ {$error}</p> | |
| </div> | |
| {/if} | |
| <!-- Category Selection --> | |
| <div class="mb-3"> | |
| <label class="text-xs text-gray-400 mb-1 block">Category:</label> | |
| <div class="relative"> | |
| <select | |
| bind:value={selectedCategory} | |
| class="w-full bg-gray-800 border border-gray-600 rounded px-2 py-1 text-xs text-gray-300 focus:border-blue-500 focus:outline-none" | |
| disabled={isUploading || $isLoading} | |
| > | |
| {#each categories as category} | |
| <option value={category.value}>{category.label}</option> | |
| {/each} | |
| </select> | |
| </div> | |
| </div> | |
| <!-- Upload Area --> | |
| <div class="relative"> | |
| <input | |
| type="file" | |
| multiple | |
| accept=".pdf,.docx,.doc,.txt" | |
| on:change={handleReferenceUpload} | |
| disabled={isUploading || $isLoading} | |
| class="absolute inset-0 opacity-0 cursor-pointer w-full h-full" | |
| /> | |
| <div class="border-2 border-dashed border-gray-600 rounded-lg p-3 text-center hover:border-blue-500/50 transition-colors {isUploading || $isLoading ? 'opacity-50' : ''}"> | |
| <div class="text-gray-400 text-xs"> | |
| {#if isUploading || $isLoading} | |
| π Uploading to backend... | |
| {:else} | |
| π Upload to {categories.find(c => c.value === selectedCategory)?.label} | |
| {/if} | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Reference Documents List by Category --> | |
| {#if $referenceDocuments.length > 0} | |
| <div class="space-y-3 max-h-48 overflow-y-auto"> | |
| {#each categories as category} | |
| {@const categoryDocs = getDocumentsByCategory(category.value)} | |
| {#if categoryDocs.length > 0} | |
| <div class="bg-gray-800/50 rounded-lg p-2"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h4 class="text-xs font-medium text-gray-300">{category.label}</h4> | |
| <span class="text-xs text-gray-500">{categoryDocs.length}</span> | |
| </div> | |
| <div class="space-y-1"> | |
| {#each categoryDocs as doc} | |
| <div class="bg-gray-800 rounded p-2 text-xs"> | |
| <div class="flex items-center justify-between"> | |
| <div class="flex-1 min-w-0 cursor-pointer" on:click={() => viewDocumentContent(doc.id)}> | |
| <div class="text-gray-300 font-medium truncate hover:text-blue-300">{doc.name}</div> | |
| <div class="flex items-center space-x-2 text-gray-500"> | |
| <span>{(doc.size / 1024 / 1024).toFixed(1)} MB</span> | |
| <span>β’</span> | |
| {#if doc.contentLength} | |
| <span>{doc.contentLength} chars</span> | |
| <span>β’</span> | |
| {/if} | |
| <span class="text-xs px-1 py-0.5 bg-blue-600/20 text-blue-300 rounded"> | |
| {doc.fileType === '.pdf' ? 'PDF' : | |
| doc.fileType === '.docx' || doc.fileType === '.doc' ? 'DOC' : | |
| doc.fileType === '.txt' ? 'TXT' : 'FILE'} | |
| </span> | |
| <span>β’</span> | |
| {#if doc.processed} | |
| <span class="text-green-400">β Processed</span> | |
| {:else} | |
| <span class="text-yellow-400">β³ Processing</span> | |
| {/if} | |
| </div> | |
| <div class="text-xs text-gray-600 mt-1"> | |
| Uploaded: {formatDate(doc.uploadDate)} | |
| {#if doc.contentLength} | |
| <span class="text-green-400">β’ Content Available ({doc.contentLength} chars)</span> | |
| {/if} | |
| </div> | |
| </div> | |
| <button | |
| on:click={() => handleDeleteDocument(doc.id)} | |
| class="ml-2 w-4 h-4 bg-red-500/20 hover:bg-red-500/30 rounded text-red-400 flex items-center justify-center text-xs" | |
| title="Delete from backend" | |
| disabled={$isLoading} | |
| > | |
| Γ | |
| </button> | |
| </div> | |
| </div> | |
| {/each} | |
| </div> | |
| </div> | |
| {/if} | |
| {/each} | |
| <button | |
| on:click={clearAllReference} | |
| class="w-full text-xs text-gray-500 hover:text-gray-400 py-1" | |
| disabled={$isLoading} | |
| > | |
| Clear all reference documents | |
| </button> | |
| <!-- Backend Info --> | |
| {#if $referenceDocuments.length > 0} | |
| <div class="mt-2 p-2 bg-gray-900/50 rounded text-xs text-gray-500"> | |
| π Backend Storage: {$referenceDocuments.length} documents | | |
| Categories: {categories.map(c => `${c.label} (${getDocumentsByCategory(c.value).length})`).filter(s => !s.includes('(0)')).join(', ')} | |
| </div> | |
| {:else} | |
| <div class="mt-2 p-2 bg-gray-900/50 rounded text-xs text-gray-500"> | |
| π‘ Upload reference documents to improve AI responses | |
| </div> | |
| {/if} | |
| </div> | |
| {/if} | |
| </div> | |
| </div> | |