refactor: pre seed responses
This commit is contained in:
@@ -1130,7 +1130,7 @@ class ApiService {
|
|||||||
if (msg.role == 'assistant' && msg.model != null)
|
if (msg.role == 'assistant' && msg.model != null)
|
||||||
'modelName': msg.model,
|
'modelName': msg.model,
|
||||||
if (msg.role == 'assistant') 'modelIdx': 0,
|
if (msg.role == 'assistant') 'modelIdx': 0,
|
||||||
if (msg.role == 'assistant') 'done': true,
|
if (msg.role == 'assistant') 'done': !msg.isStreaming,
|
||||||
if (msg.role == 'user' && model != null) 'models': [model],
|
if (msg.role == 'user' && model != null) 'models': [model],
|
||||||
if (combinedFilesMap.isNotEmpty) 'files': combinedFilesMap,
|
if (combinedFilesMap.isNotEmpty) 'files': combinedFilesMap,
|
||||||
};
|
};
|
||||||
@@ -1166,7 +1166,7 @@ class ApiService {
|
|||||||
if (msg.role == 'assistant' && msg.model != null)
|
if (msg.role == 'assistant' && msg.model != null)
|
||||||
'modelName': msg.model,
|
'modelName': msg.model,
|
||||||
if (msg.role == 'assistant') 'modelIdx': 0,
|
if (msg.role == 'assistant') 'modelIdx': 0,
|
||||||
if (msg.role == 'assistant') 'done': true,
|
if (msg.role == 'assistant') 'done': !msg.isStreaming,
|
||||||
if (msg.role == 'user' && model != null) 'models': [model],
|
if (msg.role == 'user' && model != null) 'models': [model],
|
||||||
if (combinedFilesArray.isNotEmpty) 'files': combinedFilesArray,
|
if (combinedFilesArray.isNotEmpty) 'files': combinedFilesArray,
|
||||||
});
|
});
|
||||||
@@ -2652,11 +2652,14 @@ class ApiService {
|
|||||||
String? sessionIdOverride,
|
String? sessionIdOverride,
|
||||||
List<Map<String, dynamic>>? toolServers,
|
List<Map<String, dynamic>>? toolServers,
|
||||||
Map<String, dynamic>? backgroundTasks,
|
Map<String, dynamic>? backgroundTasks,
|
||||||
|
String? responseMessageId,
|
||||||
}) {
|
}) {
|
||||||
final streamController = StreamController<String>();
|
final streamController = StreamController<String>();
|
||||||
|
|
||||||
// Generate unique IDs
|
// Generate unique IDs
|
||||||
final messageId = const Uuid().v4();
|
final messageId = (responseMessageId != null && responseMessageId.isNotEmpty)
|
||||||
|
? responseMessageId
|
||||||
|
: const Uuid().v4();
|
||||||
final sessionId =
|
final sessionId =
|
||||||
(sessionIdOverride != null && sessionIdOverride.isNotEmpty)
|
(sessionIdOverride != null && sessionIdOverride.isNotEmpty)
|
||||||
? sessionIdOverride
|
? sessionIdOverride
|
||||||
@@ -2809,6 +2812,8 @@ class ApiService {
|
|||||||
// Always use task-based background flow for unified pipeline.
|
// Always use task-based background flow for unified pipeline.
|
||||||
// When a dynamic channel (session_id) is not provided, this method falls
|
// When a dynamic channel (session_id) is not provided, this method falls
|
||||||
// back to polling and streams deltas to the UI.
|
// back to polling and streams deltas to the UI.
|
||||||
|
// Always use background task flow (matches web client) to ensure
|
||||||
|
// server maintains correct history with pre-seeded assistant id.
|
||||||
final bool useBackgroundTasks = true;
|
final bool useBackgroundTasks = true;
|
||||||
|
|
||||||
// Use background flow only when required; otherwise prefer SSE even with chat_id.
|
// Use background flow only when required; otherwise prefer SSE even with chat_id.
|
||||||
|
|||||||
@@ -236,6 +236,71 @@ class ToolCallsParser {
|
|||||||
return raw.length > max ? '${raw.substring(0, max)}…' : raw;
|
return raw.length > max ? '${raw.substring(0, max)}…' : raw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sanitize assistant/user content before sending to the API, mirroring
|
||||||
|
/// the web client's `processDetails` behavior:
|
||||||
|
/// - Remove <details type="reasoning"> and <details type="code_interpreter"> blocks
|
||||||
|
/// - Replace <details type="tool_calls" ...>...</details> blocks with the
|
||||||
|
/// JSON-serialized `result` attribute (as a quoted string) when available;
|
||||||
|
/// otherwise replace with an empty string.
|
||||||
|
static String sanitizeForApi(String content) {
|
||||||
|
if (content.isEmpty) return content;
|
||||||
|
|
||||||
|
// Remove blocks we never want to include in conversation context
|
||||||
|
final removeTypes = ['reasoning', 'code_interpreter'];
|
||||||
|
for (final t in removeTypes) {
|
||||||
|
content = content.replaceAll(
|
||||||
|
RegExp(
|
||||||
|
'<details\\s+type=\"${t}\"[^>]*>[\\s\\S]*?<\\/details>',
|
||||||
|
multiLine: true,
|
||||||
|
dotAll: true,
|
||||||
|
),
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content.contains('<details')) return content.trim();
|
||||||
|
|
||||||
|
// Replace tool_calls blocks in-order with their results
|
||||||
|
final segs = segments(content);
|
||||||
|
if (segs == null || segs.isEmpty) return content.trim();
|
||||||
|
|
||||||
|
final buf = StringBuffer();
|
||||||
|
for (final seg in segs) {
|
||||||
|
if (seg.isToolCall && seg.entry != null) {
|
||||||
|
final entry = seg.entry!;
|
||||||
|
dynamic res = entry.result;
|
||||||
|
String out;
|
||||||
|
if (res == null) {
|
||||||
|
out = '';
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
out = json.encode(res);
|
||||||
|
} catch (_) {
|
||||||
|
out = res.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Match web behavior: wrap in quotes so it's clearly a string payload
|
||||||
|
if (out.isNotEmpty && !(out.startsWith('"') && out.endsWith('"'))) {
|
||||||
|
out = '"$out"';
|
||||||
|
}
|
||||||
|
buf.write(out);
|
||||||
|
} else {
|
||||||
|
// Keep the raw text, but also remove any stray non-tool_calls details blocks
|
||||||
|
final t = (seg.text ?? '').replaceAll(
|
||||||
|
RegExp(
|
||||||
|
r'<details(?!\s+type=\"tool_calls\")[^>]*>[\s\S]*?<\/details>',
|
||||||
|
multiLine: true,
|
||||||
|
dotAll: true,
|
||||||
|
),
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
if (t.isNotEmpty) buf.write(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.toString().trim();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ordered piece of content: either plain text or a tool-call entry
|
/// Ordered piece of content: either plain text or a tool-call entry
|
||||||
|
|||||||
@@ -450,37 +450,31 @@ Future<void> regenerateMessage(
|
|||||||
|
|
||||||
for (final msg in messages) {
|
for (final msg in messages) {
|
||||||
if (msg.role.isNotEmpty && msg.content.isNotEmpty && !msg.isStreaming) {
|
if (msg.role.isNotEmpty && msg.content.isNotEmpty && !msg.isStreaming) {
|
||||||
|
// Clean up tool/details markup to match web client behavior
|
||||||
|
final cleaned = ToolCallsParser.sanitizeForApi(msg.content);
|
||||||
|
|
||||||
// Handle messages with attachments
|
// Handle messages with attachments
|
||||||
if (msg.attachmentIds != null && msg.attachmentIds!.isNotEmpty) {
|
if (msg.attachmentIds != null && msg.attachmentIds!.isNotEmpty) {
|
||||||
final List<Map<String, dynamic>> contentArray = [];
|
final List<Map<String, dynamic>> contentArray = [];
|
||||||
|
|
||||||
// Add text content first
|
// Add text content first
|
||||||
if (msg.content.isNotEmpty) {
|
if (cleaned.isNotEmpty) {
|
||||||
contentArray.add({'type': 'text', 'text': msg.content});
|
contentArray.add({'type': 'text', 'text': cleaned});
|
||||||
}
|
}
|
||||||
|
|
||||||
conversationMessages.add({
|
conversationMessages.add({
|
||||||
'role': msg.role,
|
'role': msg.role,
|
||||||
'content': contentArray.isNotEmpty ? contentArray : msg.content,
|
'content': contentArray.isNotEmpty ? contentArray : cleaned,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Regular text message
|
// Regular text message
|
||||||
conversationMessages.add({'role': msg.role, 'content': msg.content});
|
conversationMessages.add({'role': msg.role, 'content': cleaned});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stream response using SSE
|
// Pre-seed assistant skeleton
|
||||||
final response = api!.sendMessage(
|
final String assistantMessageId = const Uuid().v4();
|
||||||
messages: conversationMessages,
|
|
||||||
model: selectedModel.id,
|
|
||||||
conversationId: activeConversation.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
final stream = response.stream;
|
|
||||||
final assistantMessageId = response.messageId;
|
|
||||||
|
|
||||||
// Add assistant message placeholder
|
|
||||||
final assistantMessage = ChatMessage(
|
final assistantMessage = ChatMessage(
|
||||||
id: assistantMessageId,
|
id: assistantMessageId,
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
@@ -490,6 +484,24 @@ Future<void> regenerateMessage(
|
|||||||
isStreaming: true,
|
isStreaming: true,
|
||||||
);
|
);
|
||||||
ref.read(chatMessagesProvider.notifier).addMessage(assistantMessage);
|
ref.read(chatMessagesProvider.notifier).addMessage(assistantMessage);
|
||||||
|
try {
|
||||||
|
final msgsForSeed = ref.read(chatMessagesProvider);
|
||||||
|
await api!.updateConversationWithMessages(
|
||||||
|
activeConversation.id,
|
||||||
|
msgsForSeed,
|
||||||
|
model: selectedModel.id,
|
||||||
|
);
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
// Stream response via background task (socket/dynamic channel or polling)
|
||||||
|
final response = api!.sendMessage(
|
||||||
|
messages: conversationMessages,
|
||||||
|
model: selectedModel.id,
|
||||||
|
conversationId: activeConversation.id,
|
||||||
|
responseMessageId: assistantMessageId,
|
||||||
|
);
|
||||||
|
|
||||||
|
final stream = response.stream;
|
||||||
|
|
||||||
// Handle streaming response (basic chunking for this path)
|
// Handle streaming response (basic chunking for this path)
|
||||||
final chunkedStream = StreamChunker.chunkStream(
|
final chunkedStream = StreamChunker.chunkStream(
|
||||||
@@ -705,6 +717,9 @@ Future<void> _sendMessageInternal(
|
|||||||
// Skip only empty assistant message placeholders that are currently streaming
|
// Skip only empty assistant message placeholders that are currently streaming
|
||||||
// Include completed messages (both user and assistant) for conversation history
|
// Include completed messages (both user and assistant) for conversation history
|
||||||
if (msg.role.isNotEmpty && msg.content.isNotEmpty && !msg.isStreaming) {
|
if (msg.role.isNotEmpty && msg.content.isNotEmpty && !msg.isStreaming) {
|
||||||
|
// Prepare cleaned text content (strip tool details etc.)
|
||||||
|
final cleaned = ToolCallsParser.sanitizeForApi(msg.content);
|
||||||
|
|
||||||
// Check if message has attachments (images and non-images)
|
// Check if message has attachments (images and non-images)
|
||||||
if (msg.attachmentIds != null && msg.attachmentIds!.isNotEmpty) {
|
if (msg.attachmentIds != null && msg.attachmentIds!.isNotEmpty) {
|
||||||
// All models use the same content array format (OpenWebUI standard)
|
// All models use the same content array format (OpenWebUI standard)
|
||||||
@@ -715,8 +730,8 @@ Future<void> _sendMessageInternal(
|
|||||||
final List<Map<String, dynamic>> nonImageFiles = [];
|
final List<Map<String, dynamic>> nonImageFiles = [];
|
||||||
|
|
||||||
// Add text content first
|
// Add text content first
|
||||||
if (msg.content.isNotEmpty) {
|
if (cleaned.isNotEmpty) {
|
||||||
contentArray.add({'type': 'text', 'text': msg.content});
|
contentArray.add({'type': 'text', 'text': cleaned});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add image attachments with proper MIME type handling; collect non-image attachments
|
// Add image attachments with proper MIME type handling; collect non-image attachments
|
||||||
@@ -772,7 +787,7 @@ Future<void> _sendMessageInternal(
|
|||||||
conversationMessages.add(messageMap);
|
conversationMessages.add(messageMap);
|
||||||
} else {
|
} else {
|
||||||
// Regular text-only message
|
// Regular text-only message
|
||||||
conversationMessages.add({'role': msg.role, 'content': msg.content});
|
conversationMessages.add({'role': msg.role, 'content': cleaned});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -789,6 +804,33 @@ Future<void> _sendMessageInternal(
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Pre-seed assistant skeleton on server to ensure correct chain
|
||||||
|
// Generate assistant message id now (must be consistent across client/server)
|
||||||
|
final String assistantMessageId = const Uuid().v4();
|
||||||
|
|
||||||
|
// Add assistant placeholder locally before sending
|
||||||
|
final assistantPlaceholder = ChatMessage(
|
||||||
|
id: assistantMessageId,
|
||||||
|
role: 'assistant',
|
||||||
|
content: '',
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
model: selectedModel.id,
|
||||||
|
isStreaming: true,
|
||||||
|
);
|
||||||
|
ref.read(chatMessagesProvider.notifier).addMessage(assistantPlaceholder);
|
||||||
|
|
||||||
|
// Persist skeleton chain to server so web can load correct history
|
||||||
|
try {
|
||||||
|
final activeConvForSeed = ref.read(activeConversationProvider);
|
||||||
|
if (activeConvForSeed != null) {
|
||||||
|
final msgsForSeed = ref.read(chatMessagesProvider);
|
||||||
|
await api.updateConversationWithMessages(
|
||||||
|
activeConvForSeed.id,
|
||||||
|
msgsForSeed,
|
||||||
|
model: selectedModel.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
// Use the model's actual supported parameters if available
|
// Use the model's actual supported parameters if available
|
||||||
final supportedParams =
|
final supportedParams =
|
||||||
selectedModel.supportedParameters ??
|
selectedModel.supportedParameters ??
|
||||||
@@ -1028,11 +1070,12 @@ Future<void> _sendMessageInternal(
|
|||||||
(conv.title == 'New Chat' && msgs.length <= 1);
|
(conv.title == 'New Chat' && msgs.length <= 1);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
|
// Match web client: request background follow-ups always; title/tags on first turn
|
||||||
final bgTasks = <String, dynamic>{
|
final bgTasks = <String, dynamic>{
|
||||||
if (shouldGenerateTitle) 'title_generation': true,
|
if (shouldGenerateTitle) 'title_generation': true,
|
||||||
if (shouldGenerateTitle) 'tags_generation': true,
|
if (shouldGenerateTitle) 'tags_generation': true,
|
||||||
'follow_up_generation': true,
|
'follow_up_generation': true,
|
||||||
if (webSearchEnabled) 'web_search': true, // enable bg workflow for web search
|
if (webSearchEnabled) 'web_search': true, // enable bg web search
|
||||||
if (imageGenerationEnabled) 'image_generation': true, // enable bg image flow
|
if (imageGenerationEnabled) 'image_generation': true, // enable bg image flow
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1056,23 +1099,12 @@ Future<void> _sendMessageInternal(
|
|||||||
sessionIdOverride: wantSessionBinding ? socketSessionId : null,
|
sessionIdOverride: wantSessionBinding ? socketSessionId : null,
|
||||||
toolServers: toolServers,
|
toolServers: toolServers,
|
||||||
backgroundTasks: bgTasks,
|
backgroundTasks: bgTasks,
|
||||||
|
responseMessageId: assistantMessageId,
|
||||||
);
|
);
|
||||||
|
|
||||||
final stream = response.stream;
|
final stream = response.stream;
|
||||||
final assistantMessageId = response.messageId;
|
|
||||||
final sessionId = response.sessionId;
|
final sessionId = response.sessionId;
|
||||||
|
|
||||||
// Add assistant message placeholder with the generated ID and immediate typing indicator
|
|
||||||
final assistantMessage = ChatMessage(
|
|
||||||
id: assistantMessageId,
|
|
||||||
role: 'assistant',
|
|
||||||
content: '',
|
|
||||||
timestamp: DateTime.now(),
|
|
||||||
model: selectedModel.id,
|
|
||||||
isStreaming: true,
|
|
||||||
);
|
|
||||||
ref.read(chatMessagesProvider.notifier).addMessage(assistantMessage);
|
|
||||||
|
|
||||||
// If socket is available, start listening for chat-events immediately
|
// If socket is available, start listening for chat-events immediately
|
||||||
// Background-tools flow OR any session-bound flow relies on socket/dynamic channel for
|
// Background-tools flow OR any session-bound flow relies on socket/dynamic channel for
|
||||||
// streaming content. Allow socket TEXT in those modes. For pure SSE/polling flows, suppress
|
// streaming content. Allow socket TEXT in those modes. For pure SSE/polling flows, suppress
|
||||||
@@ -1718,10 +1750,10 @@ Future<void> _sendMessageInternal(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save conversation to OpenWebUI server only after streaming is complete
|
// Do not persist conversation to server here. Server manages chat state.
|
||||||
// Add a small delay to ensure the last message content is fully updated
|
// Keep local save only for quick resume.
|
||||||
await Future.delayed(const Duration(milliseconds: 100));
|
await Future.delayed(const Duration(milliseconds: 50));
|
||||||
await _saveConversationToServer(ref);
|
await _saveConversationLocally(ref);
|
||||||
|
|
||||||
// Removed post-assistant image generation; images are handled immediately after user message
|
// Removed post-assistant image generation; images are handled immediately after user message
|
||||||
},
|
},
|
||||||
@@ -1956,17 +1988,7 @@ Future<void> _checkForTitleInBackground(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save current conversation to OpenWebUI server
|
// Save current conversation to OpenWebUI server
|
||||||
Future<void> _saveConversationToServer(dynamic ref) async {
|
// Removed server persistence; only local caching is used in mobile app.
|
||||||
// Enqueue save task; local fallback remains if queue fails
|
|
||||||
try {
|
|
||||||
final activeConversation = ref.read(activeConversationProvider);
|
|
||||||
await ref
|
|
||||||
.read(taskQueueProvider.notifier)
|
|
||||||
.enqueueSaveConversation(conversationId: activeConversation?.id);
|
|
||||||
} catch (_) {
|
|
||||||
await _saveConversationLocally(ref);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: Save current conversation to local storage
|
// Fallback: Save current conversation to local storage
|
||||||
Future<void> _saveConversationLocally(dynamic ref) async {
|
Future<void> _saveConversationLocally(dynamic ref) async {
|
||||||
|
|||||||
@@ -1030,7 +1030,9 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
ref.read(chatMessagesProvider.notifier).finishStreaming();
|
ref.read(chatMessagesProvider.notifier).finishStreaming();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _saveConversationBeforeLeaving(ref);
|
// Do not push conversation state back to server on exit.
|
||||||
|
// Server already maintains chat state from message sends.
|
||||||
|
// Keep any local persistence only.
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
final canPopNavigator = Navigator.of(context).canPop();
|
final canPopNavigator = Navigator.of(context).canPop();
|
||||||
|
|||||||
@@ -74,17 +74,6 @@ abstract class OutboundTask with _$OutboundTask {
|
|||||||
String? error,
|
String? error,
|
||||||
}) = GenerateImageTask;
|
}) = GenerateImageTask;
|
||||||
|
|
||||||
const factory OutboundTask.saveConversation({
|
|
||||||
required String id,
|
|
||||||
String? conversationId,
|
|
||||||
@Default(TaskStatus.queued) TaskStatus status,
|
|
||||||
@Default(0) int attempt,
|
|
||||||
String? idempotencyKey,
|
|
||||||
DateTime? enqueuedAt,
|
|
||||||
DateTime? startedAt,
|
|
||||||
DateTime? completedAt,
|
|
||||||
String? error,
|
|
||||||
}) = SaveConversationTask;
|
|
||||||
|
|
||||||
const factory OutboundTask.generateTitle({
|
const factory OutboundTask.generateTitle({
|
||||||
required String id,
|
required String id,
|
||||||
@@ -121,7 +110,6 @@ abstract class OutboundTask with _$OutboundTask {
|
|||||||
uploadMedia: (t) => t.conversationId,
|
uploadMedia: (t) => t.conversationId,
|
||||||
executeToolCall: (t) => t.conversationId,
|
executeToolCall: (t) => t.conversationId,
|
||||||
generateImage: (t) => t.conversationId,
|
generateImage: (t) => t.conversationId,
|
||||||
saveConversation: (t) => t.conversationId,
|
|
||||||
generateTitle: (t) => t.conversationId,
|
generateTitle: (t) => t.conversationId,
|
||||||
imageToDataUrl: (t) => t.conversationId,
|
imageToDataUrl: (t) => t.conversationId,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -273,22 +273,7 @@ class TaskQueueNotifier extends StateNotifier<List<OutboundTask>> {
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> enqueueSaveConversation({
|
// Removed: enqueueSaveConversation — mobile app no longer persists chats to server.
|
||||||
required String? conversationId,
|
|
||||||
String? idempotencyKey,
|
|
||||||
}) async {
|
|
||||||
final id = _uuid.v4();
|
|
||||||
final task = OutboundTask.saveConversation(
|
|
||||||
id: id,
|
|
||||||
conversationId: conversationId,
|
|
||||||
idempotencyKey: idempotencyKey,
|
|
||||||
enqueuedAt: DateTime.now(),
|
|
||||||
);
|
|
||||||
state = [...state, task];
|
|
||||||
await _save();
|
|
||||||
_process();
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String> enqueueGenerateTitle({
|
Future<String> enqueueGenerateTitle({
|
||||||
required String conversationId,
|
required String conversationId,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class TaskWorker {
|
|||||||
executeToolCall: _performExecuteToolCall,
|
executeToolCall: _performExecuteToolCall,
|
||||||
generateImage: _performGenerateImage,
|
generateImage: _performGenerateImage,
|
||||||
imageToDataUrl: _performImageToDataUrl,
|
imageToDataUrl: _performImageToDataUrl,
|
||||||
saveConversation: _performSaveConversation,
|
// saveConversation removed — we no longer push chat state to server
|
||||||
generateTitle: _performGenerateTitle,
|
generateTitle: _performGenerateTitle,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -328,36 +328,7 @@ class TaskWorker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _performSaveConversation(SaveConversationTask task) async {
|
// _performSaveConversation removed
|
||||||
final api = _ref.read(apiServiceProvider);
|
|
||||||
final messages = _ref.read(chat.chatMessagesProvider);
|
|
||||||
final activeConv = _ref.read(activeConversationProvider);
|
|
||||||
final selectedModel = _ref.read(selectedModelProvider);
|
|
||||||
if (api == null || messages.isEmpty || activeConv == null) return;
|
|
||||||
|
|
||||||
// Skip if last assistant is empty placeholder
|
|
||||||
final last = messages.last;
|
|
||||||
if (last.role == 'assistant' &&
|
|
||||||
last.content.trim().isEmpty &&
|
|
||||||
(last.files?.isEmpty ?? true) &&
|
|
||||||
(last.attachmentIds?.isEmpty ?? true)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await api.updateConversationWithMessages(
|
|
||||||
activeConv.id,
|
|
||||||
messages,
|
|
||||||
model: selectedModel?.id,
|
|
||||||
);
|
|
||||||
final updated = activeConv.copyWith(
|
|
||||||
messages: messages,
|
|
||||||
updatedAt: DateTime.now(),
|
|
||||||
);
|
|
||||||
_ref.read(activeConversationProvider.notifier).state = updated;
|
|
||||||
_ref.invalidate(conversationsProvider);
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _performGenerateTitle(GenerateTitleTask task) async {
|
Future<void> _performGenerateTitle(GenerateTitleTask task) async {
|
||||||
final api = _ref.read(apiServiceProvider);
|
final api = _ref.read(apiServiceProvider);
|
||||||
@@ -387,15 +358,8 @@ class TaskWorker {
|
|||||||
updatedAt: DateTime.now(),
|
updatedAt: DateTime.now(),
|
||||||
);
|
);
|
||||||
_ref.read(activeConversationProvider.notifier).state = updated;
|
_ref.read(activeConversationProvider.notifier).state = updated;
|
||||||
try {
|
// Do not push full messages to server; skip remote update.
|
||||||
final cur = _ref.read(chat.chatMessagesProvider);
|
// Optionally refresh list to reflect server-side title when it’s generated there.
|
||||||
await api.updateConversationWithMessages(
|
|
||||||
updated.id,
|
|
||||||
cur,
|
|
||||||
title: updated.title,
|
|
||||||
model: selectedModel.id,
|
|
||||||
);
|
|
||||||
} catch (_) {}
|
|
||||||
_ref.invalidate(conversationsProvider);
|
_ref.invalidate(conversationsProvider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user