|
47 | 47 |
|
48 | 48 | export let message: ChatMessage; |
49 | 49 | export let onRetry: ((content: string) => void) | undefined = undefined; |
| 50 | + export let onClose: ((messageId: string) => void) | undefined = undefined; |
| 51 | + |
| 52 | + let isCollapsed = false; |
50 | 53 |
|
51 | 54 | // For code blocks, we'll estimate token count (rough approximation) |
52 | 55 | function estimateTokens(text: string): number { |
|
308 | 311 | function formatTimestamp(timestamp: number): string { |
309 | 312 | return new Date(timestamp).toLocaleTimeString(); |
310 | 313 | } |
| 314 | +
|
| 315 | + function toggleCollapse() { |
| 316 | + isCollapsed = !isCollapsed; |
| 317 | + } |
| 318 | +
|
| 319 | + function closeMessage() { |
| 320 | + if (onClose) { |
| 321 | + onClose(message.id); |
| 322 | + } |
| 323 | + } |
| 324 | +
|
| 325 | + function hasOriginalFileData(documentData: any): boolean { |
| 326 | + return documentData && documentData.originalFileData && documentData.originalFileData.length > 0; |
| 327 | + } |
| 328 | +
|
| 329 | + function openDocumentPopup(documentData: any): void { |
| 330 | + try { |
| 331 | + // Check if we have original file data |
| 332 | + if (!documentData.originalFileData) { |
| 333 | + console.warn('No original file data available for document:', documentData.fileName); |
| 334 | + return; |
| 335 | + } |
| 336 | +
|
| 337 | + // Determine the MIME type based on file extension |
| 338 | + const fileName = documentData.fileName.toLowerCase(); |
| 339 | + let mimeType = 'application/octet-stream'; |
| 340 | + |
| 341 | + if (fileName.endsWith('.pdf')) { |
| 342 | + mimeType = 'application/pdf'; |
| 343 | + } else if (fileName.endsWith('.docx')) { |
| 344 | + mimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; |
| 345 | + } else if (fileName.endsWith('.doc')) { |
| 346 | + mimeType = 'application/msword'; |
| 347 | + } else if (fileName.endsWith('.txt')) { |
| 348 | + mimeType = 'text/plain'; |
| 349 | + } else if (fileName.endsWith('.md')) { |
| 350 | + mimeType = 'text/markdown'; |
| 351 | + } |
| 352 | +
|
| 353 | + // Decode base64 to binary |
| 354 | + const binaryString = atob(documentData.originalFileData); |
| 355 | + const bytes = new Uint8Array(binaryString.length); |
| 356 | + for (let i = 0; i < binaryString.length; i++) { |
| 357 | + bytes[i] = binaryString.charCodeAt(i); |
| 358 | + } |
| 359 | +
|
| 360 | + // Create blob and object URL |
| 361 | + const blob = new Blob([bytes], { type: mimeType }); |
| 362 | + const url = URL.createObjectURL(blob); |
| 363 | +
|
| 364 | + // Open in new window/tab |
| 365 | + const newWindow = window.open(url, '_blank', 'noopener,noreferrer'); |
| 366 | + |
| 367 | + if (!newWindow) { |
| 368 | + // If popup was blocked, try downloading instead |
| 369 | + const link = document.createElement('a'); |
| 370 | + link.href = url; |
| 371 | + link.download = documentData.fileName; |
| 372 | + link.click(); |
| 373 | + } |
| 374 | +
|
| 375 | + // Clean up the URL after a delay to prevent memory leaks |
| 376 | + setTimeout(() => { |
| 377 | + URL.revokeObjectURL(url); |
| 378 | + }, 10000); |
| 379 | + } catch (error) { |
| 380 | + console.error('Error opening document popup:', error); |
| 381 | + } |
| 382 | + } |
311 | 383 | </script> |
312 | 384 |
|
313 | 385 | <div class="chat-message {message.role}" bind:this={messageElement}> |
|
329 | 401 | </div> |
330 | 402 |
|
331 | 403 | <div class="flex-1 min-w-0"> |
332 | | - <div class="flex items-center space-x-2 mb-1"> |
333 | | - <span class="font-semibold text-sm text-surface-700-200-token"> |
334 | | - {message.role === 'user' ? 'You' : 'Assistant'} |
335 | | - </span> |
336 | | - <span class="text-xs text-surface-700-200-token opacity-70"> |
337 | | - {formatTimestamp(message.timestamp)} |
338 | | - </span> |
339 | | - {#if message.tokenCount && message.tokenCount > 0} |
340 | | - <span |
341 | | - class="text-xs text-surface-700-200-token opacity-80 bg-surface-200-700-token px-2 py-0.5 rounded" |
342 | | - > |
343 | | - {formatTokenCount(message.tokenCount)} |
| 404 | + <div class="flex items-center justify-between mb-1"> |
| 405 | + <div class="flex items-center space-x-2"> |
| 406 | + <span class="font-semibold text-sm text-surface-700-200-token"> |
| 407 | + {message.role === 'user' ? 'You' : 'Assistant'} |
344 | 408 | </span> |
345 | | - {:else if message.content && message.content.length > 0} |
346 | | - <span class="text-xs text-red-200 bg-red-500/30 px-2 py-0.5 rounded"> No tokens </span> |
347 | | - {/if} |
348 | | - {#if message.role === 'assistant' && message.responseTime} |
349 | | - <span |
350 | | - class="text-xs text-surface-700-200-token opacity-80 bg-surface-200-700-token px-2 py-0.5 rounded" |
351 | | - > |
352 | | - ⏱️ {formatResponseTime(message.responseTime)} |
| 409 | + <span class="text-xs text-surface-700-200-token opacity-70"> |
| 410 | + {formatTimestamp(message.timestamp)} |
353 | 411 | </span> |
354 | | - {/if} |
355 | | - </div> |
356 | | - |
357 | | - {#if message.content && message.content.trim().length > 0} |
358 | | - <div class="prose prose-sm max-w-none text-surface-700-200-token"> |
359 | | - {@html formatContent(message.content)} |
| 412 | + {#if message.tokenCount && message.tokenCount > 0} |
| 413 | + <span |
| 414 | + class="text-xs text-surface-700-200-token opacity-80 bg-surface-200-700-token px-2 py-0.5 rounded" |
| 415 | + > |
| 416 | + {formatTokenCount(message.tokenCount)} |
| 417 | + </span> |
| 418 | + {:else if message.content && message.content.length > 0} |
| 419 | + <span class="text-xs text-red-200 bg-red-500/30 px-2 py-0.5 rounded"> No tokens </span> |
| 420 | + {/if} |
| 421 | + {#if message.role === 'assistant' && message.responseTime} |
| 422 | + <span |
| 423 | + class="text-xs text-surface-700-200-token opacity-80 bg-surface-200-700-token px-2 py-0.5 rounded" |
| 424 | + > |
| 425 | + ⏱️ {formatResponseTime(message.responseTime)} |
| 426 | + </span> |
| 427 | + {/if} |
360 | 428 | </div> |
361 | | - {/if} |
362 | | - |
363 | | - {#if message.role === 'user' && onRetry} |
364 | | - <div class="mt-2"> |
| 429 | + |
| 430 | + <!-- Collapse and Close buttons --> |
| 431 | + <div class="flex items-center space-x-1"> |
| 432 | + <button |
| 433 | + class="btn-icon btn-icon-sm variant-ghost-surface opacity-70 hover:opacity-100" |
| 434 | + on:click={toggleCollapse} |
| 435 | + title={isCollapsed ? 'Expand message' : 'Collapse message'} |
| 436 | + aria-label={isCollapsed ? 'Expand message' : 'Collapse message'} |
| 437 | + > |
| 438 | + <i class="fa {isCollapsed ? 'fa-chevron-down' : 'fa-chevron-up'} text-xs"></i> |
| 439 | + </button> |
365 | 440 | <button |
366 | | - class="btn btn-sm variant-ghost-surface text-xs" |
367 | | - on:click={() => onRetry && onRetry(message.content)} |
368 | | - title="Retry this prompt" |
| 441 | + class="btn-icon btn-icon-sm variant-ghost-surface opacity-70 hover:opacity-100" |
| 442 | + on:click={closeMessage} |
| 443 | + title="Close message" |
| 444 | + aria-label="Close message" |
369 | 445 | > |
370 | | - <i class="fa fa-redo mr-1"></i> |
371 | | - Retry |
| 446 | + <i class="fa fa-times text-xs"></i> |
372 | 447 | </button> |
373 | 448 | </div> |
| 449 | + </div> |
| 450 | + |
| 451 | + {#if !isCollapsed} |
| 452 | + {#if message.content && message.content.trim().length > 0} |
| 453 | + <div class="prose prose-sm max-w-none text-surface-700-200-token"> |
| 454 | + {@html formatContent(message.content)} |
| 455 | + </div> |
| 456 | + {/if} |
| 457 | + {/if} |
| 458 | + |
| 459 | + {#if !isCollapsed} |
| 460 | + {#if message.role === 'user' && onRetry} |
| 461 | + <div class="mt-2"> |
| 462 | + <button |
| 463 | + class="btn btn-sm variant-ghost-surface text-xs" |
| 464 | + on:click={() => onRetry && onRetry(message.content)} |
| 465 | + title="Retry this prompt" |
| 466 | + > |
| 467 | + <i class="fa fa-redo mr-1"></i> |
| 468 | + Retry |
| 469 | + </button> |
| 470 | + </div> |
| 471 | + {/if} |
| 472 | + |
| 473 | + {#if message.documentData && hasOriginalFileData(message.documentData)} |
| 474 | + <div class="mt-2"> |
| 475 | + <button |
| 476 | + class="btn btn-sm variant-ghost-primary text-xs" |
| 477 | + on:click={() => openDocumentPopup(message.documentData)} |
| 478 | + title="View original document" |
| 479 | + > |
| 480 | + <i class="fa fa-external-link-alt mr-1"></i> |
| 481 | + View Original |
| 482 | + </button> |
| 483 | + </div> |
| 484 | + {/if} |
374 | 485 | {/if} |
375 | 486 |
|
376 | | - {#if message.chunks && message.chunks.length > 0} |
| 487 | + {#if !isCollapsed && message.chunks && message.chunks.length > 0} |
377 | 488 | <div class="mt-3 bg-blue-500/10 border border-blue-500/20 rounded-lg p-3"> |
378 | 489 | <div class="flex items-center gap-2 mb-2"> |
379 | 490 | <div class="flex items-center gap-1 text-blue-500"> |
|
0 commit comments