/ src / components / claude / SuggestionPanel.svelte
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>