Files
999/lib/screens/provider_bakeoff_screen.dart
T
Alma Alexyz fbf04d90db feat: embed a Flutter G4F client with Cronet, cookie jar, and origin WebView
Port the Oracle off the Python g4f container onto on-device providers: Chromium HTTP via Cronet, a shared cookie session, IterList retry, hosted OpenAI-compat aggregators, and same-origin WebView fallbacks instead of CORS fetch.
2026-09-01 22:05:38 +05:00

339 lines
10 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<ProviderBakeoffScreen> createState() => _ProviderBakeoffScreenState();
}
class _ProviderBakeoffScreenState extends State<ProviderBakeoffScreen> {
static const _question = 'Что скрыто в моём нынешнем выборе?';
late final List<SessionWord> _words;
late final List<Message> _messages;
late final List<ProviderProbeRow> _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<SessionWord> _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<void> _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<void> _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)),
],
),
);
}
}