1096 lines
46 KiB
Dart
1096 lines
46 KiB
Dart
import 'manual_dive_screen.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter/services.dart';
|
||
import '../models/word_model.dart';
|
||
import '../models/culture_branch.dart';
|
||
import '../services/ai_oracle_service.dart';
|
||
import '../services/words_repository.dart';
|
||
import '../widgets/gradient_star_badge.dart';
|
||
import '../services/oracle_master_prompt_generator.dart';
|
||
|
||
class EditSymbolScreen extends StatefulWidget {
|
||
final WordItem word;
|
||
|
||
const EditSymbolScreen({super.key, required this.word});
|
||
|
||
@override
|
||
State<EditSymbolScreen> createState() => _EditSymbolScreenState();
|
||
}
|
||
|
||
class _EditSymbolScreenState extends State<EditSymbolScreen> {
|
||
final _formKey = GlobalKey<FormState>();
|
||
final _repo = WordsRepository();
|
||
final _aiService = AiOracleService();
|
||
|
||
late final TextEditingController _coreController;
|
||
late final TextEditingController _oracleController;
|
||
late final TextEditingController _shadowController;
|
||
late final TextEditingController _asksController;
|
||
late final TextEditingController _stepController;
|
||
late final TextEditingController _culturesController;
|
||
|
||
bool _isSaving = false;
|
||
bool _isGeneratingAi = false;
|
||
bool _isDistributing = false;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
final w = widget.word;
|
||
_coreController = TextEditingController(text: w.core);
|
||
_oracleController = TextEditingController(
|
||
text: w.oracle.isNotEmpty ? w.oracle : w.description,
|
||
);
|
||
_shadowController = TextEditingController(text: w.shadow);
|
||
_asksController = TextEditingController(text: w.asks);
|
||
_stepController = TextEditingController(text: w.step);
|
||
_culturesController = TextEditingController(text: formatCulturesBlock(w.cultures));
|
||
|
||
_coreController.addListener(() => _checkAndDistributeMultiBlockPaste(_coreController));
|
||
_oracleController.addListener(() => _checkAndDistributeMultiBlockPaste(_oracleController));
|
||
}
|
||
|
||
void _checkAndDistributeMultiBlockPaste(TextEditingController controller) {
|
||
if (_isDistributing) return;
|
||
final val = controller.text;
|
||
if (val.length > 80 &&
|
||
(val.contains('##') ||
|
||
(val.toUpperCase().contains('ОРАКУЛ') &&
|
||
(val.toUpperCase().contains('ТЕНЬ') || val.toUpperCase().contains('ЯДРО'))))) {
|
||
final blocks = _parseRevisionBlocks(val);
|
||
if (blocks.length >= 2) {
|
||
_isDistributing = true;
|
||
setState(() {
|
||
if (blocks.containsKey('ЯДРО')) _coreController.text = blocks['ЯДРО']!;
|
||
if (blocks.containsKey('ОРАКУЛ')) _oracleController.text = blocks['ОРАКУЛ']!;
|
||
if (blocks.containsKey('ТЕНЬ')) _shadowController.text = blocks['ТЕНЬ']!;
|
||
if (blocks.containsKey('ВОПРОС')) _asksController.text = blocks['ВОПРОС']!;
|
||
if (blocks.containsKey('ЖЕСТ')) _stepController.text = blocks['ЖЕСТ']!;
|
||
if (blocks.containsKey('КУЛЬТУРЫ')) _culturesController.text = formatCulturesBlock(parseCulturesBlock(blocks['КУЛЬТУРЫ']!));
|
||
});
|
||
_isDistributing = false;
|
||
HapticFeedback.mediumImpact();
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Row(
|
||
children: [
|
||
const Icon(Icons.auto_fix_high_rounded, color: Color(0xFF81C784), size: 20),
|
||
const SizedBox(width: 10),
|
||
Expanded(
|
||
child: Text('Умное распределение: распознано ${blocks.length} секций и заполнено по всем 5 полям!'),
|
||
),
|
||
],
|
||
),
|
||
backgroundColor: const Color(0xFF1B3D2F),
|
||
behavior: SnackBarBehavior.floating,
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_coreController.dispose();
|
||
_oracleController.dispose();
|
||
_shadowController.dispose();
|
||
_asksController.dispose();
|
||
_stepController.dispose();
|
||
_culturesController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
/// Builds the Master Prompt for deep culturological and mythological enrichment of the Archetype.
|
||
String _buildPrePrompt() {
|
||
return OracleMasterPromptGenerator.generateArchetypeEnrichmentPrompt(
|
||
widget.word,
|
||
customCore: _coreController.text,
|
||
customOracle: _oracleController.text,
|
||
customShadow: _shadowController.text,
|
||
customAsks: _asksController.text,
|
||
customStep: _stepController.text,
|
||
);
|
||
}
|
||
|
||
void _copyPrePrompt() {
|
||
final prompt = _buildPrePrompt();
|
||
Clipboard.setData(ClipboardData(text: prompt));
|
||
HapticFeedback.mediumImpact();
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: const Row(
|
||
children: [
|
||
GradientStar(size: 18),
|
||
SizedBox(width: 10),
|
||
Expanded(
|
||
child: Text(
|
||
'Предпромпт со всей текущей информацией скопирован! Вставьте его в любую нейросеть.',
|
||
style: TextStyle(fontSize: 13),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
duration: const Duration(seconds: 3),
|
||
behavior: SnackBarBehavior.floating,
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||
),
|
||
);
|
||
}
|
||
|
||
void _showPrePromptViewer() {
|
||
final prompt = _buildPrePrompt();
|
||
showModalBottomSheet(
|
||
context: context,
|
||
isScrollControlled: true,
|
||
backgroundColor: const Color(0xFF151829),
|
||
shape: const RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||
),
|
||
builder: (ctx) => DraggableScrollableSheet(
|
||
initialChildSize: 0.75,
|
||
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(0xFF3949AB),
|
||
borderRadius: BorderRadius.circular(2),
|
||
),
|
||
),
|
||
),
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
const Row(
|
||
children: [
|
||
GradientStar(size: 20),
|
||
SizedBox(width: 8),
|
||
Text(
|
||
'Полный Предпромпт для ИИ',
|
||
style: TextStyle(
|
||
color: Color(0xFFFFF9C4),
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
IconButton(
|
||
icon: const Icon(Icons.close_rounded, color: Color(0xFF8C93B8)),
|
||
onPressed: () => Navigator.pop(ctx),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
const Text(
|
||
'Включает все текущие данные знака, чтобы не повторяться и получить глубокую редакцию:',
|
||
style: TextStyle(color: Color(0xFF8C93B8), fontSize: 12),
|
||
),
|
||
const SizedBox(height: 12),
|
||
Expanded(
|
||
child: Container(
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF0C0E1A),
|
||
borderRadius: BorderRadius.circular(14),
|
||
border: Border.all(color: const Color(0xFF2A2D4A)),
|
||
),
|
||
child: SingleChildScrollView(
|
||
controller: scrollController,
|
||
child: SelectableText(
|
||
prompt,
|
||
style: const TextStyle(
|
||
color: Color(0xFFE0E2EC),
|
||
fontSize: 12.5,
|
||
height: 1.45,
|
||
fontFamily: 'monospace',
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 12),
|
||
ElevatedButton.icon(
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: const Color(0xFFFFD54F),
|
||
foregroundColor: const Color(0xFF0C0E1A),
|
||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||
),
|
||
icon: const Icon(Icons.copy_rounded, size: 18),
|
||
label: const Text(
|
||
'Скопировать предпромпт в буфер обмена',
|
||
style: TextStyle(fontWeight: FontWeight.w900, fontSize: 13),
|
||
),
|
||
onPressed: () {
|
||
Navigator.pop(ctx);
|
||
_copyPrePrompt();
|
||
},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Map<String, String> _parseRevisionBlocks(String text) {
|
||
final blocks = <String, String>{};
|
||
if (text.trim().isEmpty) return blocks;
|
||
|
||
// 1. Clean markdown code fences if wrapped in ```markdown ... ```
|
||
var src = text.trim();
|
||
if (src.startsWith('```')) {
|
||
src = src.replaceFirst(RegExp(r'^```[a-zA-Z]*\n?'), '');
|
||
if (src.endsWith('```')) {
|
||
src = src.substring(0, src.length - 3).trim();
|
||
}
|
||
}
|
||
|
||
final lines = src.replaceAll('\r\n', '\n').split('\n');
|
||
|
||
String? detectSlot(String line) {
|
||
final stripped = line.trim();
|
||
if (stripped.isEmpty || stripped.length > 95) return null;
|
||
|
||
final isHeaderFormat = stripped.startsWith('#') ||
|
||
stripped.startsWith('*') ||
|
||
stripped.startsWith('[') ||
|
||
RegExp(r'^(?:\d+[\.\)]|[IVX]+[\.\)])').hasMatch(stripped) ||
|
||
(stripped.length < 70 && (stripped.endsWith(':') || stripped == stripped.toUpperCase())) ||
|
||
RegExp(r'(?:ЯДРО|ОРАКУЛ|ТЕНЬ|ВОПРОС|ЖЕСТ|КОАН|КУЛЬТУР)', caseSensitive: false).hasMatch(stripped);
|
||
|
||
if (!isHeaderFormat) return null;
|
||
|
||
// 0. КУЛЬТУРЫ — сравнительный атлас традиций (подзаголовки ### внутри остаются в блоке)
|
||
if (RegExp(r'^[#*\s\[\d.)]*(?:🌍\s*)?КУЛЬТУР[А-Яа-я]*(?:[^\wа-яА-ЯёЁ]|$)', caseSensitive: false).hasMatch(stripped)) {
|
||
return 'КУЛЬТУРЫ';
|
||
}
|
||
// 1. ЯДРО / ФОРМУЛА
|
||
if (RegExp(r'(?:^|[^\wа-яА-ЯёЁ])(?:ЯДР[ОАЕУ]|ФОРМУЛ[А-Яа-я]*|ОНТОЛОГИ[А-Яа-я]*)(?:[^\wа-яА-ЯёЁ]|$)', caseSensitive: false).hasMatch(stripped)) {
|
||
return 'ЯДРО';
|
||
}
|
||
// 2. ВОПРОС / КОАН (checked before ТЕНЬ to prevent 'экзистенциальный' substring collision)
|
||
if (RegExp(r'(?:^|[^\wа-яА-ЯёЁ])(?:ВОПРОС[А-Яа-я]*|КОАН[А-Яа-я]*|РЕФЛЕКСИ[А-Яа-я]*)(?:[^\wа-яА-ЯёЁ]|$)', caseSensitive: false).hasMatch(stripped)) {
|
||
return 'ВОПРОС';
|
||
}
|
||
// 3. ТЕНЬ / ЛОВУШКА
|
||
if (RegExp(r'(?:^|[^\wа-яА-ЯёЁ])(?:ТЕН[ЬЯЕИЮ]|ТЕНЕВ[А-Яа-я]*|ЛОВУШК[А-Яа-я]*|ИСКАЖЕН[А-Яа-я]*|СЛЕП[А-Яа-я]*\s+ЗОН[А-Яа-я]*)(?:[^\wа-яА-ЯёЁ]|$)', caseSensitive: false).hasMatch(stripped)) {
|
||
return 'ТЕНЬ';
|
||
}
|
||
// 4. ЖЕСТ / МИКРОДЕЙСТВИЕ
|
||
if (RegExp(r'(?:^|[^\wа-яА-ЯёЁ])(?:ЖЕСТ[А-Яа-я]*|МИКРОДЕЙСТВИ[А-Яа-я]*|ДЕЙСТВИ[ЕЯ]|ПРАКТИК[А-Яа-я]*)(?:[^\wа-яА-ЯёЁ]|$)', caseSensitive: false).hasMatch(stripped)) {
|
||
return 'ЖЕСТ';
|
||
}
|
||
// 5. ОРАКУЛ / СТАТЬЯ / ЭССЕ
|
||
if (RegExp(r'(?:^|[^\wа-яА-ЯёЁ])(?:ОРАКУЛ[А-Яа-я]*|СТАТЬ[ЯЕИЮ]|ЭССЕ|ТОЛКОВАНИ[А-Яа-я]*|ОБОГАЩЕНИ[А-Яа-я]*)(?:[^\wа-яА-ЯёЁ]|$)', caseSensitive: false).hasMatch(stripped)) {
|
||
return 'ОРАКУЛ';
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
final headers = <({int lineIdx, String slot})>[];
|
||
for (int i = 0; i < lines.length; i++) {
|
||
final slot = detectSlot(lines[i]);
|
||
if (slot != null) {
|
||
headers.add((lineIdx: i, slot: slot));
|
||
}
|
||
}
|
||
|
||
for (int i = 0; i < headers.length; i++) {
|
||
final startLineIdx = headers[i].lineIdx + 1;
|
||
final slot = headers[i].slot;
|
||
final endLineIdx = (i + 1 < headers.length) ? headers[i + 1].lineIdx : lines.length;
|
||
|
||
var content = lines.sublist(startLineIdx, endLineIdx).join('\n').trim();
|
||
|
||
// Strip prompt instructions in parentheses like (1–2 чеканных...) at the top
|
||
content = content.replaceFirst(
|
||
RegExp(r'^\s*\([^)]*(?:предложен|абзац|коан|действи|формул|ловушк|стать)[^)]*\)\s*\n?', caseSensitive: false),
|
||
'',
|
||
).trim();
|
||
|
||
// Strip trailing metric block like "Метрика: 4850 символов"
|
||
content = content.replaceAll(
|
||
RegExp(r'(?:\n|\r\n)\s*(?:Метрика|Количество символов|Символов|Длина текста)[:—–\s][\s\S]*$', caseSensitive: false),
|
||
'',
|
||
).trim();
|
||
|
||
if (content.isNotEmpty) {
|
||
if (!blocks.containsKey(slot) || content.length > blocks[slot]!.length) {
|
||
blocks[slot] = content;
|
||
}
|
||
}
|
||
}
|
||
|
||
return blocks;
|
||
}
|
||
|
||
Future<void> _pasteFromMarkdownClipboard() async {
|
||
HapticFeedback.lightImpact();
|
||
final data = await Clipboard.getData(Clipboard.kTextPlain);
|
||
final text = data?.text?.trim() ?? '';
|
||
|
||
if (text.isEmpty) {
|
||
_showManualMarkdownInputDialog();
|
||
return;
|
||
}
|
||
|
||
final blocks = _parseRevisionBlocks(text);
|
||
if (blocks.isEmpty) {
|
||
_showManualMarkdownInputDialog(initialText: text);
|
||
return;
|
||
}
|
||
|
||
_showImportPreviewModal(blocks, rawText: text, title: 'Умная вставка из Markdown');
|
||
}
|
||
|
||
void _showManualMarkdownInputDialog({String? initialText}) {
|
||
final controller = TextEditingController(text: initialText ?? '');
|
||
showModalBottomSheet(
|
||
context: context,
|
||
isScrollControlled: true,
|
||
backgroundColor: const Color(0xFF151829),
|
||
shape: const RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||
),
|
||
builder: (ctx) => Padding(
|
||
padding: EdgeInsets.only(
|
||
bottom: MediaQuery.of(ctx).viewInsets.bottom + 20,
|
||
left: 16,
|
||
right: 16,
|
||
top: 16,
|
||
),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
const Icon(Icons.paste_rounded, color: Color(0xFFA5D6A7), size: 20),
|
||
const SizedBox(width: 8),
|
||
const Expanded(
|
||
child: Text(
|
||
'Вставка Markdown-статьи',
|
||
style: TextStyle(color: Color(0xFFFFF9C4), fontSize: 16, fontWeight: FontWeight.bold),
|
||
),
|
||
),
|
||
IconButton(
|
||
icon: const Icon(Icons.close_rounded, color: Color(0xFF8C93B8)),
|
||
onPressed: () => Navigator.pop(ctx),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
const Text(
|
||
'Вставьте сюда текст от нейросети с заголовками ## ЯДРО, ## ОРАКУЛ, ## ТЕНЬ, ## ВОПРОС, ## ЖЕСТ:',
|
||
style: TextStyle(color: Color(0xFFECEFF1), fontSize: 12),
|
||
),
|
||
const SizedBox(height: 12),
|
||
TextField(
|
||
controller: controller,
|
||
maxLines: 8,
|
||
style: const TextStyle(color: Color(0xFFFFF9C4), fontSize: 13, height: 1.4),
|
||
decoration: InputDecoration(
|
||
filled: true,
|
||
fillColor: const Color(0xFF1E2238),
|
||
hintText: '## ЯДРО\nФормула...\n\n## ОРАКУЛ\nСтатья...',
|
||
hintStyle: TextStyle(color: Colors.white.withOpacity(0.3)),
|
||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(14), borderSide: BorderSide.none),
|
||
),
|
||
),
|
||
const SizedBox(height: 14),
|
||
ElevatedButton.icon(
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: const Color(0xFFA5D6A7),
|
||
foregroundColor: const Color(0xFF0C0E1A),
|
||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||
),
|
||
icon: const Icon(Icons.auto_fix_high_rounded, size: 18),
|
||
label: const Text(
|
||
'Распознать и применить к полям',
|
||
style: TextStyle(fontWeight: FontWeight.w900, fontSize: 13),
|
||
),
|
||
onPressed: () {
|
||
final input = controller.text.trim();
|
||
Navigator.pop(ctx);
|
||
if (input.isEmpty) return;
|
||
final blocks = _parseRevisionBlocks(input);
|
||
if (blocks.isEmpty) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(content: Text('Не удалось найти размеченные блоки (## ЯДРО, ## ОРАКУЛ, и т.д.)')),
|
||
);
|
||
} else {
|
||
_showImportPreviewModal(blocks, rawText: input, title: 'Распознанные блоки статьи');
|
||
}
|
||
},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
void _showImportPreviewModal(Map<String, String> blocks, {String? rawText, String title = 'Импорт из Markdown'}) {
|
||
showModalBottomSheet(
|
||
context: context,
|
||
isScrollControlled: true,
|
||
backgroundColor: const Color(0xFF151829),
|
||
shape: const RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||
),
|
||
builder: (ctx) => DraggableScrollableSheet(
|
||
initialChildSize: 0.82,
|
||
minChildSize: 0.5,
|
||
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(0xFF3949AB),
|
||
borderRadius: BorderRadius.circular(2),
|
||
),
|
||
),
|
||
),
|
||
Row(
|
||
children: [
|
||
const GradientStar(size: 20),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: Text(
|
||
title,
|
||
style: const TextStyle(
|
||
color: Color(0xFFFFF9C4),
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF2E7D32).withOpacity(0.3),
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(color: const Color(0xFF81C784)),
|
||
),
|
||
child: Text(
|
||
'${blocks.length}/5 блоков',
|
||
style: const TextStyle(color: Color(0xFFA5D6A7), fontSize: 11, fontWeight: FontWeight.bold),
|
||
),
|
||
),
|
||
const SizedBox(width: 4),
|
||
IconButton(
|
||
icon: const Icon(Icons.close_rounded, color: Color(0xFF8C93B8)),
|
||
onPressed: () => Navigator.pop(ctx),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
const Text(
|
||
'Проверьте распознанные секции. При нажатии «Применить» они мгновенно заполнят форму знака.',
|
||
style: TextStyle(color: Color(0xFFECEFF1), fontSize: 12, height: 1.35),
|
||
),
|
||
const SizedBox(height: 12),
|
||
Expanded(
|
||
child: ListView(
|
||
controller: scrollController,
|
||
children: [
|
||
if (blocks.containsKey('ЯДРО'))
|
||
_buildAiBlockPreview('✨ ЯДРО', blocks['ЯДРО']!, const Color(0xFFFFD54F)),
|
||
if (blocks.containsKey('ОРАКУЛ'))
|
||
_buildAiBlockPreview('📖 ОРАКУЛ / СТАТЬЯ', blocks['ОРАКУЛ']!, const Color(0xFF64B5F6)),
|
||
if (blocks.containsKey('ТЕНЬ'))
|
||
_buildAiBlockPreview('🌑 ТЕНЬ', blocks['ТЕНЬ']!, const Color(0xFFE57373)),
|
||
if (blocks.containsKey('ВОПРОС'))
|
||
_buildAiBlockPreview('❓ ВОПРОС', blocks['ВОПРОС']!, const Color(0xFFBA68C8)),
|
||
if (blocks.containsKey('ЖЕСТ'))
|
||
_buildAiBlockPreview('⚡ ЖЕСТ', blocks['ЖЕСТ']!, const Color(0xFF81C784)),
|
||
if (blocks.containsKey('КУЛЬТУРЫ'))
|
||
_buildAiBlockPreview(
|
||
'🌍 КУЛЬТУРЫ (${parseCulturesBlock(blocks['КУЛЬТУРЫ']!).length} ветвей)',
|
||
blocks['КУЛЬТУРЫ']!,
|
||
const Color(0xFFFFB74D),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 12),
|
||
Row(
|
||
children: [
|
||
if (rawText != null)
|
||
Expanded(
|
||
flex: 2,
|
||
child: OutlinedButton(
|
||
style: OutlinedButton.styleFrom(
|
||
foregroundColor: const Color(0xFF80DEEA),
|
||
side: const BorderSide(color: Color(0xFF80DEEA)),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||
),
|
||
onPressed: () {
|
||
Navigator.pop(ctx);
|
||
_showManualMarkdownInputDialog(initialText: rawText);
|
||
},
|
||
child: const Text('Сырой MD', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)),
|
||
),
|
||
),
|
||
if (rawText != null) const SizedBox(width: 8),
|
||
Expanded(
|
||
flex: 3,
|
||
child: ElevatedButton.icon(
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: const Color(0xFFFFD54F),
|
||
foregroundColor: const Color(0xFF0C0E1A),
|
||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||
),
|
||
icon: const Icon(Icons.check_circle_rounded, size: 18),
|
||
label: const Text(
|
||
'Применить к полям',
|
||
style: TextStyle(fontWeight: FontWeight.w900, fontSize: 13),
|
||
),
|
||
onPressed: () {
|
||
setState(() {
|
||
if (blocks.containsKey('ЯДРО')) _coreController.text = blocks['ЯДРО']!;
|
||
if (blocks.containsKey('ОРАКУЛ')) _oracleController.text = blocks['ОРАКУЛ']!;
|
||
if (blocks.containsKey('ТЕНЬ')) _shadowController.text = blocks['ТЕНЬ']!;
|
||
if (blocks.containsKey('ВОПРОС')) _asksController.text = blocks['ВОПРОС']!;
|
||
if (blocks.containsKey('ЖЕСТ')) _stepController.text = blocks['ЖЕСТ']!;
|
||
if (blocks.containsKey('КУЛЬТУРЫ')) _culturesController.text = formatCulturesBlock(parseCulturesBlock(blocks['КУЛЬТУРЫ']!));
|
||
});
|
||
Navigator.pop(ctx);
|
||
HapticFeedback.mediumImpact();
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Row(
|
||
children: [
|
||
const Icon(Icons.check_circle_rounded, color: Color(0xFF81C784), size: 20),
|
||
const SizedBox(width: 10),
|
||
Expanded(child: Text('Применено ${blocks.length} блоков статьи к знаку!')),
|
||
],
|
||
),
|
||
behavior: SnackBarBehavior.floating,
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _generateAiRevision() async {
|
||
setState(() => _isGeneratingAi = true);
|
||
HapticFeedback.mediumImpact();
|
||
|
||
try {
|
||
final prompt = _buildPrePrompt();
|
||
final response = await _aiService.generateSymbolRevision(prePrompt: prompt);
|
||
|
||
if (!mounted) return;
|
||
setState(() => _isGeneratingAi = false);
|
||
|
||
if (response.trim().isEmpty) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(content: Text('Не удалось получить ответ от ИИ. Попробуйте скопировать предпромпт вручную.')),
|
||
);
|
||
return;
|
||
}
|
||
|
||
final blocks = _parseRevisionBlocks(response);
|
||
_showImportPreviewModal(blocks, rawText: response, title: 'Предложенное ИИ-обогащение');
|
||
} catch (e) {
|
||
if (mounted) {
|
||
setState(() => _isGeneratingAi = false);
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(content: Text('Ошибка ИИ: $e')),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
Widget _buildAiBlockPreview(String title, String content, Color accent) {
|
||
return Container(
|
||
margin: const EdgeInsets.only(bottom: 12),
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF1E2238),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: accent.withValues(alpha: 0.5)),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Container(
|
||
width: 8,
|
||
height: 8,
|
||
decoration: BoxDecoration(color: accent, shape: BoxShape.circle),
|
||
),
|
||
const SizedBox(width: 6),
|
||
Text(
|
||
title,
|
||
style: TextStyle(color: accent, fontSize: 12, fontWeight: FontWeight.bold),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 6),
|
||
Text(
|
||
content,
|
||
maxLines: 6,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: const TextStyle(color: Color(0xFFE0E2EC), fontSize: 13, height: 1.45),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _save() async {
|
||
if (!_formKey.currentState!.validate()) return;
|
||
|
||
setState(() => _isSaving = true);
|
||
HapticFeedback.mediumImpact();
|
||
|
||
await _repo.saveCustomEdit(
|
||
widget.word.word,
|
||
core: _coreController.text,
|
||
oracle: _oracleController.text,
|
||
shadow: _shadowController.text,
|
||
asks: _asksController.text,
|
||
step: _stepController.text,
|
||
cultures: parseCulturesBlock(_culturesController.text),
|
||
);
|
||
|
||
if (!mounted) return;
|
||
setState(() => _isSaving = false);
|
||
|
||
Navigator.pop(context, true);
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Row(
|
||
children: [
|
||
const GradientStar(size: 18),
|
||
const SizedBox(width: 10),
|
||
Expanded(child: Text('Канон «${widget.word.word}» успешно обновлен!')),
|
||
],
|
||
),
|
||
backgroundColor: const Color(0xFF2E7D32),
|
||
behavior: SnackBarBehavior.floating,
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _confirmReset() async {
|
||
final confirmed = await showDialog<bool>(
|
||
context: context,
|
||
builder: (ctx) => AlertDialog(
|
||
backgroundColor: const Color(0xFF1E2238),
|
||
title: const Text('Сбросить к канону релиза?', style: TextStyle(color: Color(0xFFFFF9C4))),
|
||
content: const Text(
|
||
'Ваша персональная редакция этого знака будет удалена, вернётся оригинальный текст из базы.',
|
||
style: TextStyle(color: Color(0xFFB0BEC5)),
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.pop(ctx, false),
|
||
child: const Text('Отмена', style: TextStyle(color: Color(0xFF9FA8DA))),
|
||
),
|
||
FilledButton(
|
||
style: FilledButton.styleFrom(backgroundColor: const Color(0xFFE57373)),
|
||
onPressed: () => Navigator.pop(ctx, true),
|
||
child: const Text('Сбросить'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
|
||
if (confirmed == true) {
|
||
setState(() => _isSaving = true);
|
||
await _repo.resetCustomEdit(widget.word.word);
|
||
if (!mounted) return;
|
||
setState(() => _isSaving = false);
|
||
Navigator.pop(context, true);
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final w = widget.word;
|
||
|
||
return Scaffold(
|
||
backgroundColor: const Color(0xFF0C0E1A),
|
||
appBar: AppBar(
|
||
title: Text(
|
||
'Редактор: ${w.word.toUpperCase()}',
|
||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
||
),
|
||
actions: [
|
||
IconButton(
|
||
tooltip: '78-полюсный паспорт (Ручной обогатитель)',
|
||
icon: const Icon(Icons.badge_outlined, color: Color(0xFFFFD54F)),
|
||
onPressed: () {
|
||
Navigator.of(context).push(
|
||
MaterialPageRoute(builder: (_) => ManualDiveScreen(initialWord: widget.word)),
|
||
);
|
||
},
|
||
),
|
||
IconButton(
|
||
tooltip: 'Скопировать промпт на обогащение',
|
||
icon: const Icon(Icons.copy_all_rounded, color: Color(0xFFFFD54F)),
|
||
onPressed: _copyPrePrompt,
|
||
),
|
||
IconButton(
|
||
tooltip: 'Вставить из Markdown',
|
||
icon: const Icon(Icons.paste_rounded, color: Color(0xFFA5D6A7)),
|
||
onPressed: _pasteFromMarkdownClipboard,
|
||
),
|
||
IconButton(
|
||
tooltip: 'Сгенерировать через ИИ',
|
||
icon: const Icon(Icons.auto_awesome_rounded, color: Color(0xFF80DEEA)),
|
||
onPressed: _isGeneratingAi ? null : _generateAiRevision,
|
||
),
|
||
if (w.isUserEdited)
|
||
IconButton(
|
||
tooltip: 'Сбросить к заводскому канону',
|
||
icon: const Icon(Icons.restore_rounded, color: Color(0xFFE57373)),
|
||
onPressed: _isSaving ? null : _confirmReset,
|
||
),
|
||
IconButton(
|
||
tooltip: 'Сохранить',
|
||
icon: const Icon(Icons.check_rounded, color: Color(0xFF81C784), size: 26),
|
||
onPressed: _isSaving ? null : _save,
|
||
),
|
||
],
|
||
),
|
||
body: Form(
|
||
key: _formKey,
|
||
child: ListView(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||
children: [
|
||
// Prominent Pre-Prompt Context Banner
|
||
Container(
|
||
padding: const EdgeInsets.all(14),
|
||
decoration: BoxDecoration(
|
||
gradient: const LinearGradient(
|
||
colors: [Color(0xFF281E44), Color(0xFF14172B)],
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
),
|
||
borderRadius: BorderRadius.circular(18),
|
||
border: Border.all(color: const Color(0xFFFFD54F).withOpacity(0.5)),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: const Color(0xFFFFD54F).withOpacity(0.08),
|
||
blurRadius: 10,
|
||
),
|
||
],
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
const GradientStar(size: 18),
|
||
const SizedBox(width: 8),
|
||
const Expanded(
|
||
child: Text(
|
||
'ПРЕДПРОМПТ ДЛЯ НЕЙРОСЕТЕЙ (ИИ)',
|
||
style: TextStyle(
|
||
color: Color(0xFFFFD54F),
|
||
fontSize: 11.5,
|
||
fontWeight: FontWeight.w900,
|
||
letterSpacing: 1.0,
|
||
),
|
||
),
|
||
),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF3B2B5E),
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
child: Text(
|
||
'№${w.formattedSessionNumber}',
|
||
style: const TextStyle(color: Color(0xFFFFF9C4), fontSize: 10, fontWeight: FontWeight.bold),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
const Text(
|
||
'Содержит всю текущую информацию о знаке (ядро, оракул, тень, вопрос, жест, слои традиций), чтобы при формулировании правок не повторяться, а углублять канон.',
|
||
style: TextStyle(color: Color(0xFFECEFF1), fontSize: 12, height: 1.35),
|
||
),
|
||
const SizedBox(height: 12),
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: OutlinedButton.icon(
|
||
style: OutlinedButton.styleFrom(
|
||
foregroundColor: const Color(0xFFFFD54F),
|
||
side: BorderSide(color: const Color(0xFFFFD54F).withValues(alpha: 0.6)),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||
),
|
||
icon: const Icon(Icons.copy_rounded, size: 14),
|
||
label: const Text('Скопировать промпт', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11.5)),
|
||
onPressed: _copyPrePrompt,
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: OutlinedButton.icon(
|
||
style: OutlinedButton.styleFrom(
|
||
foregroundColor: const Color(0xFF80DEEA),
|
||
side: BorderSide(color: const Color(0xFF80DEEA).withValues(alpha: 0.6)),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||
),
|
||
icon: const Icon(Icons.visibility_rounded, size: 14),
|
||
label: const Text('Просмотр промпта', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11.5)),
|
||
onPressed: _showPrePromptViewer,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: ElevatedButton.icon(
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: const Color(0xFF1B3D2F),
|
||
foregroundColor: const Color(0xFFA5D6A7),
|
||
side: const BorderSide(color: Color(0xFF81C784)),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||
padding: const EdgeInsets.symmetric(vertical: 9),
|
||
),
|
||
icon: const Icon(Icons.paste_rounded, size: 15, color: Color(0xFFA5D6A7)),
|
||
label: const Text('Вставить из Markdown', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11.5)),
|
||
onPressed: _pasteFromMarkdownClipboard,
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: ElevatedButton.icon(
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: const Color(0xFF382959),
|
||
foregroundColor: const Color(0xFFFFF9C4),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||
padding: const EdgeInsets.symmetric(vertical: 9),
|
||
),
|
||
icon: _isGeneratingAi
|
||
? const SizedBox(
|
||
width: 14,
|
||
height: 14,
|
||
child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFFFFD54F)),
|
||
)
|
||
: const Icon(Icons.auto_awesome_rounded, size: 15, color: Color(0xFFFFD54F)),
|
||
label: const Text('ИИ-обогащение', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11.5)),
|
||
onPressed: _isGeneratingAi ? null : _generateAiRevision,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
|
||
_buildSectionField(
|
||
title: '✨ ЯДРО / ФОРМУЛА ЗАКОНА (Core)',
|
||
hint: 'Краткая суть принципа и природы знака (1-2 предложения)',
|
||
controller: _coreController,
|
||
minLines: 2,
|
||
maxLines: 4,
|
||
accentColor: const Color(0xFFFFD54F),
|
||
),
|
||
const SizedBox(height: 16),
|
||
|
||
_buildSectionField(
|
||
title: '📖 ОРАКУЛ / ТОЛКОВАНИЕ (Oracle)',
|
||
hint: 'Развернутое символическое и практическое прочтение знака в раскладе',
|
||
controller: _oracleController,
|
||
minLines: 6,
|
||
maxLines: 14,
|
||
accentColor: const Color(0xFF64B5F6),
|
||
),
|
||
const SizedBox(height: 16),
|
||
|
||
_buildSectionField(
|
||
title: '🌑 ТЕНЬ / ИСКАЖЕНИЕ (Shadow)',
|
||
hint: 'Ловушка знака, избыток или дефицит принципа, скрытая опасность',
|
||
controller: _shadowController,
|
||
minLines: 2,
|
||
maxLines: 4,
|
||
accentColor: const Color(0xFFE57373),
|
||
),
|
||
const SizedBox(height: 16),
|
||
|
||
_buildSectionField(
|
||
title: '❓ ВОПРОС / ЭКСПЕРТИЗА (Asks)',
|
||
hint: 'Экзистенциальный вопрос вопрошающему, проверка честности',
|
||
controller: _asksController,
|
||
minLines: 2,
|
||
maxLines: 4,
|
||
accentColor: const Color(0xFFBA68C8),
|
||
),
|
||
const SizedBox(height: 16),
|
||
|
||
_buildSectionField(
|
||
title: '⚡ РИТУАЛЬНЫЙ ЖЕСТ / ДЕЙСТВИЕ (Step)',
|
||
hint: 'Конкретное заземляющее действие или практика в физическом мире',
|
||
controller: _stepController,
|
||
minLines: 2,
|
||
maxLines: 4,
|
||
accentColor: const Color(0xFF81C784),
|
||
),
|
||
const SizedBox(height: 16),
|
||
|
||
_buildSectionField(
|
||
title: '🌍 КУЛЬТУРЫ ЧЕЛОВЕЧЕСТВА (Cultures)',
|
||
hint: '### Шумер и Месопотамия\nТекст…\n\n### Древний Египет\nТекст…\n\nПусто — ветви извлекаются из ОРАКУЛА автоматически',
|
||
controller: _culturesController,
|
||
minLines: 4,
|
||
maxLines: 14,
|
||
accentColor: const Color(0xFFFFB74D),
|
||
),
|
||
const SizedBox(height: 24),
|
||
|
||
ElevatedButton.icon(
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: const Color(0xFF2E7D32),
|
||
foregroundColor: Colors.white,
|
||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||
),
|
||
onPressed: _isSaving ? null : _save,
|
||
icon: _isSaving
|
||
? const SizedBox(
|
||
width: 18,
|
||
height: 18,
|
||
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
|
||
)
|
||
: const Icon(Icons.save_rounded),
|
||
label: const Text(
|
||
'Сохранить изменения знака',
|
||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||
),
|
||
),
|
||
|
||
if (w.isUserEdited) ...[
|
||
const SizedBox(height: 12),
|
||
OutlinedButton.icon(
|
||
style: OutlinedButton.styleFrom(
|
||
foregroundColor: const Color(0xFFE57373),
|
||
side: const BorderSide(color: Color(0xFFE57373)),
|
||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||
),
|
||
onPressed: _isSaving ? null : _confirmReset,
|
||
icon: const Icon(Icons.restore_rounded),
|
||
label: const Text('Сбросить к канону из релиза'),
|
||
),
|
||
],
|
||
const SizedBox(height: 20),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildSectionField({
|
||
required String title,
|
||
required String hint,
|
||
required TextEditingController controller,
|
||
required int minLines,
|
||
required int maxLines,
|
||
required Color accentColor,
|
||
}) {
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Container(
|
||
width: 4,
|
||
height: 16,
|
||
decoration: BoxDecoration(
|
||
color: accentColor,
|
||
borderRadius: BorderRadius.circular(2),
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
title,
|
||
style: TextStyle(
|
||
color: accentColor,
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.bold,
|
||
letterSpacing: 0.5,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
TextFormField(
|
||
controller: controller,
|
||
minLines: minLines,
|
||
maxLines: maxLines,
|
||
style: const TextStyle(
|
||
color: Color(0xFFECEFF1),
|
||
fontSize: 14,
|
||
height: 1.45,
|
||
),
|
||
decoration: InputDecoration(
|
||
hintText: hint,
|
||
hintStyle: const TextStyle(color: Color(0xFF546E7A), fontSize: 13),
|
||
filled: true,
|
||
fillColor: const Color(0xFF151829),
|
||
contentPadding: const EdgeInsets.all(14),
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(14),
|
||
borderSide: const BorderSide(color: Color(0xFF263238)),
|
||
),
|
||
enabledBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(14),
|
||
borderSide: BorderSide(color: accentColor.withOpacity(0.3)),
|
||
),
|
||
focusedBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(14),
|
||
borderSide: BorderSide(color: accentColor, width: 1.5),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|