fix: prevent stream abort and improve attachment handling
- Limit max attachments to 5 files to prevent context overflow - Fix URL update timing: use SvelteKit's replaceState in onComplete callback instead of history.replaceState before streaming - Load attachment content from IndexedDB in conversation history so follow-up messages have access to file content - Show error messages in chat when Ollama fails instead of stuck "Processing..." indicator - Force file analysis when >3 files attached to reduce context usage
This commit is contained in:
@@ -278,8 +278,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Process other files
|
||||
// Process other files (limit to 5 total attachments)
|
||||
const MAX_ATTACHMENTS = 5;
|
||||
for (const file of otherFiles) {
|
||||
if (pendingAttachments.length >= MAX_ATTACHMENTS) {
|
||||
console.warn(`Maximum ${MAX_ATTACHMENTS} files reached, skipping remaining files`);
|
||||
break;
|
||||
}
|
||||
const result = await processFile(file);
|
||||
if (result.success) {
|
||||
pendingAttachments = [...pendingAttachments, result.attachment];
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import { addMessage as addStoredMessage, updateConversation, createConversation as createStoredConversation, saveAttachments } from '$lib/storage';
|
||||
import type { FileAttachment } from '$lib/types/attachment.js';
|
||||
import { fileAnalyzer, analyzeFilesInBatches, formatAnalyzedAttachment, type AnalysisResult } from '$lib/services/fileAnalyzer.js';
|
||||
import { attachmentService } from '$lib/services/attachmentService.js';
|
||||
import {
|
||||
contextManager,
|
||||
generateSummary,
|
||||
@@ -223,13 +224,38 @@
|
||||
/**
|
||||
* Convert chat state messages to Ollama API format
|
||||
* Uses messagesForContext to exclude summarized originals but include summaries
|
||||
* Now includes attachment content loaded from IndexedDB
|
||||
*/
|
||||
function getMessagesForApi(): OllamaMessage[] {
|
||||
return chatState.messagesForContext.map((node) => ({
|
||||
role: node.message.role as OllamaMessage['role'],
|
||||
content: node.message.content,
|
||||
images: node.message.images
|
||||
}));
|
||||
async function getMessagesForApi(): Promise<OllamaMessage[]> {
|
||||
const messages: OllamaMessage[] = [];
|
||||
|
||||
for (const node of chatState.messagesForContext) {
|
||||
let content = node.message.content;
|
||||
let images = node.message.images;
|
||||
|
||||
// Load attachment content if present
|
||||
if (node.message.attachmentIds && node.message.attachmentIds.length > 0) {
|
||||
// Load text content from attachments
|
||||
const attachmentContent = await attachmentService.buildOllamaContent(node.message.attachmentIds);
|
||||
if (attachmentContent) {
|
||||
content = content + '\n\n' + attachmentContent;
|
||||
}
|
||||
|
||||
// Load image base64 from attachments
|
||||
const attachmentImages = await attachmentService.buildOllamaImages(node.message.attachmentIds);
|
||||
if (attachmentImages.length > 0) {
|
||||
images = [...(images || []), ...attachmentImages];
|
||||
}
|
||||
}
|
||||
|
||||
messages.push({
|
||||
role: node.message.role as OllamaMessage['role'],
|
||||
content,
|
||||
images
|
||||
});
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -495,13 +521,17 @@
|
||||
|
||||
try {
|
||||
// Check if any files need actual LLM analysis
|
||||
const filesToAnalyze = attachments.filter(a => fileAnalyzer.shouldAnalyze(a));
|
||||
// Force analysis when >3 files to prevent context overflow (max 5 files allowed)
|
||||
const forceAnalysis = attachments.length > 3;
|
||||
const filesToAnalyze = forceAnalysis
|
||||
? attachments.filter(a => a.textContent && a.textContent.length > 2000)
|
||||
: attachments.filter(a => fileAnalyzer.shouldAnalyze(a));
|
||||
|
||||
if (filesToAnalyze.length > 0) {
|
||||
// Update indicator to show analysis
|
||||
chatState.setStreamContent(`Analyzing ${filesToAnalyze.length} ${filesToAnalyze.length === 1 ? 'file' : 'files'}...`);
|
||||
|
||||
const analysisResults = await analyzeFilesInBatches(filesToAnalyze, selectedModel, 2);
|
||||
const analysisResults = await analyzeFilesInBatches(filesToAnalyze, selectedModel, 3);
|
||||
|
||||
// Update attachments with results
|
||||
filesToAnalyze.forEach((file) => {
|
||||
@@ -527,7 +557,7 @@
|
||||
|
||||
contentForOllama = formattedParts.join('\n\n');
|
||||
} else {
|
||||
// No files need analysis, just format with content
|
||||
// No files need analysis, format with content
|
||||
const parts: string[] = [content];
|
||||
for (const a of attachments) {
|
||||
if (a.textContent) {
|
||||
@@ -587,7 +617,7 @@
|
||||
let pendingToolCalls: OllamaToolCall[] | null = null;
|
||||
|
||||
try {
|
||||
let messages = getMessagesForApi();
|
||||
let messages = await getMessagesForApi();
|
||||
const tools = getToolsForApi();
|
||||
|
||||
// If we have a content override (formatted attachments), replace the last user message content
|
||||
@@ -743,6 +773,9 @@
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Streaming error:', error);
|
||||
// Show error to user instead of leaving "Processing..."
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||
chatState.setStreamContent(`⚠️ Error: ${errorMsg}`);
|
||||
chatState.finishStreaming();
|
||||
streamingMetricsState.endStream();
|
||||
abortController = null;
|
||||
@@ -751,6 +784,10 @@
|
||||
abortController.signal
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to send message:', error);
|
||||
// Show error to user
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||
chatState.setStreamContent(`⚠️ Error: ${errorMsg}`);
|
||||
toastState.error('Failed to send message. Please try again.');
|
||||
chatState.finishStreaming();
|
||||
streamingMetricsState.endStream();
|
||||
@@ -899,7 +936,7 @@
|
||||
|
||||
try {
|
||||
// Get messages for API - excludes the current empty assistant message being streamed
|
||||
const messages = getMessagesForApi().filter(m => m.content !== '');
|
||||
const messages = (await getMessagesForApi()).filter(m => m.content !== '');
|
||||
const tools = getToolsForApi();
|
||||
|
||||
// Use function model for tool routing if enabled and tools are present
|
||||
@@ -956,6 +993,8 @@
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Regenerate error:', error);
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||
chatState.setStreamContent(`⚠️ Error: ${errorMsg}`);
|
||||
chatState.finishStreaming();
|
||||
streamingMetricsState.endStream();
|
||||
abortController = null;
|
||||
@@ -964,6 +1003,9 @@
|
||||
abortController.signal
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to regenerate:', error);
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||
chatState.setStreamContent(`⚠️ Error: ${errorMsg}`);
|
||||
toastState.error('Failed to regenerate. Please try again.');
|
||||
chatState.finishStreaming();
|
||||
streamingMetricsState.endStream();
|
||||
|
||||
@@ -52,6 +52,9 @@
|
||||
let errorMessage = $state<string | null>(null);
|
||||
let fileInputRef: HTMLInputElement | null = $state(null);
|
||||
|
||||
// Constants
|
||||
const MAX_ATTACHMENTS = 5;
|
||||
|
||||
// Derived states
|
||||
const hasAttachments = $derived(attachments.length > 0);
|
||||
const hasImages = $derived(images.length > 0);
|
||||
@@ -111,7 +114,15 @@
|
||||
}
|
||||
|
||||
if (newAttachments.length > 0) {
|
||||
onAttachmentsChange([...attachments, ...newAttachments]);
|
||||
const combined = [...attachments, ...newAttachments];
|
||||
if (combined.length > MAX_ATTACHMENTS) {
|
||||
const kept = combined.slice(0, MAX_ATTACHMENTS);
|
||||
const dropped = combined.length - MAX_ATTACHMENTS;
|
||||
onAttachmentsChange(kept);
|
||||
errors.push(`Maximum ${MAX_ATTACHMENTS} files allowed. ${dropped} file(s) not added.`);
|
||||
} else {
|
||||
onAttachmentsChange(combined);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import type { Conversation } from '$lib/types/conversation';
|
||||
import type { FileAttachment } from '$lib/types/attachment.js';
|
||||
import { fileAnalyzer, analyzeFilesInBatches, formatAnalyzedAttachment, type AnalysisResult } from '$lib/services/fileAnalyzer.js';
|
||||
import { replaceState } from '$app/navigation';
|
||||
|
||||
// RAG state
|
||||
let ragEnabled = $state(true);
|
||||
@@ -150,8 +151,8 @@
|
||||
// Persist user message to IndexedDB with the SAME ID as chatState
|
||||
await addStoredMessage(conversationId, { role: 'user', content, images, attachmentIds }, null, userMessageId);
|
||||
|
||||
// Update URL without navigation (keeps ChatWindow mounted)
|
||||
history.replaceState({}, '', `/chat/${conversationId}`);
|
||||
// NOTE: URL update moved to onComplete to avoid aborting the stream
|
||||
// The URL will update after the first response completes
|
||||
|
||||
// Process attachments if any
|
||||
let contentForOllama = content;
|
||||
@@ -168,13 +169,17 @@
|
||||
|
||||
try {
|
||||
// Check if any files need actual LLM analysis
|
||||
const filesToAnalyze = attachments.filter(a => fileAnalyzer.shouldAnalyze(a));
|
||||
// Force analysis when >3 files to prevent context overflow (max 5 files allowed)
|
||||
const forceAnalysis = attachments.length > 3;
|
||||
const filesToAnalyze = forceAnalysis
|
||||
? attachments.filter(a => a.textContent && a.textContent.length > 2000)
|
||||
: attachments.filter(a => fileAnalyzer.shouldAnalyze(a));
|
||||
|
||||
if (filesToAnalyze.length > 0) {
|
||||
// Update indicator to show analysis
|
||||
chatState.setStreamContent(`Analyzing ${filesToAnalyze.length} ${filesToAnalyze.length === 1 ? 'file' : 'files'}...`);
|
||||
|
||||
const analysisResults = await analyzeFilesInBatches(filesToAnalyze, model, 2);
|
||||
const analysisResults = await analyzeFilesInBatches(filesToAnalyze, model, 3);
|
||||
|
||||
// Update attachments with results
|
||||
filesToAnalyze.forEach((file) => {
|
||||
@@ -197,7 +202,7 @@
|
||||
}
|
||||
contentForOllama = formattedParts.join('\n\n');
|
||||
} else {
|
||||
// No files need analysis, just format with content
|
||||
// No files need analysis, format with content
|
||||
const parts: string[] = [content];
|
||||
for (const a of attachments) {
|
||||
if (a.textContent) {
|
||||
@@ -362,10 +367,16 @@
|
||||
|
||||
// Generate a smarter title in the background (don't await)
|
||||
generateSmartTitle(conversationId, content, node.message.content);
|
||||
|
||||
// Update URL now that streaming is complete
|
||||
replaceState(`/chat/${conversationId}`, {});
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Streaming error:', error);
|
||||
// Show error to user instead of leaving "Processing..."
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||
chatState.setStreamContent(`⚠️ Error: ${errorMsg}`);
|
||||
chatState.finishStreaming();
|
||||
streamingMetricsState.endStream();
|
||||
}
|
||||
@@ -373,6 +384,9 @@
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to send message:', error);
|
||||
// Show error to user
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||
chatState.setStreamContent(`⚠️ Error: ${errorMsg}`);
|
||||
chatState.finishStreaming();
|
||||
streamingMetricsState.endStream();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user