Files
999/lib/screens/geomancy_divination_screen.dart

1732 lines
65 KiB
Dart
Raw Permalink 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:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/divination_layout.dart';
import '../models/history_entry_model.dart';
import '../models/syncretic_session_model.dart';
import '../models/tradition_model.dart';
import '../models/word_model.dart';
import '../services/ai_oracle_service.dart';
import '../services/traditions_repository.dart';
import '../services/words_repository.dart';
import '../widgets/divination_instruction_modal.dart';
import 'symbol_detail_screen.dart';
import '../services/oracle_master_prompt_generator.dart';
class GeomancyFigure {
final String id;
final String fullName;
final String latinName;
final String arabicName;
final String russianName;
final List<int> lines; // 4 rows: 1 or 2 dots (Fire, Air, Water, Earth)
final String element;
final String planet;
final String group;
final String lore;
final int? primaryWordId;
final TraditionStation station;
const GeomancyFigure({
required this.id,
required this.fullName,
required this.latinName,
required this.arabicName,
required this.russianName,
required this.lines,
required this.element,
required this.planet,
required this.group,
required this.lore,
this.primaryWordId,
required this.station,
});
String get signature => lines.join(',');
int get totalDots => lines.reduce((a, b) => a + b);
bool get isEven => totalDots % 2 == 0;
static List<int> combineLines(List<int> a, List<int> b) {
return List.generate(4, (i) => (a[i] + b[i]) % 2 == 0 ? 2 : 1);
}
}
class GeomancyDivinationScreen extends StatefulWidget {
final String? initialQuestion;
const GeomancyDivinationScreen({super.key, this.initialQuestion});
@override
State<GeomancyDivinationScreen> createState() => _GeomancyDivinationScreenState();
}
class _GeomancyDivinationScreenState extends State<GeomancyDivinationScreen> with SingleTickerProviderStateMixin {
late TabController _tabController;
final _questionController = TextEditingController();
final _repo = WordsRepository();
final _aiService = AiOracleService();
List<GeomancyFigure> _allFigures = [];
final Map<String, GeomancyFigure> _figureBySig = {};
DivinationLayout _selectedLayout = DivinationLayouts.geomancyLayouts[0];
// Sand animation / casting state
bool _isCasting = false;
List<int> _currentSandDotCounts = [0, 0, 0, 0];
// Drawn results for 1-figure and 3-figure layouts
final List<({GeomancyFigure figure, String positionTitle})> _drawnResults = [];
// Shield state (4 Mothers cast, others derived)
final List<GeomancyFigure> _shieldMothers = [];
List<GeomancyFigure> _shieldDaughters = [];
List<GeomancyFigure> _shieldNephews = [];
GeomancyFigure? _shieldRightWitness;
GeomancyFigure? _shieldLeftWitness;
GeomancyFigure? _shieldJudge;
GeomancyFigure? _shieldReconciler;
GeomancyFigure? _inspectedShieldFigure;
String? _inspectedShieldTitle;
bool _savedToSession = false;
bool _isAiLoading = false;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
if (widget.initialQuestion != null) {
_questionController.text = widget.initialQuestion!;
} else {
final q = _repo.currentSyncreticSession.mainQuestion;
if (q != null && q.trim().isNotEmpty) {
_questionController.text = q.trim();
}
}
_loadGeomancyFigures();
}
void _loadGeomancyFigures() {
final canon = TraditionsRepository().byId('geomancy');
if (canon == null) return;
final parsed = <GeomancyFigure>[];
for (final st in canon.stations) {
final rawLines = st.symbol.split(RegExp(r'[\r\n]+'));
final lines = <int>[];
for (final l in rawLines) {
final clean = l.replaceAll(' ', '').trim();
if (clean.isNotEmpty) {
lines.add(clean.length);
}
}
if (lines.length != 4) continue;
String latin = st.id;
String arabic = '';
String russian = '';
final m = RegExp(r'^\d*\s*([A-Za-z\s]+)\s*\(([^/]+)/([^)]+)\)').firstMatch(st.name);
if (m != null) {
latin = m.group(1)?.trim() ?? st.id;
arabic = m.group(2)?.trim() ?? '';
russian = m.group(3)?.trim() ?? '';
} else {
latin = st.name;
}
final parts = st.group.split('·').map((p) => p.trim()).toList();
final element = parts.length > 1 ? parts[1] : 'Земля';
final planet = parts.length > 2 ? parts[2] : 'Марс';
final fig = GeomancyFigure(
id: st.id,
fullName: st.name,
latinName: latin,
arabicName: arabic,
russianName: russian.isNotEmpty ? russian : latin,
lines: lines,
element: element,
planet: planet,
group: st.group,
lore: st.lore,
primaryWordId: st.primary?.id,
station: st,
);
parsed.add(fig);
_figureBySig[fig.signature] = fig;
}
setState(() {
_allFigures = parsed;
});
}
@override
void dispose() {
_tabController.dispose();
_questionController.dispose();
super.dispose();
}
void _onLayoutChanged(DivinationLayout layout) {
setState(() {
_selectedLayout = layout;
_reset();
});
HapticFeedback.selectionClick();
}
void _reset() {
setState(() {
_drawnResults.clear();
_shieldMothers.clear();
_shieldDaughters.clear();
_shieldNephews.clear();
_shieldRightWitness = null;
_shieldLeftWitness = null;
_shieldJudge = null;
_shieldReconciler = null;
_inspectedShieldFigure = null;
_inspectedShieldTitle = null;
_savedToSession = false;
_currentSandDotCounts = [0, 0, 0, 0];
});
}
void _deriveShield() {
if (_shieldMothers.length != 4) return;
final m1 = _shieldMothers[0];
final m2 = _shieldMothers[1];
final m3 = _shieldMothers[2];
final m4 = _shieldMothers[3];
// 4 Daughters:
final d1Sig = [m1.lines[0], m2.lines[0], m3.lines[0], m4.lines[0]].join(',');
final d2Sig = [m1.lines[1], m2.lines[1], m3.lines[1], m4.lines[1]].join(',');
final d3Sig = [m1.lines[2], m2.lines[2], m3.lines[2], m4.lines[2]].join(',');
final d4Sig = [m1.lines[3], m2.lines[3], m3.lines[3], m4.lines[3]].join(',');
final d1 = _figureBySig[d1Sig] ?? m1;
final d2 = _figureBySig[d2Sig] ?? m2;
final d3 = _figureBySig[d3Sig] ?? m3;
final d4 = _figureBySig[d4Sig] ?? m4;
_shieldDaughters = [d1, d2, d3, d4];
// 4 Nephews:
// N1 = M1 + M2, N2 = M3 + M4, N3 = D1 + D2, N4 = D3 + D4
final n1Sig = GeomancyFigure.combineLines(m1.lines, m2.lines).join(',');
final n2Sig = GeomancyFigure.combineLines(m3.lines, m4.lines).join(',');
final n3Sig = GeomancyFigure.combineLines(d1.lines, d2.lines).join(',');
final n4Sig = GeomancyFigure.combineLines(d3.lines, d4.lines).join(',');
final n1 = _figureBySig[n1Sig] ?? m1;
final n2 = _figureBySig[n2Sig] ?? m2;
final n3 = _figureBySig[n3Sig] ?? d1;
final n4 = _figureBySig[n4Sig] ?? d2;
_shieldNephews = [n1, n2, n3, n4];
// 2 Witnesses:
// Right Witness (W1) = N1 + N2 (outer situation/querent's question)
// Left Witness (W2) = N3 + N4 (inner soul/querent themselves)
final w1Sig = GeomancyFigure.combineLines(n1.lines, n2.lines).join(',');
final w2Sig = GeomancyFigure.combineLines(n3.lines, n4.lines).join(',');
_shieldRightWitness = _figureBySig[w1Sig] ?? n1;
_shieldLeftWitness = _figureBySig[w2Sig] ?? n2;
// Judge = W1 + W2
final jSig = GeomancyFigure.combineLines(_shieldRightWitness!.lines, _shieldLeftWitness!.lines).join(',');
_shieldJudge = _figureBySig[jSig] ?? _shieldRightWitness!;
// Reconciler = Judge + M1
final rSig = GeomancyFigure.combineLines(_shieldJudge!.lines, m1.lines).join(',');
_shieldReconciler = _figureBySig[rSig] ?? _shieldJudge!;
_inspectedShieldFigure = _shieldJudge;
_inspectedShieldTitle = 'Судья (Итоговое Разрешение)';
}
void _addFigureToSpread(GeomancyFigure fig) {
if (_selectedLayout.id == 'geomancy_shield') {
if (_shieldMothers.length < 4) {
final completed = (_shieldMothers.length + 1 == 4);
setState(() {
_shieldMothers.add(fig);
if (completed) {
_deriveShield();
}
});
if (completed) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_saveToSyncreticSession(silent: false);
});
}
}
} else {
if (_drawnResults.length < _selectedLayout.count) {
final completed = (_drawnResults.length + 1 == _selectedLayout.count);
final posTitle = _selectedLayout.positionTitles[_drawnResults.length];
setState(() {
_drawnResults.add((figure: fig, positionTitle: posTitle));
});
if (completed) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_saveToSyncreticSession(silent: false);
});
}
}
}
}
Future<void> _performSandRitualCast() async {
if (_isCasting) return;
final isShield = _selectedLayout.id == 'geomancy_shield';
final isComplete = isShield ? _shieldMothers.length >= 4 : _drawnResults.length >= _selectedLayout.count;
if (isComplete) return;
setState(() {
_isCasting = true;
_currentSandDotCounts = [0, 0, 0, 0];
});
final rng = Random();
final counts = <int>[];
final lines = <int>[];
for (int row = 0; row < 4; row++) {
HapticFeedback.lightImpact();
final dotCount = 7 + rng.nextInt(15);
counts.add(dotCount);
lines.add(dotCount % 2 == 1 ? 1 : 2);
setState(() {
_currentSandDotCounts = List.from(counts)..addAll(List.filled(4 - counts.length, 0));
});
await Future.delayed(const Duration(milliseconds: 140));
}
final sig = lines.join(',');
final fig = _figureBySig[sig] ?? _allFigures[rng.nextInt(_allFigures.length)];
HapticFeedback.mediumImpact();
setState(() {
_isCasting = false;
_addFigureToSpread(fig);
});
}
Future<void> _castAllRemainingSteps() async {
final isShield = _selectedLayout.id == 'geomancy_shield';
final target = isShield ? 4 : _selectedLayout.count;
final current = isShield ? _shieldMothers.length : _drawnResults.length;
final remaining = target - current;
for (int i = 0; i < remaining; i++) {
await _performSandRitualCast();
await Future.delayed(const Duration(milliseconds: 120));
}
}
WordItem? _resolveLinkedWord(int? wordId) {
if (wordId == null) return null;
return _repo.allWords.firstWhere(
(w) => w.sessionNumber == wordId,
orElse: () => _repo.allWords.firstWhere(
(w) => w.id == wordId,
orElse: () => _repo.allWords.first,
),
);
}
Future<void> _saveToSyncreticSession({bool silent = false}) async {
final isShield = _selectedLayout.id == 'geomancy_shield';
final items = <ReadingSignItem>[];
if (isShield) {
if (_shieldMothers.length < 4 || _shieldJudge == null) return;
for (int i = 0; i < 4; i++) {
final m = _shieldMothers[i];
final word = _resolveLinkedWord(m.primaryWordId);
items.add(ReadingSignItem(
title: '${i + 1}-я Мать: ${m.fullName}',
subtitle: '${m.element} • ${m.planet}',
symbol: m.station.symbol,
lore: m.lore,
linkedWord: word,
));
}
for (int i = 0; i < 4; i++) {
final d = _shieldDaughters[i];
final word = _resolveLinkedWord(d.primaryWordId);
items.add(ReadingSignItem(
title: '${i + 1}-я Дочь: ${d.fullName}',
subtitle: '${d.element} • ${d.planet}',
symbol: d.station.symbol,
lore: d.lore,
linkedWord: word,
));
}
for (int i = 0; i < 4; i++) {
final n = _shieldNephews[i];
final word = _resolveLinkedWord(n.primaryWordId);
items.add(ReadingSignItem(
title: '${i + 1}-й Племянник: ${n.fullName}',
subtitle: '${n.element} • ${n.planet}',
symbol: n.station.symbol,
lore: n.lore,
linkedWord: word,
));
}
if (_shieldRightWitness != null) {
items.add(ReadingSignItem(
title: 'Правый Свидетель (Внешнее): ${_shieldRightWitness!.fullName}',
subtitle: '${_shieldRightWitness!.element} • ${_shieldRightWitness!.planet}',
symbol: _shieldRightWitness!.station.symbol,
lore: _shieldRightWitness!.lore,
linkedWord: _resolveLinkedWord(_shieldRightWitness!.primaryWordId),
));
}
if (_shieldLeftWitness != null) {
items.add(ReadingSignItem(
title: 'Левый Свидетель (Внутреннее): ${_shieldLeftWitness!.fullName}',
subtitle: '${_shieldLeftWitness!.element} • ${_shieldLeftWitness!.planet}',
symbol: _shieldLeftWitness!.station.symbol,
lore: _shieldLeftWitness!.lore,
linkedWord: _resolveLinkedWord(_shieldLeftWitness!.primaryWordId),
));
}
items.add(ReadingSignItem(
title: 'СУДЬЯ (Итог): ${_shieldJudge!.fullName}',
subtitle: '${_shieldJudge!.element} • ${_shieldJudge!.planet}',
symbol: _shieldJudge!.station.symbol,
lore: _shieldJudge!.lore,
linkedWord: _resolveLinkedWord(_shieldJudge!.primaryWordId),
));
if (_shieldReconciler != null) {
items.add(ReadingSignItem(
title: 'Примиритель: ${_shieldReconciler!.fullName}',
subtitle: '${_shieldReconciler!.element} • ${_shieldReconciler!.planet}',
symbol: _shieldReconciler!.station.symbol,
lore: _shieldReconciler!.lore,
linkedWord: _resolveLinkedWord(_shieldReconciler!.primaryWordId),
));
}
} else {
for (final r in _drawnResults) {
final word = _resolveLinkedWord(r.figure.primaryWordId);
items.add(ReadingSignItem(
title: '${r.positionTitle}: ${r.figure.fullName}',
subtitle: '${r.figure.element} • ${r.figure.planet}',
symbol: r.figure.station.symbol,
lore: r.figure.lore,
linkedWord: word,
));
}
}
final reading = SyncreticReading(
id: 'reading_${DateTime.now().millisecondsSinceEpoch}',
systemId: 'geomancy',
systemName: "Арабская Геомантия ('Илм ар-рамл)",
systemIcon: '⌛',
timestamp: DateTime.now(),
layoutTitle: _selectedLayout.title,
inputMethod: _tabController.index == 0 ? 'sand_ritual' : 'manual_selection',
items: items,
question: _questionController.text.trim(),
);
await _repo.addReadingToSyncreticSession(reading, question: _questionController.text.trim());
if (mounted) {
setState(() => _savedToSession = true);
if (!silent) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(isShield
? 'Великий Щит Геомантии (16 фигур) сохранён в сессию дня!'
: 'Расклад геомантии (${items.length} фигур) сохранён в сессию дня!'),
backgroundColor: const Color(0xFF2E7D32),
behavior: SnackBarBehavior.floating,
duration: const Duration(seconds: 3),
),
);
}
}
}
void _copySpreadSummary() {
final buffer = StringBuffer();
buffer.writeln("=== АРАБСКАЯ ГЕОМАНТИЯ ('ИЛМ АР-РАМЛ) ===");
if (_questionController.text.trim().isNotEmpty) {
buffer.writeln('Вопрос: «${_questionController.text.trim()}»');
}
buffer.writeln('Расклад: ${_selectedLayout.title}\n');
final isShield = _selectedLayout.id == 'geomancy_shield';
if (isShield) {
buffer.writeln('СУДЬЯ: ${_shieldJudge?.fullName ?? "—"}');
buffer.writeln('ПРИМИРИТЕЛЬ: ${_shieldReconciler?.fullName ?? "—"}');
buffer.writeln('Правый свидетель (Внешнее): ${_shieldRightWitness?.fullName ?? "—"}');
buffer.writeln('Левый свидетель (Внутреннее): ${_shieldLeftWitness?.fullName ?? "—"}\n');
buffer.writeln('4 МАТЕРИ:');
for (int i = 0; i < _shieldMothers.length; i++) {
buffer.writeln(' М${i + 1}: ${_shieldMothers[i].fullName}');
}
buffer.writeln('\n4 ДОЧЕРИ:');
for (int i = 0; i < _shieldDaughters.length; i++) {
buffer.writeln(' Д${i + 1}: ${_shieldDaughters[i].fullName}');
}
buffer.writeln('\n4 ПЛЕМЯННИКА:');
for (int i = 0; i < _shieldNephews.length; i++) {
buffer.writeln(' П${i + 1}: ${_shieldNephews[i].fullName}');
}
} else {
for (final r in _drawnResults) {
buffer.writeln('${r.positionTitle}: ${r.figure.fullName} (${r.figure.element} • ${r.figure.planet})');
buffer.writeln(' Смысл: ${r.figure.lore}');
}
}
Clipboard.setData(ClipboardData(text: buffer.toString()));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Расклад геомантии скопирован в буфер обмена!'),
behavior: SnackBarBehavior.floating,
),
);
}
String _buildMasterPrompt() {
final isShield = _selectedLayout.id == 'geomancy_shield';
if (isShield) {
GeomancyPromptFigure toPromptFigure(GeomancyFigure f, String pos) {
return GeomancyPromptFigure(
position: pos,
name: f.fullName,
latinName: f.latinName,
arabicName: f.arabicName,
lines: f.lines,
element: f.element,
planet: f.planet,
lore: f.lore,
linkedWord: _resolveLinkedWord(f.primaryWordId),
);
}
return OracleMasterPromptGenerator.generateGeomancyMasterPrompt(
question: _questionController.text.trim(),
layoutTitle: _selectedLayout.title,
isShield: true,
mothers: List.generate(
_shieldMothers.length,
(i) => toPromptFigure(_shieldMothers[i], 'Мать #' + (i + 1).toString()),
),
daughters: List.generate(
_shieldDaughters.length,
(i) => toPromptFigure(_shieldDaughters[i], 'Дочь #' + (i + 1).toString()),
),
nephews: List.generate(
_shieldNephews.length,
(i) => toPromptFigure(_shieldNephews[i], 'Племянник #' + (i + 1).toString()),
),
rightWitness: _shieldRightWitness != null
? toPromptFigure(_shieldRightWitness!, 'Правый Свидетель')
: null,
leftWitness: _shieldLeftWitness != null
? toPromptFigure(_shieldLeftWitness!, 'Левый Свидетель')
: null,
judge: _shieldJudge != null ? toPromptFigure(_shieldJudge!, 'Верховный Судья') : null,
reconciler: _shieldReconciler != null
? toPromptFigure(_shieldReconciler!, 'Примиритель')
: null,
);
} else {
final figures = _drawnResults.map((r) {
return GeomancyPromptFigure(
position: r.positionTitle,
name: r.figure.fullName,
latinName: r.figure.latinName,
arabicName: r.figure.arabicName,
lines: r.figure.lines,
element: r.figure.element,
planet: r.figure.planet,
lore: r.figure.lore,
linkedWord: _resolveLinkedWord(r.figure.primaryWordId),
);
}).toList();
return OracleMasterPromptGenerator.generateGeomancyMasterPrompt(
question: _questionController.text.trim(),
layoutTitle: _selectedLayout.title,
isShield: false,
figures: figures,
);
}
}
void _showMasterPromptModal() {
final prompt = _buildMasterPrompt();
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: const Color(0xFF100E19),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
builder: (ctx) => DraggableScrollableSheet(
initialChildSize: 0.8,
minChildSize: 0.4,
maxChildSize: 0.95,
expand: false,
builder: (_, scrollController) => Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Container(
width: 40,
height: 4,
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: const Color(0xFFFFD54F).withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(2),
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Row(
children: [
Icon(Icons.psychology_alt_rounded, color: Color(0xFFFFD54F), size: 24),
SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Мастер-Промпт Геомантии ('Илм ар-рамл)",
style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
),
Text(
'Для Grok, Claude 3.5 Sonnet, Gemini 1.5 Pro, GPT-4o',
style: TextStyle(color: Color(0xFF90A4AE), fontSize: 11),
),
],
),
],
),
IconButton(
icon: const Icon(Icons.close_rounded, color: Color(0xFF90A4AE)),
onPressed: () => Navigator.pop(ctx),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFF2A2338),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'${prompt.length} символов • ${prompt.split(RegExp(r"\s+")).length} слов',
style: const TextStyle(color: Color(0xFFFFD54F), fontSize: 11, fontWeight: FontWeight.bold),
),
),
const Spacer(),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFFFD54F),
foregroundColor: Colors.black,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
),
onPressed: () {
Clipboard.setData(ClipboardData(text: prompt));
HapticFeedback.heavyImpact();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Мастер-промпт геомантии скопирован! Вставьте в Grok или Claude.'),
backgroundColor: Color(0xFF388E3C),
behavior: SnackBarBehavior.floating,
),
);
},
icon: const Icon(Icons.copy_rounded, size: 16),
label: const Text('Скопировать всё', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12.5)),
),
],
),
const SizedBox(height: 12),
Expanded(
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF07060B),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF2A2338), width: 1.0),
),
child: SingleChildScrollView(
controller: scrollController,
child: SelectableText(
prompt,
style: const TextStyle(
color: Color(0xFFECEFF1),
fontFamily: 'monospace',
fontSize: 12.5,
height: 1.45,
),
),
),
),
),
],
),
),
),
);
}
Future<void> _requestAiInterpretation() async {
final isShield = _selectedLayout.id == 'geomancy_shield';
final words = <SessionWord>[];
if (isShield) {
if (_shieldJudge == null) return;
final jWord = _resolveLinkedWord(_shieldJudge!.primaryWordId);
if (jWord != null) {
words.add(SessionWord.fromWordItem(jWord, positionTitle: 'Судья: ${_shieldJudge!.fullName}'));
}
if (_shieldRightWitness != null) {
final wWord = _resolveLinkedWord(_shieldRightWitness!.primaryWordId);
if (wWord != null) {
words.add(SessionWord.fromWordItem(wWord, positionTitle: 'Правый Свидетель: ${_shieldRightWitness!.fullName}'));
}
}
if (_shieldLeftWitness != null) {
final wWord = _resolveLinkedWord(_shieldLeftWitness!.primaryWordId);
if (wWord != null) {
words.add(SessionWord.fromWordItem(wWord, positionTitle: 'Левый Свидетель: ${_shieldLeftWitness!.fullName}'));
}
}
for (int i = 0; i < _shieldMothers.length; i++) {
final mWord = _resolveLinkedWord(_shieldMothers[i].primaryWordId);
if (mWord != null) {
words.add(SessionWord.fromWordItem(mWord, positionTitle: 'Мать #${i + 1}: ${_shieldMothers[i].fullName}'));
}
}
} else {
for (final r in _drawnResults) {
final word = _resolveLinkedWord(r.figure.primaryWordId);
if (word != null) {
words.add(SessionWord.fromWordItem(word, positionTitle: '${r.positionTitle}: ${r.figure.fullName}'));
}
}
}
if (words.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Сначала сформируйте расклад геомантии!')),
);
return;
}
setState(() => _isAiLoading = true);
try {
final q = _questionController.text.trim().isNotEmpty
? _questionController.text.trim()
: 'Срез геомантического расклада: ключевые векторы стихий и вердикт Судьи.';
final promptText = StringBuffer();
promptText.writeln("РАСКЛАД АРАБСКОЙ ГЕОМАНТИИ ('ИЛМ АР-РАМЛ):");
promptText.writeln('Расклад: ${_selectedLayout.title}');
if (isShield) {
promptText.writeln('Вердикт Судьи: ${_shieldJudge?.fullName} (${_shieldJudge?.element}, ${_shieldJudge?.planet})');
promptText.writeln('Правый свидетель (Внешний мир): ${_shieldRightWitness?.fullName}');
promptText.writeln('Левый свидетель (Внутренний мир): ${_shieldLeftWitness?.fullName}');
promptText.writeln('Примиритель: ${_shieldReconciler?.fullName}');
}
promptText.writeln('\nДай сакральный синтез песка: как стихии взаимодействуют, где конфликт или поддержка, и каков главный совет геомантии.');
final interpretation = await _aiService.interpretSession(
words: words,
question: '$q\n\n$promptText',
);
if (mounted) {
setState(() => _isAiLoading = false);
_showAiInterpretationModal(interpretation);
}
} catch (e) {
if (mounted) {
setState(() => _isAiLoading = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Ошибка ИИ-толкования: $e')),
);
}
}
}
void _showAiInterpretationModal(String text) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: const Color(0xFF121018),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
builder: (ctx) => DraggableScrollableSheet(
initialChildSize: 0.8,
minChildSize: 0.4,
maxChildSize: 0.95,
expand: false,
builder: (_, scroll) => Container(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 24),
child: ListView(
controller: scroll,
children: [
Center(
child: Container(
width: 44,
height: 4,
decoration: BoxDecoration(
color: Colors.white24,
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: 14),
const Row(
children: [
Text('⌛', style: TextStyle(fontSize: 22)),
SizedBox(width: 8),
Text(
'Толкование Песка (ИИ-Оракул)',
style: TextStyle(
color: Color(0xFFFFD54F),
fontSize: 17,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 14),
SelectableText(
text,
style: const TextStyle(color: Color(0xFFECEFF1), fontSize: 14, height: 1.5),
),
],
),
),
),
);
}
Widget _buildDotsVisual(List<int> lines, {double dotSize = 9, double spacing = 6, Color? color}) {
final activeColor = color ?? const Color(0xFFFFD54F);
return Column(
mainAxisSize: MainAxisSize.min,
children: lines.map((count) {
return Padding(
padding: EdgeInsets.symmetric(vertical: spacing / 2),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(count, (idx) {
return Container(
margin: EdgeInsets.symmetric(horizontal: count == 2 ? 5 : 0),
width: dotSize,
height: dotSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: activeColor,
boxShadow: [
BoxShadow(
color: activeColor.withValues(alpha: 0.4),
blurRadius: 4,
spreadRadius: 1,
),
],
),
);
}),
),
);
}).toList(),
);
}
Widget _buildLayoutSelector() {
return Container(
decoration: BoxDecoration(
color: const Color(0xFF14121E),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.2)),
),
padding: const EdgeInsets.all(4),
child: Row(
children: DivinationLayouts.geomancyLayouts.map((layout) {
final isSelected = _selectedLayout.id == layout.id;
return Expanded(
child: InkWell(
onTap: () => _onLayoutChanged(layout),
borderRadius: BorderRadius.circular(12),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF332616) : Colors.transparent,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? const Color(0xFFFFD54F) : Colors.transparent,
),
),
child: Column(
children: [
Text(
layout.title.split('(').first.trim(),
textAlign: TextAlign.center,
style: TextStyle(
color: isSelected ? const Color(0xFFFFD54F) : const Color(0xFF8C93B8),
fontSize: 12,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
Text(
layout.id == 'geomancy_1'
? '1 знак'
: (layout.id == 'geomancy_3' ? '3 знака' : '16 знаков'),
style: TextStyle(
color: isSelected ? const Color(0xFFFFE082) : const Color(0xFF61688B),
fontSize: 10,
),
),
],
),
),
),
);
}).toList(),
),
);
}
Widget _buildSandRitualTab() {
final isShield = _selectedLayout.id == 'geomancy_shield';
final currentCount = isShield ? _shieldMothers.length : _drawnResults.length;
final totalCount = isShield ? 4 : _selectedLayout.count;
final isComplete = currentCount >= totalCount;
String stepLabel;
if (isShield) {
stepLabel = isComplete
? 'Все 4 Матери нанесены! Щит рассчитан.'
: 'Нанесение ${currentCount + 1}-й Матери (${_selectedLayout.positionTitles[currentCount]})';
} else {
stepLabel = isComplete
? 'Расклад сформирован!'
: 'Фигура ${currentCount + 1} из $totalCount: ${_selectedLayout.positionTitles[currentCount]}';
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFF1E1A29),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.3)),
),
child: Row(
children: [
const Text('⏳', style: TextStyle(fontSize: 16)),
const SizedBox(width: 8),
Expanded(
child: Text(
stepLabel,
style: const TextStyle(
color: Color(0xFFFFD54F),
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
Text(
'$currentCount/$totalCount',
style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 11, fontWeight: FontWeight.bold),
),
],
),
),
const SizedBox(height: 14),
// Interactive Sand Box
Container(
height: 180,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF241C16), Color(0xFF15101A)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: _isCasting ? const Color(0xFFFFD54F) : const Color(0xFF8D6E63).withValues(alpha: 0.5),
width: _isCasting ? 2 : 1,
),
boxShadow: [
BoxShadow(
color: const Color(0xFFFFB74D).withValues(alpha: 0.08),
blurRadius: 16,
),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: List.generate(4, (rowIdx) {
final rowNames = ['Огонь (Голова)', 'Воздух (Шея)', 'Вода (Тело)', 'Земля (Стопы)'];
final count = _currentSandDotCounts[rowIdx];
final parity = count == 0 ? null : (count % 2 == 1 ? 1 : 2);
return Row(
children: [
SizedBox(
width: 95,
child: Text(
rowNames[rowIdx],
style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 10, fontWeight: FontWeight.bold),
),
),
Expanded(
child: Container(
height: 24,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0xFF0F0C14),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.white10),
),
child: count == 0
? const Text('••• песок •••', style: TextStyle(color: Colors.white24, fontSize: 10))
: Text(
'${"· " * min(count, 12)}${count > 12 ? "..." : ""} ($count)',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Color(0xFFFFD54F), fontSize: 11, letterSpacing: 2),
),
),
),
const SizedBox(width: 10),
Container(
width: 44,
height: 24,
alignment: Alignment.center,
decoration: BoxDecoration(
color: parity == null
? Colors.transparent
: (parity == 1 ? const Color(0xFF3E2723) : const Color(0xFF263238)),
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: parity == null
? Colors.white12
: (parity == 1 ? const Color(0xFFFFB74D) : const Color(0xFF80DEEA)),
),
),
child: Text(
parity == null ? '—' : (parity == 1 ? '· (1)' : '·· (2)'),
style: TextStyle(
color: parity == 1 ? const Color(0xFFFFD54F) : const Color(0xFF80DEEA),
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
],
);
}),
),
),
const SizedBox(height: 14),
// Casting Action Buttons
Row(
children: [
Expanded(
flex: 3,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFFFD54F),
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
),
onPressed: (isComplete || _isCasting) ? null : _performSandRitualCast,
icon: _isCasting
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.black),
)
: const Icon(Icons.grain_rounded, size: 20),
label: Text(
_isCasting
? 'Песок осыпается...'
: (isComplete ? 'Расклад заполнен' : 'Нанести точки на песок'),
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
),
),
),
if (!isComplete) ...[
const SizedBox(width: 8),
Expanded(
flex: 2,
child: OutlinedButton(
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFFFFD54F),
side: const BorderSide(color: Color(0xFFFFD54F)),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
),
onPressed: _isCasting ? null : _castAllRemainingSteps,
child: const Text('Все сразу', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold)),
),
),
],
],
),
],
);
}
Widget _buildManualCatalogTab() {
final isShield = _selectedLayout.id == 'geomancy_shield';
final currentCount = isShield ? _shieldMothers.length : _drawnResults.length;
final totalCount = isShield ? 4 : _selectedLayout.count;
final isComplete = currentCount >= totalCount;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
isComplete
? 'Все позиции заполнены. Нажмите «Сброс» для нового расклада.'
: 'Выберите фигуру для: ${isShield ? "Матери #${currentCount + 1}" : _selectedLayout.positionTitles[currentCount]}',
style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 12),
),
const SizedBox(height: 10),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1.8,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: _allFigures.length,
itemBuilder: (ctx, idx) {
final fig = _allFigures[idx];
return InkWell(
onTap: isComplete ? null : () => _addFigureToSpread(fig),
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFF14121E),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.2)),
),
child: Row(
children: [
_buildDotsVisual(fig.lines, dotSize: 7, spacing: 4),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
fig.russianName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Color(0xFFFFD54F),
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
Text(
fig.latinName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 10),
),
const SizedBox(height: 2),
Text(
'${fig.element} • ${fig.planet}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Color(0xFF80DEEA), fontSize: 9.5),
),
],
),
),
],
),
),
);
},
),
],
);
}
Widget _buildShieldFigureBadge({
required GeomancyFigure? figure,
required String label,
required String role,
bool isSupreme = false,
}) {
final isSelected = figure != null && _inspectedShieldFigure?.signature == figure.signature;
return InkWell(
onTap: figure == null
? null
: () {
setState(() {
_inspectedShieldFigure = figure;
_inspectedShieldTitle = '$role: ${figure.russianName}';
});
HapticFeedback.selectionClick();
},
borderRadius: BorderRadius.circular(12),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
decoration: BoxDecoration(
color: isSupreme
? const Color(0xFF332211)
: (isSelected ? const Color(0xFF2A2038) : const Color(0xFF14121E)),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSupreme
? const Color(0xFFFFD54F)
: (isSelected
? const Color(0xFF80DEEA)
: const Color(0xFFFFD54F).withValues(alpha: 0.25)),
width: isSupreme ? 1.5 : 1.0,
),
boxShadow: isSupreme
? [
BoxShadow(
color: const Color(0xFFFFD54F).withValues(alpha: 0.2),
blurRadius: 8,
)
]
: null,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
label,
style: TextStyle(
color: isSupreme ? const Color(0xFFFFD54F) : const Color(0xFF8C93B8),
fontSize: 9,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
if (figure != null) ...[
_buildDotsVisual(figure.lines, dotSize: 5, spacing: 3),
const SizedBox(height: 4),
Text(
figure.russianName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: isSupreme ? const Color(0xFFFFD54F) : Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
] else ...[
const Text('···', style: TextStyle(color: Colors.white24, fontSize: 10)),
],
],
),
),
);
}
Widget _buildShieldHierarchyView() {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF0F0D16),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.3)),
),
child: Column(
children: [
const Text(
'ВЕЛИКИЙ ЩИТ ГЕОМАНТИИ',
style: TextStyle(
color: Color(0xFFFFD54F),
fontSize: 11,
fontWeight: FontWeight.w900,
letterSpacing: 1.2,
),
),
const SizedBox(height: 10),
// Row 1: Judge & Reconciler
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
flex: 4,
child: _buildShieldFigureBadge(
figure: _shieldJudge,
label: 'СУДЬЯ',
role: 'Судья (Вердикт)',
isSupreme: true,
),
),
const SizedBox(width: 8),
Expanded(
flex: 3,
child: _buildShieldFigureBadge(
figure: _shieldReconciler,
label: 'ПРИМИРИТЕЛЬ',
role: 'Примиритель (Судья + М1)',
),
),
],
),
const SizedBox(height: 8),
// Row 2: Witnesses (Right & Left)
Row(
children: [
Expanded(
child: _buildShieldFigureBadge(
figure: _shieldLeftWitness,
label: 'ЛЕВЫЙ СВИДЕТЕЛЬ',
role: 'Левый Свидетель (Внутреннее)',
),
),
const SizedBox(width: 8),
Expanded(
child: _buildShieldFigureBadge(
figure: _shieldRightWitness,
label: 'ПРАВЫЙ СВИДЕТЕЛЬ',
role: 'Правый Свидетель (Внешнее)',
),
),
],
),
const SizedBox(height: 8),
// Row 3: 4 Nephews
Row(
children: List.generate(4, (idx) {
final n = _shieldNephews.length > (3 - idx) ? _shieldNephews[3 - idx] : null;
return Expanded(
child: Padding(
padding: EdgeInsets.only(right: idx < 3 ? 4 : 0),
child: _buildShieldFigureBadge(
figure: n,
label: 'П${4 - idx}',
role: 'Племянник #${4 - idx}',
),
),
);
}),
),
const SizedBox(height: 8),
// Row 4: 4 Daughters
Row(
children: List.generate(4, (idx) {
final d = _shieldDaughters.length > (3 - idx) ? _shieldDaughters[3 - idx] : null;
return Expanded(
child: Padding(
padding: EdgeInsets.only(right: idx < 3 ? 4 : 0),
child: _buildShieldFigureBadge(
figure: d,
label: 'Д${4 - idx}',
role: 'Дочь #${4 - idx}',
),
),
);
}),
),
const SizedBox(height: 8),
// Row 5: 4 Mothers
Row(
children: List.generate(4, (idx) {
final m = _shieldMothers.length > (3 - idx) ? _shieldMothers[3 - idx] : null;
return Expanded(
child: Padding(
padding: EdgeInsets.only(right: idx < 3 ? 4 : 0),
child: _buildShieldFigureBadge(
figure: m,
label: 'М${4 - idx}',
role: 'Мать #${4 - idx}',
),
),
);
}),
),
],
),
);
}
Widget _buildFigureDetailCard({
required GeomancyFigure figure,
required String positionTitle,
}) {
final linkedWord = _resolveLinkedWord(figure.primaryWordId);
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF161320),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFF0F0C14),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.4)),
),
child: _buildDotsVisual(figure.lines, dotSize: 8, spacing: 5),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
positionTitle,
style: const TextStyle(
color: Color(0xFFFFD54F),
fontSize: 11,
fontWeight: FontWeight.bold,
letterSpacing: 0.8,
),
),
const SizedBox(height: 2),
Text(
figure.russianName,
style: const TextStyle(
color: Colors.white,
fontSize: 17,
fontWeight: FontWeight.bold,
),
),
Text(
'${figure.latinName} (${figure.arabicName})',
style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 11.5),
),
const SizedBox(height: 6),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFF263238),
borderRadius: BorderRadius.circular(6),
),
child: Text(
figure.element,
style: const TextStyle(color: Color(0xFF80DEEA), fontSize: 10, fontWeight: FontWeight.bold),
),
),
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFF33202C),
borderRadius: BorderRadius.circular(6),
),
child: Text(
figure.planet,
style: const TextStyle(color: Color(0xFFFF80AB), fontSize: 10, fontWeight: FontWeight.bold),
),
),
const SizedBox(width: 6),
Text(
figure.isEven ? 'Чёт (внутр.)' : 'Нечет (вектор)',
style: const TextStyle(color: Color(0xFF61688B), fontSize: 10),
),
],
),
],
),
),
],
),
const SizedBox(height: 10),
Text(
figure.lore,
style: const TextStyle(color: Color(0xFFCFD8DC), fontSize: 12.5, height: 1.4),
),
if (linkedWord != null) ...[
const SizedBox(height: 12),
InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => SymbolDetailScreen(word: linkedWord)),
);
},
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFF241C16),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFFFB74D).withValues(alpha: 0.4)),
),
child: Row(
children: [
Text(linkedWord.emoji.isNotEmpty ? linkedWord.emoji : '🔗', style: const TextStyle(fontSize: 16)),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Связанный знак 999: #${linkedWord.formattedSessionNumber} «${linkedWord.word.toUpperCase()}»',
style: const TextStyle(
color: Color(0xFFFFD54F),
fontSize: 11.5,
fontWeight: FontWeight.bold,
),
),
if (linkedWord.core.isNotEmpty)
Text(
linkedWord.core,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 10),
),
],
),
),
const Icon(Icons.arrow_forward_ios_rounded, size: 12, color: Color(0xFFFFD54F)),
],
),
),
),
],
],
),
);
}
@override
Widget build(BuildContext context) {
final isShield = _selectedLayout.id == 'geomancy_shield';
final hasResults = isShield ? _shieldJudge != null : _drawnResults.isNotEmpty;
return Scaffold(
backgroundColor: const Color(0xFF0C0A12),
appBar: AppBar(
title: const Row(
children: [
Text('⌛', style: TextStyle(fontSize: 20)),
SizedBox(width: 8),
Text(
"Геомантия ('Илм ар-рамл)",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
],
),
actions: [
IconButton(
tooltip: 'Инструкция по Геомантии',
icon: const Icon(Icons.help_outline_rounded, color: Color(0xFFFFD54F)),
onPressed: () => DivinationInstructionModal.show(context, 'geomancy'),
),
if (hasResults) ...[
IconButton(
tooltip: 'Мастер-Промпт для Нейросетей',
icon: const Icon(Icons.psychology_alt_rounded, color: Color(0xFFFFD54F)),
onPressed: _showMasterPromptModal,
),
IconButton(
tooltip: 'Сбросить расклад',
icon: const Icon(Icons.refresh_rounded),
onPressed: _reset,
),
],
],
bottom: TabBar(
controller: _tabController,
indicatorColor: const Color(0xFFFFD54F),
tabs: const [
Tab(icon: Icon(Icons.grain_rounded, size: 18), text: 'Ритуал Песка'),
Tab(icon: Icon(Icons.grid_view_rounded, size: 18), text: '16 Фигур'),
],
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Question Field
TextField(
controller: _questionController,
style: const TextStyle(color: Colors.white, fontSize: 14),
decoration: InputDecoration(
hintText: 'Вопрос к песку и стихиям...',
hintStyle: const TextStyle(color: Color(0xFF8C93B8), fontSize: 13),
filled: true,
fillColor: const Color(0xFF14121E),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Color(0xFF2A2338)),
),
prefixIcon: const Icon(Icons.psychology_alt_rounded, color: Color(0xFFFFD54F), size: 20),
),
),
const SizedBox(height: 12),
// Layout Selector
_buildLayoutSelector(),
const SizedBox(height: 16),
// Tab Content (Ritual or Manual)
AnimatedBuilder(
animation: _tabController,
builder: (ctx, _) {
return _tabController.index == 0 ? _buildSandRitualTab() : _buildManualCatalogTab();
},
),
const SizedBox(height: 20),
// Results Section
if (hasResults) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'РЕЗУЛЬТАТ РАСКЛАДА',
style: TextStyle(
color: Color(0xFFFFD54F),
fontSize: 11.5,
fontWeight: FontWeight.w900,
letterSpacing: 1.0,
),
),
Row(
children: [
IconButton(
tooltip: 'Скопировать сводку',
icon: const Icon(Icons.copy_rounded, size: 18, color: Color(0xFF8C93B8)),
onPressed: _copySpreadSummary,
),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: _savedToSession ? const Color(0xFF1B5E20) : const Color(0xFF2E7D32),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
onPressed: _savedToSession ? null : () => _saveToSyncreticSession(silent: false),
icon: Icon(_savedToSession ? Icons.check_circle_rounded : Icons.bookmark_add_rounded, size: 16),
label: Text(_savedToSession ? 'В сессии ✓' : 'В сессию', style: const TextStyle(fontSize: 11)),
),
],
),
],
),
const SizedBox(height: 10),
// Shield Layout or List of Figures
if (isShield) ...[
_buildShieldHierarchyView(),
const SizedBox(height: 12),
if (_inspectedShieldFigure != null)
_buildFigureDetailCard(
figure: _inspectedShieldFigure!,
positionTitle: _inspectedShieldTitle ?? 'Инспекция Фигуры',
),
] else ...[
..._drawnResults.map((r) => _buildFigureDetailCard(
figure: r.figure,
positionTitle: r.positionTitle,
)),
],
const SizedBox(height: 12),
// AI Interpretation Button
OutlinedButton.icon(
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFFFFD54F),
side: const BorderSide(color: Color(0xFFFFD54F)),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
),
onPressed: _showMasterPromptModal,
icon: const Icon(Icons.psychology_alt_rounded, size: 18),
label: const Text(
'Мастер-Промпт для Нейросетей (Grok / Claude / GPT)',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
),
),
const SizedBox(height: 10),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF33202C),
foregroundColor: const Color(0xFFFF80AB),
side: const BorderSide(color: Color(0xFFFF80AB)),
padding: const EdgeInsets.symmetric(vertical: 13),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
),
onPressed: _isAiLoading ? null : _requestAiInterpretation,
icon: _isAiLoading
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFFFF80AB)),
)
: const Icon(Icons.auto_awesome_rounded, size: 18),
label: Text(
_isAiLoading ? 'ИИ-Оракул читает песок...' : 'ИИ-Толкование Песка',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
),
),
],
],
),
),
);
}
}