SuggestionPanel.svelte
1 <script lang="ts"> 2 import type { WorkspaceSuggestion } from '../../lib/types/claude'; 3 import { Button } from '../common'; 4 5 interface Props { 6 suggestions: WorkspaceSuggestion[]; 7 isLoading: boolean; 8 isConfigured: boolean; 9 onGetSuggestions?: () => void; 10 onApplySuggestion?: (suggestion: WorkspaceSuggestion) => void; 11 onDismissSuggestion?: (suggestion: WorkspaceSuggestion) => void; 12 onConfigure?: () => void; 13 } 14 15 let { 16 suggestions = [], 17 isLoading = false, 18 isConfigured = false, 19 onGetSuggestions, 20 onApplySuggestion, 21 onDismissSuggestion, 22 onConfigure, 23 }: Props = $props(); 24 25 const suggestionTypeIcons: Record<string, string> = { 26 organize: 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z', 27 merge: 'M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2', 28 split: 'M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z', 29 rename: 'M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z', 30 transcendent: 'M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z', 31 }; 32 33 function getTypeLabel(type: string): string { 34 const labels: Record<string, string> = { 35 organize: 'Organize', 36 merge: 'Merge', 37 split: 'Split', 38 rename: 'Rename', 39 transcendent: 'Mark Transcendent', 40 }; 41 return labels[type] || type; 42 } 43 44 function getConfidenceColor(confidence?: number): string { 45 if (confidence === undefined) return 'text-text-muted'; 46 if (confidence >= 0.8) return 'text-status-success'; 47 if (confidence >= 0.5) return 'text-phosphor'; 48 return 'text-status-warning'; 49 } 50 </script> 51 52 <div class="suggestion-panel"> 53 <div class="panel-header"> 54 <div class="header-left"> 55 <svg class="header-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 56 <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" /> 57 </svg> 58 <h3 class="header-title">Claude Suggestions</h3> 59 </div> 60 61 {#if isConfigured} 62 <Button 63 variant="ghost" 64 size="sm" 65 disabled={isLoading} 66 onclick={onGetSuggestions} 67 > 68 {#if isLoading} 69 <div class="spinner"></div> 70 {:else} 71 <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 72 <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" /> 73 </svg> 74 {/if} 75 Get Suggestions 76 </Button> 77 {/if} 78 </div> 79 80 <div class="panel-content"> 81 {#if !isConfigured} 82 <div class="not-configured"> 83 <svg class="config-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 84 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" /> 85 </svg> 86 <p class="config-text">Configure your Claude API key to get intelligent workspace suggestions.</p> 87 <Button variant="primary" size="sm" onclick={onConfigure}> 88 Configure API Key 89 </Button> 90 </div> 91 {:else if isLoading} 92 <div class="loading-state"> 93 <div class="spinner large"></div> 94 <p>Analyzing your workspaces...</p> 95 </div> 96 {:else if suggestions.length === 0} 97 <div class="empty-state"> 98 <svg class="empty-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 99 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> 100 </svg> 101 <p>Your workspaces are well organized!</p> 102 <p class="empty-hint">Click "Get Suggestions" to analyze your workspace structure.</p> 103 </div> 104 {:else} 105 <ul class="suggestion-list"> 106 {#each suggestions as suggestion (suggestion.description)} 107 <li class="suggestion-item"> 108 <div class="suggestion-header"> 109 <div class="suggestion-type"> 110 <svg class="type-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 111 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={suggestionTypeIcons[suggestion.type] || suggestionTypeIcons.organize} /> 112 </svg> 113 <span class="type-label">{getTypeLabel(suggestion.type)}</span> 114 </div> 115 {#if suggestion.confidence !== undefined} 116 <span class="confidence {getConfidenceColor(suggestion.confidence)}"> 117 {Math.round(suggestion.confidence * 100)}% 118 </span> 119 {/if} 120 </div> 121 122 <p class="suggestion-description">{suggestion.description}</p> 123 124 <div class="suggestion-actions"> 125 <Button 126 variant="primary" 127 size="sm" 128 onclick={() => onApplySuggestion?.(suggestion)} 129 > 130 Apply 131 </Button> 132 <Button 133 variant="ghost" 134 size="sm" 135 onclick={() => onDismissSuggestion?.(suggestion)} 136 > 137 Dismiss 138 </Button> 139 </div> 140 </li> 141 {/each} 142 </ul> 143 {/if} 144 </div> 145 </div> 146 147 <style> 148 .suggestion-panel { 149 background: var(--color-surface-raised); 150 border: 1px solid rgba(217, 137, 46, 0.2); 151 border-radius: 8px; 152 overflow: hidden; 153 } 154 155 .panel-header { 156 display: flex; 157 align-items: center; 158 justify-content: space-between; 159 padding: 0.75rem 1rem; 160 border-bottom: 1px solid rgba(217, 137, 46, 0.15); 161 } 162 163 .header-left { 164 display: flex; 165 align-items: center; 166 gap: 0.5rem; 167 } 168 169 .header-icon { 170 width: 1.25rem; 171 height: 1.25rem; 172 color: var(--color-phosphor); 173 } 174 175 .header-title { 176 font-size: 0.875rem; 177 font-weight: 500; 178 color: var(--color-text-primary); 179 } 180 181 .panel-content { 182 padding: 1rem; 183 } 184 185 .not-configured, 186 .loading-state, 187 .empty-state { 188 display: flex; 189 flex-direction: column; 190 align-items: center; 191 gap: 0.75rem; 192 padding: 1.5rem; 193 text-align: center; 194 } 195 196 .config-icon, 197 .empty-icon { 198 width: 2.5rem; 199 height: 2.5rem; 200 color: var(--color-text-muted); 201 } 202 203 .config-text, 204 .empty-state p { 205 font-size: 0.875rem; 206 color: var(--color-text-secondary); 207 max-width: 280px; 208 } 209 210 .empty-hint { 211 font-size: 0.75rem; 212 color: var(--color-text-muted); 213 } 214 215 .spinner { 216 width: 1rem; 217 height: 1rem; 218 border: 2px solid var(--color-bg-light); 219 border-top-color: var(--color-phosphor); 220 border-radius: 50%; 221 animation: spin 0.6s linear infinite; 222 } 223 224 .spinner.large { 225 width: 2rem; 226 height: 2rem; 227 } 228 229 @keyframes spin { 230 to { transform: rotate(360deg); } 231 } 232 233 .suggestion-list { 234 list-style: none; 235 margin: 0; 236 padding: 0; 237 display: flex; 238 flex-direction: column; 239 gap: 0.75rem; 240 } 241 242 .suggestion-item { 243 padding: 0.75rem; 244 background: rgba(13, 61, 61, 0.3); 245 border: 1px solid rgba(217, 137, 46, 0.1); 246 border-radius: 6px; 247 } 248 249 .suggestion-header { 250 display: flex; 251 align-items: center; 252 justify-content: space-between; 253 margin-bottom: 0.5rem; 254 } 255 256 .suggestion-type { 257 display: flex; 258 align-items: center; 259 gap: 0.375rem; 260 } 261 262 .type-icon { 263 width: 1rem; 264 height: 1rem; 265 color: var(--color-phosphor); 266 } 267 268 .type-label { 269 font-size: 0.75rem; 270 font-weight: 500; 271 text-transform: uppercase; 272 letter-spacing: 0.05em; 273 color: var(--color-phosphor); 274 } 275 276 .confidence { 277 font-size: 0.6875rem; 278 font-family: var(--font-mono); 279 } 280 281 .suggestion-description { 282 font-size: 0.8125rem; 283 color: var(--color-text-primary); 284 line-height: 1.4; 285 margin-bottom: 0.75rem; 286 } 287 288 .suggestion-actions { 289 display: flex; 290 gap: 0.5rem; 291 } 292 </style>