feat: session start screen, question context input, and G4F-style AI multi-provider fallback

This commit is contained in:
Alma Alexyz
2026-08-31 13:43:45 +05:00
parent c99e36e9ac
commit a24fdaee97
11 changed files with 857 additions and 99 deletions
+85 -12
View File
@@ -15,10 +15,13 @@ class OracleSpreadScreen extends StatefulWidget {
class _OracleSpreadScreenState extends State<OracleSpreadScreen> {
final _repo = WordsRepository();
final _questionController = TextEditingController();
int _selectedMode = 0; // 0 = Single Card (Жребий момента), 1 = Triad (3 карты)
WordItem? _singleDraw;
String? _singleHistoryId;
List<WordItem> _triadDraw = [];
List<String> _triadHistoryIds = [];
bool _isGenerating = false;
@override
@@ -27,35 +30,47 @@ class _OracleSpreadScreenState extends State<OracleSpreadScreen> {
_singleDraw = _repo.getRandomWord();
}
@override
void dispose() {
_questionController.dispose();
super.dispose();
}
Future<void> _performSingleDraw({bool saveHistory = true}) async {
setState(() => _isGenerating = true);
HapticFeedback.mediumImpact();
await Future.delayed(const Duration(milliseconds: 250));
await Future.delayed(const Duration(milliseconds: 200));
final word = _repo.getRandomWord();
setState(() {
_singleDraw = word;
_isGenerating = false;
});
String? histId;
if (saveHistory) {
histId = DateTime.now().millisecondsSinceEpoch.toString();
final question = _questionController.text.trim();
final entry = HistoryEntry(
id: DateTime.now().millisecondsSinceEpoch.toString(),
id: histId,
timestamp: DateTime.now(),
word: word.word,
sessionNumber: word.sessionNumber,
partOfSpeech: word.partOfSpeech,
symbolicMeaning: word.description,
readingType: 'Жребий момента (1 символ)',
question: question.isNotEmpty ? question : null,
);
await _repo.addHistoryEntry(entry);
}
setState(() {
_singleDraw = word;
_singleHistoryId = histId ?? _singleHistoryId;
_isGenerating = false;
});
}
Future<void> _performTriadDraw() async {
setState(() => _isGenerating = true);
HapticFeedback.heavyImpact();
await Future.delayed(const Duration(milliseconds: 300));
await Future.delayed(const Duration(milliseconds: 250));
final words = <WordItem>[];
final titles = [
@@ -71,14 +86,14 @@ class _OracleSpreadScreenState extends State<OracleSpreadScreen> {
}
}
setState(() {
_triadDraw = words;
_isGenerating = false;
});
final question = _questionController.text.trim();
final ids = <String>[];
for (int i = 0; i < words.length; i++) {
final histId = '${DateTime.now().millisecondsSinceEpoch}_$i';
ids.add(histId);
final entry = HistoryEntry(
id: '${DateTime.now().millisecondsSinceEpoch}_$i',
id: histId,
timestamp: DateTime.now(),
word: words[i].word,
sessionNumber: words[i].sessionNumber,
@@ -86,9 +101,16 @@ class _OracleSpreadScreenState extends State<OracleSpreadScreen> {
symbolicMeaning: words[i].description,
readingType: 'Триада судьбы',
positionTitle: titles[i],
question: question.isNotEmpty ? question : null,
);
await _repo.addHistoryEntry(entry);
}
setState(() {
_triadDraw = words;
_triadHistoryIds = ids;
_isGenerating = false;
});
}
@override
@@ -115,6 +137,45 @@ class _OracleSpreadScreenState extends State<OracleSpreadScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Question Input
TextField(
controller: _questionController,
decoration: InputDecoration(
labelText: 'Ваш вопрос к раскладу (необязательно)',
hintText: 'Например: Чему учит эта ситуация?',
prefixIcon: const Icon(Icons.help_outline_rounded, color: Color(0xFFFFD54F), size: 20),
suffixIcon: _questionController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear, size: 18),
onPressed: () => _questionController.clear(),
)
: null,
filled: true,
fillColor: isDark ? const Color(0xFF17192C) : theme.colorScheme.surfaceContainerHigh,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(
color: const Color(0xFFFFD54F).withValues(alpha: 0.25),
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(
color: const Color(0xFFFFD54F).withValues(alpha: 0.2),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(
color: Color(0xFFFFD54F),
width: 1.5,
),
),
),
),
const SizedBox(height: 12),
// Mode Selector (1 символ / Триада 3 символа)
Container(
padding: const EdgeInsets.all(4),
@@ -264,26 +325,38 @@ class _OracleSpreadScreenState extends State<OracleSpreadScreen> {
)
else if (_selectedMode == 0 && _singleDraw != null)
WordCardWidget(
key: ValueKey('${_singleDraw!.id}_${_singleDraw!.sessionNumber}'),
word: _singleDraw!,
positionTitle: 'Символ момента',
question: _questionController.text.trim(),
historyEntryId: _singleHistoryId,
onRandom: () => _performSingleDraw(saveHistory: true),
)
else if (_selectedMode == 1 && _triadDraw.isNotEmpty)
Column(
children: [
WordCardWidget(
key: ValueKey('triad_0_${_triadDraw[0].id}_${_triadDraw[0].sessionNumber}'),
word: _triadDraw[0],
positionTitle: '1. Истоки / Скрытая причина',
question: _questionController.text.trim(),
historyEntryId: _triadHistoryIds.isNotEmpty ? _triadHistoryIds[0] : null,
),
const SizedBox(height: 14),
WordCardWidget(
key: ValueKey('triad_1_${_triadDraw[1].id}_${_triadDraw[1].sessionNumber}'),
word: _triadDraw[1],
positionTitle: '2. Настоящее / Вызов',
question: _questionController.text.trim(),
historyEntryId: _triadHistoryIds.length > 1 ? _triadHistoryIds[1] : null,
),
const SizedBox(height: 14),
WordCardWidget(
key: ValueKey('triad_2_${_triadDraw[2].id}_${_triadDraw[2].sessionNumber}'),
word: _triadDraw[2],
positionTitle: '3. Совет / Исход',
question: _questionController.text.trim(),
historyEntryId: _triadHistoryIds.length > 2 ? _triadHistoryIds[2] : null,
),
],
),