import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import '../data/words_data.dart'; import '../g4f/g4f.dart'; import '../models/history_entry_model.dart'; import '../models/word_model.dart'; import '../services/ai_oracle_service.dart'; class ProviderProbeRow { ProviderProbeRow({ required this.provider, this.running = false, this.ok, this.text = '', this.error, this.elapsedMs = 0, }); final BaseProvider provider; bool running; bool? ok; String text; String? error; int elapsedMs; String get name => provider.name; bool get isWebView => provider.isWebView; } /// On-device comparison: the real Oracle prompt, every G4F provider, Android process. class ProviderBakeoffScreen extends StatefulWidget { const ProviderBakeoffScreen({super.key, this.autoStart = true}); final bool autoStart; @override State createState() => _ProviderBakeoffScreenState(); } class _ProviderBakeoffScreenState extends State { static const _question = 'Что скрыто в моём нынешнем выборе?'; late final List _words; late final List _messages; late final List _rows; bool _running = false; bool _done = false; @override void initState() { super.initState(); _words = _pickWords(); _messages = AiOracleService().buildInterpretationMessages( words: _words, question: _question, ); _rows = [ for (final p in G4FClient().chat.completions.providers) if (p.working) ProviderProbeRow(provider: p), ]; if (widget.autoStart) { WidgetsBinding.instance.addPostFrameCallback((_) => _run()); } } List _pickWords() { WordItem byName(String name) => kRussianTop1000Words.firstWhere( (w) => w.word == name, ); final items = [ byName('огонь').copyWith(sessionNumber: 17), byName('зеркало').copyWith(sessionNumber: 92), byName('порог').copyWith(sessionNumber: 112), ]; return [ for (final item in items) SessionWord.fromWordItem(item), ]; } Future _run() async { if (_running) return; setState(() { _running = true; _done = false; for (final row in _rows) { row.running = false; row.ok = null; row.text = ''; row.error = null; row.elapsedMs = 0; } }); _log('BEGIN question=$_question words=${_words.map((w) => w.word).join(",")}'); for (final row in _rows) { if (!mounted) return; setState(() => row.running = true); _log('START ${row.name} webview=${row.isWebView}'); final sw = Stopwatch()..start(); try { final model = row.provider.models.contains(G4FModels.gpt4o) ? G4FModels.gpt4o : row.provider.defaultModel; final text = await row.provider.complete(model, _messages); sw.stop(); final ok = looksLikeModelOutput(text) && text.trim().length >= 2; row ..ok = ok ..text = text.trim() ..error = ok ? null : 'unusable' ..elapsedMs = sw.elapsedMilliseconds ..running = false; _log( 'DONE ${row.name} ok=$ok ms=${row.elapsedMs} chars=${row.text.length}', ); _log('TEXT ${row.name} ${jsonEncode(row.text)}'); } catch (e) { sw.stop(); row ..ok = false ..error = e.toString() ..elapsedMs = sw.elapsedMilliseconds ..running = false; _log('FAIL ${row.name} ms=${row.elapsedMs} $e'); } if (mounted) setState(() {}); } await _writeReport(); _log('END ok=${_rows.where((r) => r.ok == true).length}/${_rows.length}'); if (mounted) { setState(() { _running = false; _done = true; }); } } Future _writeReport() async { final report = { 'platform': Platform.operatingSystem, 'question': _question, 'words': [ for (final w in _words) { 'word': w.word, 'sessionNumber': w.sessionNumber, 'category': w.category, 'symbolicMeaning': w.symbolicMeaning, } ], 'results': [ for (final r in _rows) { 'name': r.name, 'webview': r.isWebView, 'ok': r.ok, 'ms': r.elapsedMs, 'error': r.error, 'text': r.text, } ], }; final encoded = const JsonEncoder.withIndent(' ').convert(report); _log('REPORT $encoded'); try { final file = File('/data/data/com.example.words999.words999/cache/oracle_bakeoff.json'); await file.parent.create(recursive: true); await file.writeAsString(encoded); _log('WROTE ${file.path}'); } catch (e) { _log('WRITE_FAIL $e'); } } void _log(String line) { debugPrint('ORACLE_BAKEOFF|$line'); } @override Widget build(BuildContext context) { final okCount = _rows.where((r) => r.ok == true).length; return Scaffold( backgroundColor: const Color(0xFF0C0E1A), appBar: AppBar( title: const Text('Сравнение провайдеров'), actions: [ if (!_running) IconButton( onPressed: _run, icon: const Icon(Icons.refresh), tooltip: 'Запустить снова', ), ], ), body: ListView( padding: const EdgeInsets.fromLTRB(16, 12, 16, 32), children: [ _headerCard(okCount), const SizedBox(height: 12), for (final row in _rows) ...[ _resultCard(row), const SizedBox(height: 10), ], ], ), ); } Widget _headerCard(int okCount) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF151829), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.25)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _running ? 'Идёт опрос на Android…' : _done ? 'Готово: $okCount из ${_rows.length} ответили' : 'Ожидание запуска', style: const TextStyle( color: Color(0xFFFFD54F), fontSize: 16, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 8), Text( 'Вопрос: «$_question»', style: const TextStyle(color: Color(0xFFE8EAF6), height: 1.35), ), const SizedBox(height: 8), Text( 'Знаки: ${_words.map((w) => '«${w.word.toUpperCase()}» (#${w.formattedSessionNumber})').join(' → ')}', style: const TextStyle(color: Color(0xFF80DEEA), height: 1.35), ), const SizedBox(height: 6), const Text( 'Тот же system-промпт и user-промпт, что у interpretSession. Каждый провайдер вызывается отдельно, без запасного переключения.', style: TextStyle(color: Color(0xFF8C93B8), fontSize: 12, height: 1.35), ), if (_running) const Padding( padding: EdgeInsets.only(top: 12), child: LinearProgressIndicator(color: Color(0xFFFFD54F)), ), ], ), ); } Widget _resultCard(ProviderProbeRow row) { final Color accent; if (row.running) { accent = const Color(0xFF80DEEA); } else if (row.ok == true) { accent = const Color(0xFF81C784); } else if (row.ok == false) { accent = const Color(0xFFE57373); } else { accent = const Color(0xFF5C638A); } return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color(0xFF151829), borderRadius: BorderRadius.circular(14), border: Border.all(color: accent.withValues(alpha: 0.45)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( row.running ? Icons.hourglass_top : row.ok == true ? Icons.check_circle : row.ok == false ? Icons.cancel : Icons.circle_outlined, color: accent, size: 18, ), const SizedBox(width: 8), Expanded( child: Text( row.name, style: const TextStyle( color: Color(0xFFFFF9C4), fontWeight: FontWeight.w700, ), ), ), Text( row.isWebView ? 'WebView' : 'REST', style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 11), ), if (row.elapsedMs > 0) ...[ const SizedBox(width: 8), Text( '${(row.elapsedMs / 1000).toStringAsFixed(1)}с', style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 11), ), ], ], ), const SizedBox(height: 8), if (row.running) const Text('Ждём полный ответ…', style: TextStyle(color: Color(0xFF80DEEA))) else if (row.ok == true) SelectableText( row.text, style: const TextStyle(color: Color(0xFFE8EAF6), height: 1.4, fontSize: 14), ) else if (row.ok == false) Text( row.error ?? 'ошибка', style: const TextStyle(color: Color(0xFFE57373), fontSize: 12, height: 1.35), ) else const Text('в очереди', style: TextStyle(color: Color(0xFF5C638A), fontSize: 12)), ], ), ); } }