Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8cd2162
feat edit summary and transcript
krushnarout Dec 14, 2025
5994939
fix search hightlight
krushnarout Dec 14, 2025
94f41a8
fix
krushnarout Dec 15, 2025
096b499
fix search highlight
krushnarout Dec 16, 2025
5d7821f
Merge remote-tracking branch 'origin/main' into feat/edit-summary-tra…
krushnarout Dec 19, 2025
2265885
feat add double tap to edit and cleanup
krushnarout Dec 22, 2025
170fc9c
Merge remote-tracking branch 'origin/main' into feat/edit-summary-tra…
krushnarout Dec 22, 2025
1495837
remove unrelated format changes
krushnarout Dec 22, 2025
5749f99
clean
krushnarout Dec 22, 2025
eebbba6
feat add double tap handling for edit
krushnarout Dec 24, 2025
dccf383
Merge remote-tracking branch 'origin/main' into feat/edit-summary-tra…
krushnarout Dec 24, 2025
1b36c24
fix transcript edit
krushnarout Dec 24, 2025
c6feaa8
fix minor
krushnarout Dec 24, 2025
e4f514f
Merge remote-tracking branch 'origin/main' into feat/edit-summary-tra…
krushnarout Jan 1, 2026
19cdc16
Merge remote-tracking branch 'origin/main' into feat/edit-summary-tra…
krushnarout Jan 6, 2026
4b396c6
Merge remote-tracking branch 'origin/main' into feat/edit-summary-tra…
krushnarout Jan 12, 2026
80dff1a
Merge remote-tracking branch 'origin/main' into feat/edit-summary-tra…
krushnarout Jan 12, 2026
c2476e5
fix search tab issue and edit feature
krushnarout Jan 12, 2026
5a08bd5
Merge remote-tracking branch 'origin/main' into feat/edit-summary-tra…
krushnarout Jan 15, 2026
7e12160
Merge remote-tracking branch 'origin/main' into feat/edit-summary-tra…
krushnarout Jan 20, 2026
3e3c0f0
fix refresh overview controller and update convo overview
krushnarout Jan 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions app/lib/backend/http/api/conversations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,30 @@ Future<bool> updateConversationTitle(String conversationId, String title) async
return response.statusCode == 200;
}

Future<bool> updateConversationOverview(String conversationId, String overview) async {
var response = await makeApiCall(
url: '${Env.apiBaseUrl}v1/conversations/$conversationId/overview',
headers: {'Content-Type': 'application/json'},
method: 'PATCH',
body: jsonEncode({'overview': overview}),
);
if (response == null) return false;
debugPrint('updateConversationOverview: ${response.body}');
return response.statusCode == 200;
}

Future<bool> updateSegmentText(String conversationId, String segmentId, String text) async {
var response = await makeApiCall(
url: '${Env.apiBaseUrl}v1/conversations/$conversationId/segments/$segmentId/text',
headers: {'Content-Type': 'application/json'},
method: 'PATCH',
body: jsonEncode({'text': text}),
);
if (response == null) return false;
debugPrint('updateSegmentText: ${response.body}');
return response.statusCode == 200;
}

