Skip to content

Commit

Permalink
Assign segments improvements and multiple segments assigning (#1591)
Browse files Browse the repository at this point in the history
- [x] Assign multiple segments to a single person
- [x] Assign single segment to a single person
- [x] Create a person if does not exist while assigning
- [x] Improve assigning sheet UI



https://github.com/user-attachments/assets/ed8ac85f-ff6e-44e8-bce0-44664ec03512
  • Loading branch information
beastoin authored Dec 29, 2024
2 parents 7a237aa + ce174d3 commit b5396ae
Show file tree
Hide file tree
Showing 6 changed files with 526 additions and 147 deletions.
20 changes: 20 additions & 0 deletions app/lib/backend/http/api/conversations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,26 @@ Future<bool> assignConversationTranscriptSegment(
return response.statusCode == 200;
}

Future<bool> assignConversationSpeaker(
String conversationId,
int speakerId,
bool isUser, {
String? personId,
bool useForSpeechTraining = true,
}) async {
String assignType = isUser ? 'is_user' : 'person_id';
var response = await makeApiCall(
url: '${Env.apiBaseUrl}v1/memories/$conversationId/assign-speaker/$speakerId?value=${isUser ? 'true' : personId}'
'&assign_type=$assignType&use_for_speech_training=$useForSpeechTraining',
headers: {},
method: 'PATCH',
body: '',
);
if (response == null) return false;
debugPrint('assignConversationSpeaker: ${response.body}');
return response.statusCode == 200;
}

Future<bool> setConversationVisibility(String conversationId, {String visibility = 'shared'}) async {
var response = await makeApiCall(
url: '${Env.apiBaseUrl}v1/memories/$conversationId/visibility?value=$visibility&visibility=$visibility',
Expand Down
15 changes: 15 additions & 0 deletions app/lib/backend/schema/conversation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,21 @@ class ServerConversation {
return transcriptSegments.where((element) => (element.personId == null && !element.isUser)).length;
}

int speakerWithMostUnassignedSegments() {
var speakers = transcriptSegments
.where((element) => element.personId == null && !element.isUser)
.map((e) => e.speakerId)
.toList();
if (speakers.isEmpty) return -1;
var segmentsBySpeakers =
groupBy(speakers, (e) => e).entries.reduce((a, b) => a.value.length > b.value.length ? a : b).key;
return segmentsBySpeakers;
}

int firstSegmentIndexForSpeaker(int speakerId) {
return transcriptSegments.indexWhere((element) => element.speakerId == speakerId);
}

String getTag() {
if (source == ConversationSource.screenpipe) return 'Screenpipe';
if (source == ConversationSource.openglass) return 'Openglass';
Expand Down
271 changes: 126 additions & 145 deletions app/lib/pages/conversation_detail/page.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_provider_utilities/flutter_provider_utilities.dart';
import 'package:friend_private/backend/http/api/conversations.dart';
import 'package:friend_private/backend/preferences.dart';
Expand All @@ -22,6 +21,7 @@ import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';

import 'conversation_detail_provider.dart';
import 'widgets/name_speaker_sheet.dart';

class ConversationDetailPage extends StatefulWidget {
final ServerConversation conversation;
Expand Down Expand Up @@ -143,29 +143,29 @@ class _ConversationDetailPageState extends State<ConversationDetailPage> with Ti
);
}),
),
floatingActionButton: Selector<ConversationDetailProvider, int>(
selector: (context, provider) => provider.selectedTab,
builder: (context, selectedTab, child) {
return selectedTab == 0
? FloatingActionButton(
backgroundColor: Colors.black,
elevation: 8,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(32)),
side: BorderSide(color: Colors.grey, width: 1)),
onPressed: () {
var provider = Provider.of<ConversationDetailProvider>(context, listen: false);
Clipboard.setData(ClipboardData(text: provider.conversation.getTranscript(generate: true)));
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('Transcript copied to clipboard'),
duration: Duration(seconds: 1),
));
MixpanelManager().copiedConversationDetails(provider.conversation, source: 'Transcript');
},
child: const Icon(Icons.copy_rounded, color: Colors.white, size: 20),
)
: const SizedBox.shrink();
}),
// floatingActionButton: Selector<ConversationDetailProvider, int>(
// selector: (context, provider) => provider.selectedTab,
// builder: (context, selectedTab, child) {
// return selectedTab == 0
// ? FloatingActionButton(
// backgroundColor: Colors.black,
// elevation: 8,
// shape: const RoundedRectangleBorder(
// borderRadius: BorderRadius.all(Radius.circular(32)),
// side: BorderSide(color: Colors.grey, width: 1)),
// onPressed: () {
// var provider = Provider.of<ConversationDetailProvider>(context, listen: false);
// Clipboard.setData(ClipboardData(text: provider.conversation.getTranscript(generate: true)));
// ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
// content: Text('Transcript copied to clipboard'),
// duration: Duration(seconds: 1),
// ));
// MixpanelManager().copiedConversationDetails(provider.conversation, source: 'Transcript');
// },
// child: const Icon(Icons.copy_rounded, color: Colors.white, size: 20),
// )
// : const SizedBox.shrink();
// }),
body: Stack(
children: [
Column(
Expand Down Expand Up @@ -221,92 +221,117 @@ class _ConversationDetailPageState extends State<ConversationDetailPage> with Ti
),
],
),
Selector<ConversationDetailProvider, ({bool shouldShow, int count})>(selector: (context, provider) {
return (
count: provider.conversation.unassignedSegmentsLength(),
shouldShow: provider.showUnassignedFloatingButton && (provider.selectedTab == 0),
);
}, builder: (context, value, child) {
if (value.count == 0 || !value.shouldShow) return const SizedBox.shrink();
return Positioned(
bottom: MediaQuery.sizeOf(context).height * 0.06,
left: 86,
child: Material(
elevation: 8,
],
),

bottomNavigationBar: Container(
padding: const EdgeInsets.only(left: 30.0, right: 30, bottom: 50, top: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.0),
color: Colors.grey.shade900,
gradient: LinearGradient(
colors: [Colors.black54, Colors.black.withOpacity(0)],
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
),
),
child:
Selector<ConversationDetailProvider, ({bool shouldShow, int count})>(selector: (context, provider) {
return (
count: provider.conversation.unassignedSegmentsLength(),
shouldShow: provider.showUnassignedFloatingButton && (provider.selectedTab == 0),
);
}, builder: (context, value, child) {
if (value.count == 0 || !value.shouldShow) return const SizedBox.shrink();
return Material(
elevation: 8,
borderRadius: BorderRadius.circular(16),
color: Colors.grey.shade900,
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 4,
horizontal: 12,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: Colors.grey.shade900,
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 14,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
spreadRadius: 1,
blurRadius: 2,
offset: const Offset(0, 1),
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: Colors.grey.shade900,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
spreadRadius: 1,
blurRadius: 2,
offset: const Offset(0, 1),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
InkWell(
onTap: () {
var provider = Provider.of<ConversationDetailProvider>(context, listen: false);
provider.setShowUnassignedFloatingButton(false);
},
child: const Icon(
Icons.close,
color: Colors.white,
),
),
const SizedBox(width: 8),
Text(
"${value.count} unassigned segment${value.count == 1 ? '' : 's'}",
style: const TextStyle(
color: Colors.white,
fontSize: 16,
),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
Row(
children: [
Row(
children: [
InkWell(
onTap: () {
var provider = Provider.of<ConversationDetailProvider>(context, listen: false);
provider.setShowUnassignedFloatingButton(false);
},
child: const Icon(
Icons.close,
color: Colors.white,
),
const SizedBox(width: 8),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white24,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
const SizedBox(width: 8),
Text(
"${value.count} unassigned segment${value.count == 1 ? '' : 's'}",
style: const TextStyle(
color: Colors.white,
fontSize: 16,
),
onPressed: () {
var provider = Provider.of<ConversationDetailProvider>(context, listen: false);
var speakerId = provider.conversation.speakerWithMostUnassignedSegments();
var segmentIdx = provider.conversation.firstSegmentIndexForSpeaker(speakerId);
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.black,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) {
return NameSpeakerBottomSheet(
segmentIdx: segmentIdx,
speakerId: speakerId,
);
},
);
},
child: const Text(
"Tag",
style: TextStyle(
color: Colors.white,
),
],
),
),
//TODO: when we move the copy button to settings, we can add this to give a cleaner look
// Row(
// children: [
// const SizedBox(width: 8),
// ElevatedButton(
// style: ElevatedButton.styleFrom(
// backgroundColor: Colors.white24,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(16),
// ),
// ),
// onPressed: () {
// // Tag action
// },
// child: const Text(
// "Tag",
// style: TextStyle(
// color: Colors.white,
// ),
// ),
// ),
// ],
// ),
],
),
),
],
),
);
}),
],
),
);
}),
),
),
),
Expand Down Expand Up @@ -435,7 +460,7 @@ class TranscriptWidgets extends StatelessWidget {
canDisplaySeconds: provider.canDisplaySeconds,
isConversationDetail: true,
// editSegment: (_) {},
editSegment: (i) {
editSegment: (i, j) {
final connectivityProvider = Provider.of<ConnectivityProvider>(context, listen: false);
if (!connectivityProvider.isConnected) {
ConnectivityProvider.showNoInternetDialog(context);
Expand All @@ -444,14 +469,14 @@ class TranscriptWidgets extends StatelessWidget {
showModalBottomSheet(
context: context,
isScrollControlled: true,
isDismissible: provider.editSegmentLoading ? false : true,
backgroundColor: Colors.black,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(topLeft: Radius.circular(16), topRight: Radius.circular(16)),
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) {
return EditSegmentWidget(
return NameSpeakerBottomSheet(
speakerId: j,
segmentIdx: i,
people: SharedPreferencesUtil().cachedPeople,
);
},
);
Expand Down Expand Up @@ -496,11 +521,6 @@ class EditSegmentWidget extends StatelessWidget {
onPressed: () {
MixpanelManager().unassignedSegment();
provider.unassignConversationTranscriptSegment(provider.conversation.id, segmentIdx);
// setModalState(() {
// personId = null;
// isUserSegment = false;
// });
// setState(() {});
Navigator.pop(context);
},
child: const Text(
Expand All @@ -514,45 +534,6 @@ class EditSegmentWidget extends StatelessWidget {
],
),
),
// !provider.hasAudioRecording ? const SizedBox(height: 12) : const SizedBox(),
// !provider.hasAudioRecording
// ? GestureDetector(
// onTap: () {
// showDialog(
// context: context,
// builder: (c) => getDialog(
// context,
// () => Navigator.pop(context),
// () {
// Navigator.pop(context);
// routeToPage(context, const RecordingsStoragePermission());
// },
// 'Can\'t be used for speech training',
// 'This segment can\'t be used for speech training as there is no audio recording available. Check if you have the required permissions for future memories.',
// okButtonText: 'View',
// ),
// );
// },
// child: Padding(
// padding: const EdgeInsets.symmetric(horizontal: 16.0),
// child: Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// crossAxisAlignment: CrossAxisAlignment.center,
// children: [
// Text('Can\'t be used for speech training',
// style: Theme.of(context)
// .textTheme
// .bodyMedium!
// .copyWith(decoration: TextDecoration.underline)),
// const Padding(
// padding: EdgeInsets.only(right: 12),
// child: Icon(Icons.info, color: Colors.grey, size: 20),
// ),
// ],
// ),
// ),
// )
// : const SizedBox(),
const SizedBox(height: 12),
CheckboxListTile(
title: const Text('Yours'),
Expand Down
Loading

0 comments on commit b5396ae

Please sign in to comment.