Files
999/lib/screens/edit_symbol_screen.dart

1096 lines
46 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 '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),
),
),
),
],
);
}
}