Future<List<ConversationPhoto>> getConversationPhotos(String conversationId) async {
var response = await makeApiCall(
url: '${Env.apiBaseUrl}v1/conversations/$conversationId/photos',
Expand Down
10 changes: 10 additions & 0 deletions app/lib/backend/schema/conversation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,16 @@ class ServerConversation {
return transcriptSegments.indexWhere((element) => element.speakerId == speakerId);
}

String getSummary() {
if (structured.overview.trim().isNotEmpty) {
return structured.overview.trim();
}
if (appResults.isNotEmpty && appResults[0].content.trim().isNotEmpty) {
return appResults[0].content.trim();
}
return structured.toString();
}

String getTag() {
if (source == ConversationSource.screenpipe) return 'Screenpipe';
if (source == ConversationSource.openglass) return 'OmiGlass';
Expand Down
8 changes: 8 additions & 0 deletions app/lib/pages/capture/widgets/widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ getTranscriptWidget(
String searchQuery = '',
int currentResultIndex = -1,
VoidCallback? onTapWhenSearchEmpty,
Map<String, TextEditingController>? segmentControllers,
Map<String, FocusNode>? segmentFocusNodes,
Function(int)? onMatchCountChanged,
Function(String)? onSegmentEdit,
}) {
if (conversationCreating) {
return const Padding(
Expand Down Expand Up @@ -223,6 +227,10 @@ getTranscriptWidget(
searchQuery: searchQuery,
currentResultIndex: currentResultIndex,
onTapWhenSearchEmpty: onTapWhenSearchEmpty,
segmentControllers: segmentControllers,
segmentFocusNodes: segmentFocusNodes,
onMatchCountChanged: onMatchCountChanged,
onSegmentEdit: onSegmentEdit,
);
}

Expand Down
190 changes: 181 additions & 9 deletions app/lib/pages/conversation_detail/conversation_detail_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ class ConversationDetailProvider extends ChangeNotifier with MessageNotifierMixi
TextEditingController? titleController;
FocusNode? titleFocusNode;

TextEditingController? overviewController;
FocusNode? overviewFocusNode;

Map<String, TextEditingController> segmentControllers = {};
Map<String, FocusNode> segmentFocusNodes = {};

bool isTranscriptExpanded = false;

bool canDisplaySeconds = true;
Expand All @@ -95,6 +101,89 @@ class ConversationDetailProvider extends ChangeNotifier with MessageNotifierMixi

bool showUnassignedFloatingButton = true;

bool isEditingSummary = false;

void enterSummaryEdit() {
isEditingSummary = true;
overviewFocusNode?.requestFocus();
notifyListeners();
}

void exitSummaryEdit() {
isEditingSummary = false;
overviewFocusNode?.unfocus();
notifyListeners();
}

void toggleSummaryEdit() {
if (isEditingSummary) {
exitSummaryEdit();
} else {
enterSummaryEdit();
}
}

void refreshOverviewController() {
final summarizedApp = getSummarizedApp();
if (summarizedApp != null && overviewController != null) {
overviewController!.text = summarizedApp.content;
}
}

String? editingSegmentId;

void enterSegmentEdit(String segmentId) {
if (segmentControllers.containsKey(segmentId)) {
editingSegmentId = segmentId;
notifyListeners();
return;
}

final segmentIndex = conversation.transcriptSegments.indexWhere((s) => s.id == segmentId);

if (segmentIndex == -1) {
debugPrint('Segment not found for edit: $segmentId');
return;
}

final segment = conversation.transcriptSegments[segmentIndex];
final controller = TextEditingController(text: segment.text);
final focusNode = FocusNode();

segmentControllers[segmentId] = controller;
segmentFocusNodes[segmentId] = focusNode;

focusNode.addListener(() async {
if (!focusNode.hasFocus) {
final updatedText = controller.text;
if (segment.text != updatedText) {
final oldText = segment.text;
segment.text = updatedText;

final success = await updateSegmentText(
conversation.id,
segment.id,
updatedText,
);

if (!success) {
segment.text = oldText;
controller.text = oldText;
notifyListeners();
}
}
}
});

editingSegmentId = segmentId;
notifyListeners();
}

void exitSegmentEdit() {
editingSegmentId = null;
notifyListeners();
}

void toggleEditSegmentLoading(bool value) {
editSegmentLoading = value;
notifyListeners();
Expand Down Expand Up @@ -207,28 +296,107 @@ class ConversationDetailProvider extends ChangeNotifier with MessageNotifierMixi
setShowRatingUi(false);
}

Future initConversation() async {
// updateLoadingState(true);
void _disposeControllers() {
titleController?.dispose();
titleFocusNode?.dispose();
overviewController?.dispose();
overviewFocusNode?.dispose();

for (var controller in segmentControllers.values) {
controller.dispose();
}
for (var focusNode in segmentFocusNodes.values) {
focusNode.dispose();
}
segmentControllers.clear();
segmentFocusNodes.clear();
}

Future initConversation() async {
_disposeControllers();

_ratingTimer?.cancel();
showRatingUI = false;
hasConversationSummaryRatingSet = false;
isEditingSummary = false;

titleController = TextEditingController();
titleFocusNode = FocusNode();

overviewController = TextEditingController();
overviewFocusNode = FocusNode();

showUnassignedFloatingButton = true;

titleController!.text = conversation.structured.title;
titleFocusNode!.addListener(() {
print('titleFocusNode focus changed');
var lastSavedTitle = conversation.structured.title;
titleController!.text = lastSavedTitle;
titleFocusNode!.addListener(() async {
debugPrint('titleFocusNode focus changed');
if (!titleFocusNode!.hasFocus) {
conversation.structured.title = titleController!.text;
updateConversationTitle(conversation.id, titleController!.text);
final newTitle = titleController!.text;
if (lastSavedTitle != newTitle) {
final oldTitle = lastSavedTitle;
conversation.structured.title = newTitle;
lastSavedTitle = newTitle;
notifyListeners(); // Update UI immediately if needed

final success = await updateConversationTitle(conversation.id, newTitle);
if (!success) {
conversation.structured.title = oldTitle;
lastSavedTitle = oldTitle;
titleController!.text = oldTitle;
notifyListeners();
}
}
}
});

final summarizedApp = getSummarizedApp();
if (summarizedApp != null) {
overviewController!.text = summarizedApp.content;

String lastSavedOverview = summarizedApp.content;

overviewFocusNode!.addListener(() async {
if (!overviewFocusNode!.hasFocus) {
if (isEditingSummary) {
exitSummaryEdit();
}

final newOverview = overviewController!.text;
if (lastSavedOverview != newOverview) {
final oldOverview = lastSavedOverview;
// Update both the structured overview and the app result content
conversation.structured.overview = newOverview;
if (conversation.appResults.isNotEmpty) {
conversation.appResults[0] = AppResponse(
newOverview,
appId: conversation.appResults[0].appId,
id: conversation.appResults[0].id,
);
}
lastSavedOverview = newOverview;
notifyListeners();

final success = await updateConversationOverview(conversation.id, newOverview);
if (!success) {
conversation.structured.overview = oldOverview;
lastSavedOverview = oldOverview;
overviewController!.text = oldOverview;
if (conversation.appResults.isNotEmpty) {
conversation.appResults[0] = AppResponse(
oldOverview,
appId: conversation.appResults[0].appId,
id: conversation.appResults[0].id,
);
}
notifyListeners();
}
}
}
});
}

canDisplaySeconds = TranscriptSegment.canDisplaySeconds(conversation.transcriptSegments);

loadPreferredSummarizationApp();
Expand Down Expand Up @@ -283,6 +451,8 @@ class ConversationDetailProvider extends ChangeNotifier with MessageNotifierMixi
// Update the cached conversation to ensure we have the latest data
_cachedConversation = updatedConversation;

refreshOverviewController();

// Check if the summarized app is in the apps list
AppResponse? summaryApp = getSummarizedApp();
if (summaryApp != null && summaryApp.appId != null && appProvider != null) {
Expand Down Expand Up @@ -323,14 +493,15 @@ class ConversationDetailProvider extends ChangeNotifier with MessageNotifierMixi
/// Returns the first app result from the conversation if available
/// This is typically the summary of the conversation
AppResponse? getSummarizedApp() {
// First check appResults as this contains the properly formatted markdown summary
if (conversation.appResults.isNotEmpty) {
return conversation.appResults[0];
}
// If no appResults but we have structured overview, create a fake AppResponse
if (conversation.structured.overview.isNotEmpty) {
if (conversation.structured.overview.trim().isNotEmpty) {
return AppResponse(
conversation.structured.overview,
appId: null,
id: 0,
);
}
return null;
Expand Down Expand Up @@ -554,6 +725,7 @@ class ConversationDetailProvider extends ChangeNotifier with MessageNotifierMixi
void dispose() {
_isDisposed = true;
_ratingTimer?.cancel();
_disposeControllers();
super.dispose();
}
}
Loading