Skip to content

Commit c6cc662

Browse files
more ui changes with collapsable messages and doc previews
1 parent 9596333 commit c6cc662

8 files changed

Lines changed: 271 additions & 45 deletions

File tree

src/app.css

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,88 @@ body {
127127
@apply bg-surface-100-800-token mr-8;
128128
}
129129

130+
.chat-bubble-preview {
131+
@apply relative;
132+
background: var(--color-surface-100);
133+
border: 1px solid var(--color-surface-300);
134+
scrollbar-width: thin;
135+
scrollbar-color: var(--color-surface-400) var(--color-surface-100);
136+
}
137+
138+
.chat-bubble-preview::-webkit-scrollbar {
139+
width: 8px;
140+
}
141+
142+
.chat-bubble-preview::-webkit-scrollbar-track {
143+
background: var(--color-surface-200);
144+
border-radius: 4px;
145+
}
146+
147+
.chat-bubble-preview::-webkit-scrollbar-thumb {
148+
background: var(--color-surface-400);
149+
border-radius: 4px;
150+
}
151+
152+
.chat-bubble-preview::-webkit-scrollbar-thumb:hover {
153+
background: var(--color-surface-500);
154+
}
155+
156+
@media (prefers-color-scheme: dark) {
157+
.chat-bubble-preview {
158+
background: var(--color-surface-800);
159+
border-color: var(--color-surface-600);
160+
scrollbar-color: var(--color-surface-500) var(--color-surface-800);
161+
}
162+
}
163+
164+
/* Scrollable chat area positioning */
165+
.chat-scrollable-area {
166+
height: 100%;
167+
min-height: calc(100vh - 8rem);
168+
max-height: calc(100vh - 8rem);
169+
overflow-y: auto;
170+
scroll-behavior: smooth;
171+
-webkit-overflow-scrolling: touch;
172+
scrollbar-width: thin;
173+
scrollbar-color: var(--color-surface-400) transparent;
174+
background-color: var(--color-surface-50);
175+
}
176+
177+
178+
.chat-scrollable-area::-webkit-scrollbar {
179+
width: 8px;
180+
}
181+
182+
.chat-scrollable-area::-webkit-scrollbar-track {
183+
background: transparent;
184+
}
185+
186+
.chat-scrollable-area::-webkit-scrollbar-thumb {
187+
background: var(--color-surface-400);
188+
border-radius: 4px;
189+
}
190+
191+
.chat-scrollable-area::-webkit-scrollbar-thumb:hover {
192+
background: var(--color-surface-500);
193+
}
194+
195+
/* Responsive adjustments */
196+
@media (max-width: 768px) {
197+
.chat-scrollable-area {
198+
min-height: calc(100vh - 6rem);
199+
max-height: calc(100vh - 6rem);
200+
}
201+
}
202+
203+
/* Adjust for RAG panel - removed margins to let background extend to border */
204+
205+
@media (prefers-color-scheme: dark) {
206+
.chat-scrollable-area {
207+
scrollbar-color: var(--color-surface-500) transparent;
208+
background-color: var(--color-surface-900);
209+
}
210+
}
211+
130212
.upload-zone {
131213
@apply border-2 border-dashed border-surface-300-600-token rounded-lg p-8 text-center;
132214
transition: all 200ms ease-in-out;

src/lib/components/ChatInterface.svelte

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,10 @@
571571
handleSubmit();
572572
}
573573
574+
function handleMessageClose(messageId: string) {
575+
removeMessageById(messageId);
576+
}
577+
574578
// File handling for drag-and-drop
575579
async function handleFilesDropped(event: CustomEvent<FileList>) {
576580
if (!featureManager.isEnabled('dragDropUpload')) return;
@@ -1107,6 +1111,27 @@
11071111
}
11081112
}
11091113
1114+
// Function to send document preview to chat
1115+
export function sendDocumentPreviewToChat(document: any) {
1116+
const content = document.content.length <= 2000 ? document.content : document.content.substring(0, 2000) + '...';
1117+
1118+
const previewMessage: ChatMessageType = {
1119+
id: crypto.randomUUID(),
1120+
role: 'assistant',
1121+
content: `📄 **Document Preview: ${document.fileName}**\n\n${content}`,
1122+
timestamp: Date.now(),
1123+
documentData: document // Store document data for popup functionality
1124+
};
1125+
1126+
addMessage(previewMessage);
1127+
1128+
// Auto-scroll to show the new message
1129+
shouldAutoScroll = true;
1130+
tick().then(() => {
1131+
scrollToBottom();
1132+
});
1133+
}
1134+
11101135
async function handleFileInputChange(event: Event) {
11111136
const input = event.target as HTMLInputElement;
11121137
const files = input.files;
@@ -1223,7 +1248,7 @@
12231248
// Removed problematic reactive statement that caused infinite loops
12241249
</script>
12251250

1226-
<div class="h-full flex overflow-hidden">
1251+
<div class="h-full flex overflow-hidden bg-surface-50-900-token">
12271252
<!-- Progress bar - positioned absolutely at top -->
12281253
{#if !$isModelLoaded && $modelLoadingProgress < 100}
12291254
<div
@@ -1243,14 +1268,15 @@
12431268
{/if}
12441269

12451270
<!-- Main chat area -->
1246-
<div class="flex-1 min-h-0 flex flex-col transition-all duration-300" class:mr-80={showRAGPanel}>
1271+
<div class="flex-1 min-h-0 flex flex-col transition-all duration-300 bg-surface-50-900-token" class:mr-80={showRAGPanel}>
12471272
<!-- Chat messages with drag-and-drop -->
1248-
<DragDropZone className="flex-1 min-h-0" on:files={handleFilesDropped} on:error={handleFileError}>
1273+
<DragDropZone className="flex-1 min-h-0 h-full bg-surface-50-900-token" on:files={handleFilesDropped} on:error={handleFileError}>
12491274
<div
12501275
bind:this={chatContainer}
1251-
class="h-full overflow-y-auto p-4 space-y-4 scroll-smooth"
1276+
class="chat-scrollable-area p-4 space-y-4 h-full"
12521277
class:pt-24={!$isModelLoaded && $modelLoadingProgress < 100}
1253-
style="scroll-behavior: smooth; -webkit-overflow-scrolling: touch;"
1278+
class:with-rag-panel={showRAGPanel}
1279+
style="padding-bottom: 1rem; min-height: 100vh;"
12541280
on:scroll={handleScroll}
12551281
>
12561282
{#if $currentMessages.length === 0}
@@ -1264,7 +1290,7 @@
12641290
</div>
12651291
{:else}
12661292
{#each $currentMessages.filter(m => m.content && m.content.trim().length > 0) as message (message.id)}
1267-
<ChatMessage {message} onRetry={handleRetry} />
1293+
<ChatMessage {message} onRetry={handleRetry} onClose={handleMessageClose} />
12681294
{/each}
12691295

12701296
{#if $isTyping}
@@ -1349,6 +1375,7 @@
13491375
bind:isVisible={showRAGPanel}
13501376
bind:lastQuery={lastRAGQuery}
13511377
forceRefresh={ragRefreshCounter}
1378+
{sendDocumentPreviewToChat}
13521379
/>
13531380
{/if}
13541381
</div>

src/lib/components/ChatMessage.svelte

Lines changed: 147 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@
4747
4848
export let message: ChatMessage;
4949
export let onRetry: ((content: string) => void) | undefined = undefined;
50+
export let onClose: ((messageId: string) => void) | undefined = undefined;
51+
52+
let isCollapsed = false;
5053
5154
// For code blocks, we'll estimate token count (rough approximation)
5255
function estimateTokens(text: string): number {
@@ -308,6 +311,75 @@
308311
function formatTimestamp(timestamp: number): string {
309312
return new Date(timestamp).toLocaleTimeString();
310313
}
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+
}
311383
</script>
312384

313385
<div class="chat-message {message.role}" bind:this={messageElement}>
@@ -329,51 +401,90 @@
329401
</div>
330402

331403
<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'}
344408
</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)}
353411
</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}
360428
</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>
365440
<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"
369445
>
370-
<i class="fa fa-redo mr-1"></i>
371-
Retry
446+
<i class="fa fa-times text-xs"></i>
372447
</button>
373448
</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}
374485
{/if}
375486

376-
{#if message.chunks && message.chunks.length > 0}
487+
{#if !isCollapsed && message.chunks && message.chunks.length > 0}
377488
<div class="mt-3 bg-blue-500/10 border border-blue-500/20 rounded-lg p-3">
378489
<div class="flex items-center gap-2 mb-2">
379490
<div class="flex items-center gap-1 text-blue-500">

0 commit comments

Comments
 (0)