Merge pull request #3182 from pangeachat/3168-allow-clicking-of-words-in-transcripts
feat: make tokens in STT transcript clickablepull/2245/head
commit
12e66cfcdc
@ -1,29 +1,85 @@
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
|
||||
class TokenPositionModel {
|
||||
/// Start index of the full substring in the message
|
||||
final int start;
|
||||
class TokenPosition {
|
||||
final PangeaToken? token;
|
||||
final int startIndex;
|
||||
final int endIndex;
|
||||
|
||||
const TokenPosition({
|
||||
this.token,
|
||||
required this.startIndex,
|
||||
required this.endIndex,
|
||||
});
|
||||
}
|
||||
|
||||
/// End index of the full substring in the message
|
||||
final int end;
|
||||
class TokensUtil {
|
||||
static List<TokenPosition> getTokenPositions(
|
||||
List<PangeaToken> tokens,
|
||||
) {
|
||||
final List<TokenPosition> tokenPositions = [];
|
||||
int tokenPointer = 0;
|
||||
int globalPointer = 0;
|
||||
|
||||
/// Start index of the token in the message
|
||||
final int tokenStart;
|
||||
while (tokenPointer < tokens.length) {
|
||||
int endIndex = tokenPointer;
|
||||
PangeaToken token = tokens[tokenPointer];
|
||||
|
||||
/// End index of the token in the message
|
||||
final int tokenEnd;
|
||||
if (token.text.offset > globalPointer) {
|
||||
// If the token starts after the current global pointer, we need to
|
||||
// create a new token position for the gap
|
||||
tokenPositions.add(
|
||||
TokenPosition(
|
||||
startIndex: globalPointer,
|
||||
endIndex: token.text.offset,
|
||||
),
|
||||
);
|
||||
|
||||
final bool selected;
|
||||
final bool hideContent;
|
||||
final PangeaToken? token;
|
||||
globalPointer = token.text.offset;
|
||||
}
|
||||
|
||||
const TokenPositionModel({
|
||||
required this.start,
|
||||
required this.end,
|
||||
required this.tokenStart,
|
||||
required this.tokenEnd,
|
||||
required this.hideContent,
|
||||
required this.selected,
|
||||
this.token,
|
||||
});
|
||||
// move the end index if the next token is right next to the current token
|
||||
// and either the current token is punctuation or the next token is punctuation
|
||||
while (endIndex < tokens.length - 1) {
|
||||
final PangeaToken currentToken = tokens[endIndex];
|
||||
final PangeaToken nextToken = tokens[endIndex + 1];
|
||||
|
||||
final currentIsPunct = currentToken.pos == 'PUNCT' &&
|
||||
currentToken.text.content.trim().isNotEmpty;
|
||||
final nextIsPunct = nextToken.pos == 'PUNCT' &&
|
||||
nextToken.text.content.trim().isNotEmpty;
|
||||
|
||||
if (currentToken.text.offset + currentToken.text.length !=
|
||||
nextToken.text.offset) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ((currentIsPunct && nextIsPunct) ||
|
||||
(currentIsPunct && nextToken.text.content.trim().isNotEmpty) ||
|
||||
(nextIsPunct && currentToken.text.content.trim().isNotEmpty)) {
|
||||
if (token.pos == 'PUNCT' && !nextIsPunct) {
|
||||
token = nextToken;
|
||||
}
|
||||
|
||||
endIndex++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
tokenPositions.add(
|
||||
TokenPosition(
|
||||
token: token,
|
||||
startIndex: tokens[tokenPointer].text.offset,
|
||||
endIndex: tokens[endIndex].text.offset + tokens[endIndex].text.length,
|
||||
),
|
||||
);
|
||||
|
||||
// Move to the next token
|
||||
tokenPointer = tokenPointer + (endIndex - tokenPointer) + 1;
|
||||
globalPointer =
|
||||
tokens[endIndex].text.offset + tokens[endIndex].text.length;
|
||||
}
|
||||
|
||||
return tokenPositions;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,91 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/message_token_text/token_position_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/speech_to_text_models.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class SttTranscriptTokens extends StatelessWidget {
|
||||
final SpeechToTextModel model;
|
||||
final TextStyle? style;
|
||||
|
||||
final void Function(PangeaToken)? onClick;
|
||||
final bool Function(PangeaToken)? isSelected;
|
||||
|
||||
const SttTranscriptTokens({
|
||||
super.key,
|
||||
required this.model,
|
||||
this.onClick,
|
||||
this.isSelected,
|
||||
this.style,
|
||||
});
|
||||
|
||||
List<PangeaToken> get tokens =>
|
||||
model.transcript.sttTokens.map((t) => t.token).toList();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (model.transcript.sttTokens.isEmpty) {
|
||||
return Text(
|
||||
model.transcript.text,
|
||||
style: style ?? DefaultTextStyle.of(context).style,
|
||||
textScaler: TextScaler.noScaling,
|
||||
);
|
||||
}
|
||||
|
||||
final messageCharacters = model.transcript.text.characters;
|
||||
return RichText(
|
||||
textScaler: TextScaler.noScaling,
|
||||
text: TextSpan(
|
||||
style: style ?? DefaultTextStyle.of(context).style,
|
||||
children: TokensUtil.getTokenPositions(tokens).map((tokenPosition) {
|
||||
final text = messageCharacters
|
||||
.skip(tokenPosition.startIndex)
|
||||
.take(tokenPosition.endIndex - tokenPosition.startIndex)
|
||||
.toString();
|
||||
|
||||
if (tokenPosition.token == null) {
|
||||
return TextSpan(
|
||||
text: text,
|
||||
style: style ?? DefaultTextStyle.of(context).style,
|
||||
);
|
||||
}
|
||||
|
||||
final token = tokenPosition.token!;
|
||||
final selected = isSelected?.call(token) ?? false;
|
||||
|
||||
return WidgetSpan(
|
||||
child: CompositedTransformTarget(
|
||||
link: MatrixState.pAnyState
|
||||
.layerLinkAndKey(token.text.uniqueKey)
|
||||
.link,
|
||||
child: MouseRegion(
|
||||
key: MatrixState.pAnyState
|
||||
.layerLinkAndKey(token.text.uniqueKey)
|
||||
.key,
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: onClick != null ? () => onClick?.call(token) : null,
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: text,
|
||||
style: (style ?? DefaultTextStyle.of(context).style)
|
||||
.copyWith(
|
||||
decoration: TextDecoration.underline,
|
||||
decorationThickness: 4,
|
||||
decorationColor: selected
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Colors.white.withAlpha(0),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue