Spaces:
Sleeping
Sleeping
| <script lang="ts"> | |
| import { onMount } from 'svelte'; | |
| import { backendService, type CurrentRFPDocument } from '../utils/backendService'; | |
| import { suggestionEngine } from '../utils/suggestionEngine'; | |
| export let visible = false; | |
| let currentRFPs: CurrentRFPDocument[] = []; | |
| let selectedRFP: CurrentRFPDocument | null = null; | |
| let rfpContent = ''; | |
| let insights: any = null; | |
| let uploading = false; | |
| let loading = false; | |
| // File upload | |
| let fileInput: HTMLInputElement; | |
| onMount(() => { | |
| if (visible) { | |
| loadCurrentRFPs(); | |
| } | |
| }); | |
| $: if (visible) { | |
| loadCurrentRFPs(); | |
| } | |
| async function loadCurrentRFPs() { | |
| try { | |
| loading = true; | |
| currentRFPs = await backendService.getCurrentRFPs(); | |
| } catch (error) { | |
| console.error('Failed to load current RFPs:', error); | |
| } finally { | |
| loading = false; | |
| } | |
| } | |
| async function handleFileUpload(event: Event) { | |
| const target = event.target as HTMLInputElement; | |
| const file = target.files?.[0]; | |
| if (!file) return; | |
| try { | |
| uploading = true; | |
| const result = await backendService.uploadCurrentRFP(file); | |
| // Add to current RFPs list | |
| currentRFPs = [...currentRFPs, result.document]; | |
| // Auto-select the uploaded document | |
| selectedRFP = result.document; | |
| await loadRFPContent(result.document.id); | |
| // Reset file input | |
| target.value = ''; | |
| } catch (error) { | |
| console.error('Upload failed:', error); | |
| alert('Failed to upload file: ' + (error as Error).message); | |
| } finally { | |
| uploading = false; | |
| } | |
| } | |
| async function selectRFP(rfp: CurrentRFPDocument) { | |
| selectedRFP = rfp; | |
| await loadRFPContent(rfp.id); | |
| } | |
| async function loadRFPContent(id: string) { | |
| try { | |
| loading = true; | |
| const result = await backendService.getCurrentRFPContent(id); | |
| rfpContent = result.content; | |
| // Analyze the RFP document for insights | |
| insights = suggestionEngine.analyzeRFPDocument(rfpContent); | |
| } catch (error) { | |
| console.error('Failed to load RFP content:', error); | |
| } finally { | |
| loading = false; | |
| } | |
| } | |
| function formatDate(dateString: string) { | |
| return new Date(dateString).toLocaleDateString(); | |
| } | |
| function getFileIcon(fileType: string) { | |
| switch (fileType.toLowerCase()) { | |
| case 'pdf': return '📄'; | |
| case 'docx': case 'doc': return '📝'; | |
| case 'txt': return '📋'; | |
| default: return '📁'; | |
| } | |
| } | |
| </script> | |
| <div class="rfp-manager" class:visible> | |
| <div class="header"> | |
| <h3>Current RFP Documents</h3> | |
| <button class="close-btn" on:click={() => visible = false}>×</button> | |
| </div> | |
| <div class="content"> | |
| <!-- Upload Section --> | |
| <div class="upload-section"> | |
| <h4>Upload Current RFP</h4> | |
| <div class="upload-area"> | |
| <input | |
| bind:this={fileInput} | |
| type="file" | |
| accept=".pdf,.doc,.docx,.txt" | |
| on:change={handleFileUpload} | |
| style="display: none;" | |
| /> | |
| <button | |
| class="upload-btn" | |
| on:click={() => fileInput.click()} | |
| disabled={uploading} | |
| > | |
| {uploading ? 'Uploading...' : 'Choose RFP Document'} | |
| </button> | |
| <p class="upload-help">Upload the current RFP document to analyze and get suggestions</p> | |
| </div> | |
| </div> | |
| <!-- Documents List --> | |
| <div class="documents-section"> | |
| <h4>Uploaded RFPs ({currentRFPs.length})</h4> | |
| {#if loading && currentRFPs.length === 0} | |
| <div class="loading">Loading documents...</div> | |
| {:else if currentRFPs.length === 0} | |
| <div class="empty-state"> | |
| No RFP documents uploaded yet. Upload one to get started! | |
| </div> | |
| {:else} | |
| <div class="documents-list"> | |
| {#each currentRFPs as rfp (rfp.id)} | |
| <div | |
| class="document-item" | |
| class:selected={selectedRFP?.id === rfp.id} | |
| on:click={() => selectRFP(rfp)} | |
| > | |
| <div class="doc-icon">{getFileIcon(rfp.fileType)}</div> | |
| <div class="doc-info"> | |
| <div class="doc-name">{rfp.name}</div> | |
| <div class="doc-meta"> | |
| {rfp.fileType.toUpperCase()} • {formatDate(rfp.uploadDate)} | |
| {#if rfp.contentLength} | |
| • {Math.round(rfp.contentLength / 1000)}k chars | |
| {/if} | |
| </div> | |
| </div> | |
| <div class="doc-status"> | |
| {#if rfp.processed} | |
| <span class="status-processed">✓ Processed</span> | |
| {:else} | |
| <span class="status-pending">⏳ Processing</span> | |
| {/if} | |
| </div> | |
| </div> | |
| {/each} | |
| </div> | |
| {/if} | |
| </div> | |
| <!-- RFP Analysis --> | |
| {#if selectedRFP && insights} | |
| <div class="analysis-section"> | |
| <h4>RFP Analysis: {selectedRFP.name}</h4> | |
| <div class="insights-grid"> | |
| <!-- Key Requirements --> | |
| <div class="insight-card"> | |
| <h5>Key Requirements</h5> | |
| <div class="tag-list"> | |
| {#each insights.keyRequirements.slice(0, 8) as requirement} | |
| <span class="tag">{requirement}</span> | |
| {/each} | |
| </div> | |
| </div> | |
| <!-- Technology Mentions --> | |
| {#if insights.technologyMentions.length > 0} | |
| <div class="insight-card"> | |
| <h5>Technology Stack</h5> | |
| <div class="tag-list"> | |
| {#each insights.technologyMentions as tech} | |
| <span class="tag tech-tag">{tech}</span> | |
| {/each} | |
| </div> | |
| </div> | |
| {/if} | |
| <!-- Compliance Requirements --> | |
| {#if insights.complianceRequirements.length > 0} | |
| <div class="insight-card"> | |
| <h5>Compliance & Security</h5> | |
| <div class="tag-list"> | |
| {#each insights.complianceRequirements as compliance} | |
| <span class="tag compliance-tag">{compliance}</span> | |
| {/each} | |
| </div> | |
| </div> | |
| {/if} | |
| <!-- Timeline Indicators --> | |
| {#if insights.timelineIndicators.length > 0} | |
| <div class="insight-card"> | |
| <h5>Timeline Indicators</h5> | |
| <div class="tag-list"> | |
| {#each insights.timelineIndicators as timeline} | |
| <span class="tag timeline-tag">{timeline}</span> | |
| {/each} | |
| </div> | |
| </div> | |
| {/if} | |
| <!-- Budget Indicators --> | |
| {#if insights.budgetIndicators.length > 0} | |
| <div class="insight-card"> | |
| <h5>Budget Indicators</h5> | |
| <div class="tag-list"> | |
| {#each insights.budgetIndicators as budget} | |
| <span class="tag budget-tag">{budget}</span> | |
| {/each} | |
| </div> | |
| </div> | |
| {/if} | |
| </div> | |
| <!-- Strategy Recommendations --> | |
| <div class="strategy-section"> | |
| <h5>Strategic Recommendations</h5> | |
| <div class="recommendations"> | |
| <div class="recommendation"> | |
| <strong>Emphasize SEDNA's Experience:</strong> | |
| Highlight 15+ years and 200+ successful projects | |
| </div> | |
| <div class="recommendation"> | |
| <strong>Security Focus:</strong> | |
| Lead with SOC 2, ISO 27001 certifications | |
| </div> | |
| <div class="recommendation"> | |
| <strong>Methodology:</strong> | |
| Detail 5-phase approach: Discovery → Design → Implementation → Testing → Deployment | |
| </div> | |
| <div class="recommendation"> | |
| <strong>Transparency:</strong> | |
| Emphasize fixed-price model with milestone billing | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/if} | |
| </div> | |
| </div> | |
| <style> | |
| .rfp-manager { | |
| position: fixed; | |
| top: 0; | |
| right: -600px; | |
| width: 600px; | |
| height: 100vh; | |
| background: #1a1a1a; | |
| border-left: 1px solid #333; | |
| transition: right 0.3s ease; | |
| z-index: 1000; | |
| overflow-y: auto; | |
| } | |
| .rfp-manager.visible { | |
| right: 0; | |
| } | |
| .header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 1rem; | |
| border-bottom: 1px solid #333; | |
| background: #2a2a2a; | |
| } | |
| .header h3 { | |
| margin: 0; | |
| color: #e0e0e0; | |
| font-size: 1.2rem; | |
| } | |
| .close-btn { | |
| background: none; | |
| border: none; | |
| color: #888; | |
| font-size: 1.5rem; | |
| cursor: pointer; | |
| padding: 0.25rem; | |
| } | |
| .close-btn:hover { | |
| color: #e0e0e0; | |
| } | |
| .content { | |
| padding: 1rem; | |
| } | |
| .upload-section { | |
| margin-bottom: 2rem; | |
| } | |
| .upload-section h4, .documents-section h4, .analysis-section h4 { | |
| color: #4a9eff; | |
| margin: 0 0 1rem 0; | |
| font-size: 1rem; | |
| } | |
| .upload-area { | |
| text-align: center; | |
| padding: 1rem; | |
| border: 2px dashed #333; | |
| border-radius: 8px; | |
| } | |
| .upload-btn { | |
| background: #4a9eff; | |
| color: white; | |
| border: none; | |
| padding: 0.75rem 1.5rem; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| font-size: 0.9rem; | |
| margin-bottom: 0.5rem; | |
| } | |
| .upload-btn:hover:not(:disabled) { | |
| background: #357abd; | |
| } | |
| .upload-btn:disabled { | |
| background: #666; | |
| cursor: not-allowed; | |
| } | |
| .upload-help { | |
| color: #888; | |
| font-size: 0.8rem; | |
| margin: 0; | |
| } | |
| .documents-list { | |
| max-height: 300px; | |
| overflow-y: auto; | |
| } | |
| .document-item { | |
| display: flex; | |
| align-items: center; | |
| padding: 0.75rem; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| transition: background-color 0.2s; | |
| margin-bottom: 0.5rem; | |
| } | |
| .document-item:hover { | |
| background: #2a2a2a; | |
| } | |
| .document-item.selected { | |
| background: #1e3a5f; | |
| border: 1px solid #4a9eff; | |
| } | |
| .doc-icon { | |
| font-size: 1.5rem; | |
| margin-right: 0.75rem; | |
| } | |
| .doc-info { | |
| flex: 1; | |
| } | |
| .doc-name { | |
| color: #e0e0e0; | |
| font-weight: 500; | |
| margin-bottom: 0.25rem; | |
| } | |
| .doc-meta { | |
| color: #888; | |
| font-size: 0.8rem; | |
| } | |
| .doc-status { | |
| margin-left: 1rem; | |
| } | |
| .status-processed { | |
| color: #4caf50; | |
| font-size: 0.8rem; | |
| } | |
| .status-pending { | |
| color: #ff9800; | |
| font-size: 0.8rem; | |
| } | |
| .empty-state, .loading { | |
| text-align: center; | |
| color: #888; | |
| padding: 2rem; | |
| font-style: italic; | |
| } | |
| .analysis-section { | |
| margin-top: 2rem; | |
| padding-top: 2rem; | |
| border-top: 1px solid #333; | |
| } | |
| .insights-grid { | |
| display: grid; | |
| gap: 1rem; | |
| margin-bottom: 2rem; | |
| } | |
| .insight-card { | |
| background: #2a2a2a; | |
| padding: 1rem; | |
| border-radius: 6px; | |
| border: 1px solid #333; | |
| } | |
| .insight-card h5 { | |
| margin: 0 0 0.75rem 0; | |
| color: #4a9eff; | |
| font-size: 0.9rem; | |
| } | |
| .tag-list { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 0.5rem; | |
| } | |
| .tag { | |
| background: #333; | |
| color: #e0e0e0; | |
| padding: 0.25rem 0.5rem; | |
| border-radius: 4px; | |
| font-size: 0.8rem; | |
| } | |
| .tech-tag { | |
| background: #1e3a5f; | |
| color: #4a9eff; | |
| } | |
| .compliance-tag { | |
| background: #1e4a1e; | |
| color: #4caf50; | |
| } | |
| .timeline-tag { | |
| background: #4a1e4a; | |
| color: #e91e63; | |
| } | |
| .budget-tag { | |
| background: #4a4a1e; | |
| color: #ffeb3b; | |
| } | |
| .strategy-section { | |
| background: #2a2a2a; | |
| padding: 1rem; | |
| border-radius: 6px; | |
| border: 1px solid #333; | |
| } | |
| .strategy-section h5 { | |
| margin: 0 0 1rem 0; | |
| color: #4a9eff; | |
| } | |
| .recommendations { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.75rem; | |
| } | |
| .recommendation { | |
| color: #e0e0e0; | |
| font-size: 0.9rem; | |
| line-height: 1.4; | |
| } | |
| .recommendation strong { | |
| color: #4a9eff; | |
| } | |
| </style> | |