* docs: add PT v2 and token-info-feedback design docs - Add phonetic-transcription-v2-design.instructions.md (client PT v2 migration) - Add token-info-feedback-v2.instructions.md (client token feedback v2 migration) * fix: update applyTo path for token info feedback v2 migration * feat: Refactor phonetic transcription to v2 models and repository (in progress) * feat: PT v2 migration - tts_phoneme rename, v1 cleanup, disambiguation, TTS integration * feat: Update phonetic transcription v2 design document for endpoint changes and response structure * docs: fix stale _storageKeys claim in pt-v2 design doc * style: reformat PT v2 files with Dart 3.10 formatter (Flutter 3.38) * feat: add speakingRate to TTS request model (default 0.85) Passes speaking_rate to the choreo TTS endpoint. Default preserves current behavior; can be overridden for single-word playback later. * feat: use normal speed (1.0) for single-word TTS playback The 0.85x slowdown is helpful for full sentences but makes single words sound unnaturally slow. tts_controller._speakFromChoreo now sends speakingRate=1.0. Full-sentence TTS via pangea_message_event still defaults to 0.85. * style: clean up formatting and reduce line breaks in TtsController * fix: env goofiness * formatting, fix linter issues * don't return widgets from functions --------- Co-authored-by: ggurdin <ggurdin@gmail.com> Co-authored-by: ggurdin <46800240+ggurdin@users.noreply.github.com>
150 lines
4.3 KiB
Dart
150 lines
4.3 KiB
Dart
import 'dart:developer';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:fluffychat/config/app_config.dart';
|
|
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
|
import 'package:fluffychat/pangea/common/widgets/shimmer_background.dart';
|
|
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
|
import 'package:fluffychat/pangea/practice_activities/practice_choice.dart';
|
|
import 'package:fluffychat/pangea/text_to_speech/tts_controller.dart';
|
|
import 'package:fluffychat/pangea/toolbar/message_practice/practice_controller.dart';
|
|
import 'package:fluffychat/widgets/matrix.dart';
|
|
|
|
class PracticeMatchItem extends StatefulWidget {
|
|
final Widget content;
|
|
final PangeaToken? token;
|
|
final PracticeChoice constructForm;
|
|
final String? audioContent;
|
|
final PracticeController controller;
|
|
final bool? isCorrect;
|
|
final bool isSelected;
|
|
final bool shimmer;
|
|
|
|
const PracticeMatchItem({
|
|
super.key,
|
|
required this.content,
|
|
required this.token,
|
|
required this.constructForm,
|
|
required this.isCorrect,
|
|
required this.isSelected,
|
|
this.audioContent,
|
|
required this.controller,
|
|
this.shimmer = false,
|
|
});
|
|
|
|
@override
|
|
PracticeMatchItemState createState() => PracticeMatchItemState();
|
|
}
|
|
|
|
class PracticeMatchItemState extends State<PracticeMatchItem> {
|
|
bool _isHovered = false;
|
|
bool _isPlaying = false;
|
|
|
|
bool get isSelected => widget.isSelected;
|
|
|
|
bool? get isCorrect => widget.isCorrect;
|
|
|
|
Future<void> play() async {
|
|
if (widget.audioContent == null) {
|
|
return;
|
|
}
|
|
|
|
if (_isPlaying) {
|
|
await TtsController.stop();
|
|
if (mounted) {
|
|
setState(() => _isPlaying = false);
|
|
}
|
|
} else {
|
|
if (mounted) {
|
|
setState(() => _isPlaying = true);
|
|
}
|
|
try {
|
|
final l2 = MatrixState.pangeaController.userController.userL2Code;
|
|
if (l2 != null) {
|
|
await TtsController.tryToSpeak(
|
|
widget.audioContent!,
|
|
context: context,
|
|
targetID: 'word-audio-button',
|
|
langCode: l2,
|
|
pos: widget.token?.pos,
|
|
morph: widget.token?.morph.map((k, v) => MapEntry(k.name, v)),
|
|
);
|
|
}
|
|
} catch (e, s) {
|
|
debugger(when: kDebugMode);
|
|
ErrorHandler.logError(e: e, s: s, data: {"text": widget.audioContent});
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() => _isPlaying = false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Color color(BuildContext context) {
|
|
if (isCorrect != null) {
|
|
return isCorrect! ? AppConfig.success : AppConfig.warning;
|
|
}
|
|
|
|
if (isSelected) {
|
|
return Theme.of(context).colorScheme.primaryContainer;
|
|
}
|
|
|
|
if (_isHovered) {
|
|
return Theme.of(context).colorScheme.primaryContainer;
|
|
}
|
|
|
|
return Theme.of(context).colorScheme.surface;
|
|
}
|
|
|
|
@override
|
|
didUpdateWidget(PracticeMatchItem oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (oldWidget.isSelected != widget.isSelected ||
|
|
oldWidget.isCorrect != widget.isCorrect) {
|
|
setState(() {});
|
|
}
|
|
}
|
|
|
|
void onTap() {
|
|
play();
|
|
if (isCorrect == null || !isCorrect! || widget.token == null) {
|
|
widget.controller.onChoiceSelect(widget.constructForm);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final content = Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Flexible(
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: color(context).withAlpha((0.4 * 255).toInt()),
|
|
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
|
border: isSelected
|
|
? Border.all(color: color(context).withAlpha(255), width: 2)
|
|
: Border.all(color: Colors.transparent, width: 2),
|
|
),
|
|
child: widget.content,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
|
|
return Draggable<PracticeChoice>(
|
|
data: widget.constructForm,
|
|
feedback: Material(type: MaterialType.transparency, child: content),
|
|
onDragStarted: onTap,
|
|
child: InkWell(
|
|
onHover: (isHovered) => setState(() => _isHovered = isHovered),
|
|
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
|
onTap: onTap,
|
|
child: ShimmerBackground(enabled: widget.shimmer, child: content),
|
|
),
|
|
);
|
|
}
|
|
}
|