feat: interactive oracle session chat and dynamic draw additional d10 cards for context refinement
This commit is contained in:
@@ -48,6 +48,40 @@ class SessionWord {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SessionChatMessage {
|
||||||
|
final String id;
|
||||||
|
final String sender; // 'user' | 'oracle' | 'system'
|
||||||
|
final String text;
|
||||||
|
final DateTime timestamp;
|
||||||
|
final SessionWord? attachedWord;
|
||||||
|
|
||||||
|
const SessionChatMessage({
|
||||||
|
required this.id,
|
||||||
|
required this.sender,
|
||||||
|
required this.text,
|
||||||
|
required this.timestamp,
|
||||||
|
this.attachedWord,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() => {
|
||||||
|
'id': id,
|
||||||
|
'sender': sender,
|
||||||
|
'text': text,
|
||||||
|
'timestamp': timestamp.toIso8601String(),
|
||||||
|
if (attachedWord != null) 'attachedWord': attachedWord!.toMap(),
|
||||||
|
};
|
||||||
|
|
||||||
|
factory SessionChatMessage.fromMap(Map<String, dynamic> map) => SessionChatMessage(
|
||||||
|
id: map['id'] as String? ?? '',
|
||||||
|
sender: map['sender'] as String? ?? 'oracle',
|
||||||
|
text: map['text'] as String? ?? '',
|
||||||
|
timestamp: DateTime.tryParse(map['timestamp'] as String? ?? '') ?? DateTime.now(),
|
||||||
|
attachedWord: map['attachedWord'] != null
|
||||||
|
? SessionWord.fromMap(map['attachedWord'] as Map<String, dynamic>)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
class OracleSession {
|
class OracleSession {
|
||||||
final String id;
|
final String id;
|
||||||
final DateTime timestamp;
|
final DateTime timestamp;
|
||||||
@@ -55,6 +89,7 @@ class OracleSession {
|
|||||||
final int wordsCount;
|
final int wordsCount;
|
||||||
final List<SessionWord> words;
|
final List<SessionWord> words;
|
||||||
final String? aiInterpretation;
|
final String? aiInterpretation;
|
||||||
|
final List<SessionChatMessage> messages;
|
||||||
|
|
||||||
const OracleSession({
|
const OracleSession({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -63,6 +98,7 @@ class OracleSession {
|
|||||||
required this.wordsCount,
|
required this.wordsCount,
|
||||||
required this.words,
|
required this.words,
|
||||||
this.aiInterpretation,
|
this.aiInterpretation,
|
||||||
|
this.messages = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
OracleSession copyWith({
|
OracleSession copyWith({
|
||||||
@@ -72,6 +108,7 @@ class OracleSession {
|
|||||||
int? wordsCount,
|
int? wordsCount,
|
||||||
List<SessionWord>? words,
|
List<SessionWord>? words,
|
||||||
String? aiInterpretation,
|
String? aiInterpretation,
|
||||||
|
List<SessionChatMessage>? messages,
|
||||||
}) {
|
}) {
|
||||||
return OracleSession(
|
return OracleSession(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -80,6 +117,7 @@ class OracleSession {
|
|||||||
wordsCount: wordsCount ?? this.wordsCount,
|
wordsCount: wordsCount ?? this.wordsCount,
|
||||||
words: words ?? this.words,
|
words: words ?? this.words,
|
||||||
aiInterpretation: aiInterpretation ?? this.aiInterpretation,
|
aiInterpretation: aiInterpretation ?? this.aiInterpretation,
|
||||||
|
messages: messages ?? this.messages,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,6 +138,7 @@ class OracleSession {
|
|||||||
'wordsCount': wordsCount,
|
'wordsCount': wordsCount,
|
||||||
'words': words.map((w) => w.toMap()).toList(),
|
'words': words.map((w) => w.toMap()).toList(),
|
||||||
if (aiInterpretation != null) 'aiInterpretation': aiInterpretation,
|
if (aiInterpretation != null) 'aiInterpretation': aiInterpretation,
|
||||||
|
'messages': messages.map((m) => m.toMap()).toList(),
|
||||||
};
|
};
|
||||||
|
|
||||||
factory OracleSession.fromMap(Map<String, dynamic> map) {
|
factory OracleSession.fromMap(Map<String, dynamic> map) {
|
||||||
@@ -113,6 +152,10 @@ class OracleSession {
|
|||||||
.toList() ??
|
.toList() ??
|
||||||
[],
|
[],
|
||||||
aiInterpretation: map['aiInterpretation'] as String?,
|
aiInterpretation: map['aiInterpretation'] as String?,
|
||||||
|
messages: (map['messages'] as List<dynamic>?)
|
||||||
|
?.map((item) => SessionChatMessage.fromMap(item as Map<String, dynamic>))
|
||||||
|
.toList() ??
|
||||||
|
[],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,5 +163,4 @@ class OracleSession {
|
|||||||
factory OracleSession.fromJson(String source) => OracleSession.fromMap(jsonDecode(source));
|
factory OracleSession.fromJson(String source) => OracleSession.fromMap(jsonDecode(source));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backward compatibility alias
|
|
||||||
typedef HistoryEntry = OracleSession;
|
typedef HistoryEntry = OracleSession;
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import '../models/history_entry_model.dart';
|
import '../models/history_entry_model.dart';
|
||||||
|
import '../models/word_model.dart';
|
||||||
import '../services/ai_oracle_service.dart';
|
import '../services/ai_oracle_service.dart';
|
||||||
import '../services/words_repository.dart';
|
import '../services/words_repository.dart';
|
||||||
|
import '../widgets/d10_dice_roller_widget.dart';
|
||||||
import '../widgets/gradient_star_badge.dart';
|
import '../widgets/gradient_star_badge.dart';
|
||||||
|
|
||||||
class SessionResultScreen extends StatefulWidget {
|
class SessionResultScreen extends StatefulWidget {
|
||||||
@@ -20,21 +22,34 @@ class SessionResultScreen extends StatefulWidget {
|
|||||||
class _SessionResultScreenState extends State<SessionResultScreen> {
|
class _SessionResultScreenState extends State<SessionResultScreen> {
|
||||||
final _aiService = AiOracleService();
|
final _aiService = AiOracleService();
|
||||||
final _repo = WordsRepository();
|
final _repo = WordsRepository();
|
||||||
|
final _chatInputController = TextEditingController();
|
||||||
|
final _scrollController = ScrollController();
|
||||||
|
|
||||||
late OracleSession _session;
|
late OracleSession _session;
|
||||||
bool _isLoadingAi = false;
|
bool _isGeneratingInitialAi = false;
|
||||||
|
bool _isChatLoading = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_session = widget.session;
|
_session = widget.session;
|
||||||
// Persist the session atomically
|
|
||||||
_repo.saveSession(_session);
|
_repo.saveSession(_session);
|
||||||
|
|
||||||
|
// If initial interpretation is not yet done, trigger it
|
||||||
|
if (_session.aiInterpretation == null) {
|
||||||
|
_fetchInitialAi();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _fetchAiInterpretation() async {
|
@override
|
||||||
setState(() => _isLoadingAi = true);
|
void dispose() {
|
||||||
HapticFeedback.mediumImpact();
|
_chatInputController.dispose();
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _fetchInitialAi() async {
|
||||||
|
setState(() => _isGeneratingInitialAi = true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final result = await _aiService.interpretSession(
|
final result = await _aiService.interpretSession(
|
||||||
@@ -43,26 +58,138 @@ class _SessionResultScreenState extends State<SessionResultScreen> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
final updated = _session.copyWith(aiInterpretation: result);
|
final initialMessage = SessionChatMessage(
|
||||||
|
id: 'msg_0',
|
||||||
|
sender: 'oracle',
|
||||||
|
text: result,
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final updated = _session.copyWith(
|
||||||
|
aiInterpretation: result,
|
||||||
|
messages: _session.messages.isEmpty ? [initialMessage] : _session.messages,
|
||||||
|
);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_session = updated;
|
_session = updated;
|
||||||
_isLoadingAi = false;
|
_isGeneratingInitialAi = false;
|
||||||
});
|
});
|
||||||
await _repo.saveSession(updated);
|
await _repo.saveSession(updated);
|
||||||
|
_scrollToBottom();
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _isGeneratingInitialAi = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _sendMessage({SessionWord? newlyAddedWord}) async {
|
||||||
|
final text = _chatInputController.text.trim();
|
||||||
|
if (text.isEmpty && newlyAddedWord == null) return;
|
||||||
|
|
||||||
|
final userMessageText = text.isNotEmpty
|
||||||
|
? text
|
||||||
|
: 'Раскрой значение добавленного знака #${newlyAddedWord?.formattedSessionNumber} «${newlyAddedWord?.word.toUpperCase()}».';
|
||||||
|
|
||||||
|
_chatInputController.clear();
|
||||||
|
|
||||||
|
final userMsg = SessionChatMessage(
|
||||||
|
id: 'msg_${DateTime.now().millisecondsSinceEpoch}',
|
||||||
|
sender: 'user',
|
||||||
|
text: userMessageText,
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
attachedWord: newlyAddedWord,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updatedWithUser = _session.copyWith(
|
||||||
|
messages: [..._session.messages, userMsg],
|
||||||
|
);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_session = updatedWithUser;
|
||||||
|
_isChatLoading = true;
|
||||||
|
});
|
||||||
|
_scrollToBottom();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final reply = await _aiService.chatWithOracle(
|
||||||
|
session: updatedWithUser,
|
||||||
|
userMessage: userMessageText,
|
||||||
|
newlyAddedWord: newlyAddedWord,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
final oracleMsg = SessionChatMessage(
|
||||||
|
id: 'msg_${DateTime.now().millisecondsSinceEpoch + 1}',
|
||||||
|
sender: 'oracle',
|
||||||
|
text: reply,
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final finalSession = updatedWithUser.copyWith(
|
||||||
|
messages: [...updatedWithUser.messages, oracleMsg],
|
||||||
|
);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_session = finalSession;
|
||||||
|
_isChatLoading = false;
|
||||||
|
});
|
||||||
|
await _repo.saveSession(finalSession);
|
||||||
|
_scrollToBottom();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() => _isLoadingAi = false);
|
setState(() => _isChatLoading = false);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(content: Text('Ошибка отправки: $e')),
|
||||||
content: Text('Ошибка генерации: $e'),
|
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _scrollToBottom() {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (_scrollController.hasClients) {
|
||||||
|
_scrollController.animateTo(
|
||||||
|
_scrollController.position.maxScrollExtent + 100,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showAddWordModal() {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
builder: (ctx) => _AddWordBottomSheet(
|
||||||
|
onWordChosen: (WordItem word) async {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
final sessionWord = SessionWord.fromWordItem(
|
||||||
|
word,
|
||||||
|
positionTitle: 'Уточняющий знак #${_session.words.length + 1}',
|
||||||
|
);
|
||||||
|
|
||||||
|
final updated = _session.copyWith(
|
||||||
|
words: [..._session.words, sessionWord],
|
||||||
|
wordsCount: _session.words.length + 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_session = updated;
|
||||||
|
});
|
||||||
|
await _repo.saveSession(updated);
|
||||||
|
|
||||||
|
// Trigger chat refinement with this new word
|
||||||
|
await _sendMessage(newlyAddedWord: sessionWord);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _copySessionToClipboard() {
|
void _copySessionToClipboard() {
|
||||||
final buf = StringBuffer();
|
final buf = StringBuffer();
|
||||||
buf.writeln('✨ Оракул 999 — Сессия Клеромантии ✨');
|
buf.writeln('✨ Оракул 999 — Сессия Клеромантии ✨');
|
||||||
@@ -76,12 +203,11 @@ class _SessionResultScreenState extends State<SessionResultScreen> {
|
|||||||
final w = _session.words[i];
|
final w = _session.words[i];
|
||||||
final pos = w.positionTitle != null ? ' (${w.positionTitle})' : '';
|
final pos = w.positionTitle != null ? ' (${w.positionTitle})' : '';
|
||||||
buf.writeln('🔮 Знак #${w.formattedSessionNumber}: ${w.word.toUpperCase()}$pos');
|
buf.writeln('🔮 Знак #${w.formattedSessionNumber}: ${w.word.toUpperCase()}$pos');
|
||||||
buf.writeln(' Аспект: ${w.partOfSpeech} • ${w.category}');
|
buf.writeln(' ${w.symbolicMeaning}\n');
|
||||||
buf.writeln(' Смысл: ${w.symbolicMeaning}\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_session.aiInterpretation != null) {
|
if (_session.aiInterpretation != null) {
|
||||||
buf.writeln('🤖 AI Трактовка Оракула (G4F):');
|
buf.writeln('🤖 Толкование Оракула (G4F):');
|
||||||
buf.writeln(_session.aiInterpretation);
|
buf.writeln(_session.aiInterpretation);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +219,7 @@ class _SessionResultScreenState extends State<SessionResultScreen> {
|
|||||||
children: [
|
children: [
|
||||||
GradientStar(size: 18),
|
GradientStar(size: 18),
|
||||||
SizedBox(width: 10),
|
SizedBox(width: 10),
|
||||||
Expanded(child: Text('Вся сессия скопирована в буфер обмена')),
|
Expanded(child: Text('Сессия скопирована в буфер обмена')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
duration: const Duration(seconds: 2),
|
duration: const Duration(seconds: 2),
|
||||||
@@ -115,12 +241,17 @@ class _SessionResultScreenState extends State<SessionResultScreen> {
|
|||||||
const GradientStar(size: 22),
|
const GradientStar(size: 22),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'Знаки Сессии (${_session.words.length})',
|
'Сессия (${_session.words.length} ${_session.words.length == 1 ? "знак" : (_session.words.length < 5 ? "знака" : "знаков")})',
|
||||||
style: const TextStyle(fontWeight: FontWeight.w900),
|
style: const TextStyle(fontWeight: FontWeight.w900),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Докинуть уточняющий знак',
|
||||||
|
icon: const Icon(Icons.add_circle_outline_rounded, color: Color(0xFFFFD54F)),
|
||||||
|
onPressed: _showAddWordModal,
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: 'Скопировать сессию',
|
tooltip: 'Скопировать сессию',
|
||||||
icon: const Icon(Icons.copy_rounded),
|
icon: const Icon(Icons.copy_rounded),
|
||||||
@@ -129,231 +260,513 @@ class _SessionResultScreenState extends State<SessionResultScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: SingleChildScrollView(
|
child: Column(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
children: [
|
||||||
child: Column(
|
// Top Collapsible / Horizontal Words Strip
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
Container(
|
||||||
children: [
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||||
// Question Banner
|
decoration: BoxDecoration(
|
||||||
if (_session.question != null && _session.question!.isNotEmpty) ...[
|
color: isDark ? const Color(0xFF141728) : theme.colorScheme.surfaceContainerLow,
|
||||||
Container(
|
border: const Border(
|
||||||
padding: const EdgeInsets.all(12),
|
bottom: BorderSide(color: Colors.white12, width: 1),
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0xFF1E2138),
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
border: Border.all(
|
|
||||||
color: const Color(0xFFFFD54F).withValues(alpha: 0.3),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.help_outline_rounded, size: 18, color: Color(0xFFFFD54F)),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
'Вопрос: «${_session.question}»',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Color(0xFFFFF9C4),
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
),
|
||||||
],
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
// AI Interpretation Box if generated or button
|
children: [
|
||||||
if (_session.aiInterpretation != null) ...[
|
// Question line if present
|
||||||
Container(
|
if (_session.question != null && _session.question!.isNotEmpty) ...[
|
||||||
padding: const EdgeInsets.all(16),
|
Padding(
|
||||||
decoration: BoxDecoration(
|
padding: const EdgeInsets.only(bottom: 6),
|
||||||
gradient: const LinearGradient(
|
child: Row(
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
colors: [Color(0xFF2A1E4A), Color(0xFF191833)],
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
border: Border.all(
|
|
||||||
color: const Color(0xFFBA68C8).withValues(alpha: 0.6),
|
|
||||||
width: 1.2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
const Row(
|
const Icon(Icons.help_outline_rounded, size: 14, color: Color(0xFFFFD54F)),
|
||||||
children: [
|
const SizedBox(width: 6),
|
||||||
Icon(Icons.psychology_rounded, size: 20, color: Color(0xFFE1BEE7)),
|
Expanded(
|
||||||
SizedBox(width: 8),
|
child: Text(
|
||||||
Text(
|
'Вопрос: «${_session.question}»',
|
||||||
'AI ТРАКТОВКА ИСТОРИИ (G4F)',
|
maxLines: 1,
|
||||||
style: TextStyle(
|
overflow: TextOverflow.ellipsis,
|
||||||
fontSize: 12,
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.w900,
|
fontSize: 12,
|
||||||
letterSpacing: 0.8,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFFE1BEE7),
|
color: Color(0xFFFFF9C4),
|
||||||
),
|
fontStyle: FontStyle.italic,
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.refresh_rounded, size: 18, color: Color(0xFFE1BEE7)),
|
|
||||||
tooltip: 'Перегенерировать',
|
|
||||||
onPressed: _fetchAiInterpretation,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
),
|
||||||
Text(
|
],
|
||||||
_session.aiInterpretation!,
|
|
||||||
style: const TextStyle(
|
// Words Horizontal Badges List + Add Button
|
||||||
fontSize: 13.5,
|
SingleChildScrollView(
|
||||||
height: 1.5,
|
scrollDirection: Axis.horizontal,
|
||||||
color: Color(0xFFF3E5F5),
|
child: Row(
|
||||||
|
children: [
|
||||||
|
..._session.words.asMap().entries.map((e) {
|
||||||
|
final idx = e.key;
|
||||||
|
final w = e.value;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 6),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => _showWordDetails(w),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF232742),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: const Color(0xFFFFD54F).withValues(alpha: 0.4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${idx + 1}. ',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: theme.colorScheme.outline,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'#${w.formattedSessionNumber} ${w.word.toUpperCase()}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
color: Color(0xFFFFD54F),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
// Button: Add word
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: _showAddWordModal,
|
||||||
|
icon: const Icon(Icons.casino_rounded, size: 14, color: Color(0xFFFFD54F)),
|
||||||
|
label: const Text(
|
||||||
|
'+ Докинуть знак',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11.5,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFFFFD54F),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||||
|
side: const BorderSide(color: Color(0xFFFFD54F)),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
const SizedBox(height: 16),
|
),
|
||||||
] else ...[
|
),
|
||||||
FilledButton.icon(
|
|
||||||
onPressed: _isLoadingAi ? null : _fetchAiInterpretation,
|
// Live Chat Area
|
||||||
icon: _isLoadingAi
|
Expanded(
|
||||||
? const SizedBox(
|
child: _session.messages.isEmpty && _isGeneratingInitialAi
|
||||||
width: 18,
|
? const Center(
|
||||||
height: 18,
|
child: Column(
|
||||||
child: CircularProgressIndicator(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
strokeWidth: 2,
|
children: [
|
||||||
|
CircularProgressIndicator(
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFFFD54F)),
|
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFFFD54F)),
|
||||||
),
|
),
|
||||||
)
|
SizedBox(height: 16),
|
||||||
: const Icon(Icons.auto_awesome, size: 18, color: Color(0xFFE1BEE7)),
|
Text(
|
||||||
label: Text(
|
'Оракул синтезирует символы сессии...',
|
||||||
_isLoadingAi ? 'Синтез толкования...' : 'Синтезировать связную AI-трактовку (G4F)',
|
style: TextStyle(fontStyle: FontStyle.italic),
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 13,
|
|
||||||
color: Color(0xFFFFF9C4),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
style: FilledButton.styleFrom(
|
|
||||||
backgroundColor: const Color(0xFF2A2145),
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
side: const BorderSide(
|
|
||||||
color: Color(0xFFBA68C8),
|
|
||||||
width: 1.2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
],
|
|
||||||
|
|
||||||
// Word Cards List
|
|
||||||
..._session.words.asMap().entries.map((entry) {
|
|
||||||
final idx = entry.key;
|
|
||||||
final w = entry.value;
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 14),
|
|
||||||
child: Card(
|
|
||||||
elevation: 6,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
side: BorderSide(
|
|
||||||
color: const Color(0xFFFFD54F).withValues(alpha: 0.25),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
color: isDark ? const Color(0xFF1B1D30) : theme.colorScheme.surface,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const GradientStar(size: 15),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Text(
|
|
||||||
'${idx + 1}. ЗНАК #${w.formattedSessionNumber}',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.w900,
|
|
||||||
fontSize: 13,
|
|
||||||
color: Color(0xFFFFD54F),
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isDark ? const Color(0xFF262A48) : Colors.black12,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
w.partOfSpeech,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
color: isDark ? const Color(0xFFB0B7DE) : Colors.black87,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Center(
|
|
||||||
child: Text(
|
|
||||||
w.word.toUpperCase(),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.w900,
|
|
||||||
letterSpacing: 1.5,
|
|
||||||
color: Color(0xFFFFF9C4),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isDark ? const Color(0xFF131525) : theme.colorScheme.surfaceContainerLow,
|
|
||||||
borderRadius: BorderRadius.circular(14),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
w.symbolicMeaning,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
height: 1.45,
|
|
||||||
color: isDark ? const Color(0xFFD4D6E6) : Colors.black87,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
: ListView.builder(
|
||||||
|
controller: _scrollController,
|
||||||
|
padding: const EdgeInsets.fromLTRB(14, 12, 14, 12),
|
||||||
|
itemCount: _session.messages.length + (_isChatLoading ? 1 : 0),
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
if (idx == _session.messages.length) {
|
||||||
|
return _buildTypingIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
final msg = _session.messages[idx];
|
||||||
|
final isUser = msg.sender == 'user';
|
||||||
|
|
||||||
|
return _buildChatBubble(msg, isUser, isDark, theme);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Bottom Chat Input Bar
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.fromLTRB(10, 8, 10, 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isDark ? const Color(0xFF141728) : theme.colorScheme.surfaceContainerHigh,
|
||||||
|
border: const Border(
|
||||||
|
top: BorderSide(color: Colors.white12, width: 1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Докинуть уточняющий знак d10',
|
||||||
|
icon: const Icon(Icons.casino_rounded, color: Color(0xFFFFD54F), size: 24),
|
||||||
|
onPressed: _showAddWordModal,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _chatInputController,
|
||||||
|
textInputAction: TextInputAction.send,
|
||||||
|
onSubmitted: (_) => _sendMessage(),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Задайте уточняющий вопрос оракулу...',
|
||||||
|
hintStyle: const TextStyle(fontSize: 13),
|
||||||
|
filled: true,
|
||||||
|
fillColor: isDark ? const Color(0xFF1F223B) : theme.colorScheme.surface,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
const SizedBox(width: 6),
|
||||||
}),
|
IconButton.filled(
|
||||||
|
onPressed: _isChatLoading ? null : () => _sendMessage(),
|
||||||
|
icon: const Icon(Icons.arrow_upward_rounded, size: 20),
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFF2A2D4A),
|
||||||
|
foregroundColor: const Color(0xFFFFD54F),
|
||||||
|
side: const BorderSide(color: Color(0xFFFFD54F), width: 1.2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildChatBubble(
|
||||||
|
SessionChatMessage msg, bool isUser, bool isDark, ThemeData theme) {
|
||||||
|
return Align(
|
||||||
|
alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: MediaQuery.of(context).size.width * 0.85,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: isUser
|
||||||
|
? const LinearGradient(
|
||||||
|
colors: [Color(0xFF2A2D4A), Color(0xFF1E2138)],
|
||||||
|
)
|
||||||
|
: const LinearGradient(
|
||||||
|
colors: [Color(0xFF2B1F4A), Color(0xFF1A1B35)],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: const Radius.circular(18),
|
||||||
|
topRight: const Radius.circular(18),
|
||||||
|
bottomLeft: Radius.circular(isUser ? 18 : 4),
|
||||||
|
bottomRight: Radius.circular(isUser ? 4 : 18),
|
||||||
|
),
|
||||||
|
border: Border.all(
|
||||||
|
color: isUser
|
||||||
|
? const Color(0xFFFFD54F).withValues(alpha: 0.3)
|
||||||
|
: const Color(0xFFBA68C8).withValues(alpha: 0.45),
|
||||||
|
width: 1.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Header: Sender icon & name
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
isUser ? Icons.person_outline_rounded : Icons.psychology_rounded,
|
||||||
|
size: 14,
|
||||||
|
color: isUser ? const Color(0xFFFFD54F) : const Color(0xFFE1BEE7),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
Text(
|
||||||
|
isUser ? 'Вы' : 'Оракул 999',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
color: isUser ? const Color(0xFFFFD54F) : const Color(0xFFE1BEE7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Attached new word pill if present
|
||||||
|
if (msg.attachedWord != null) ...[
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF3B2B5E),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.4)),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'🎲 Добавлен знак: #${msg.attachedWord!.formattedSessionNumber} «${msg.attachedWord!.word.toUpperCase()}»',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFFFFF9C4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
|
||||||
|
// Message text
|
||||||
|
Text(
|
||||||
|
msg.text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13.5,
|
||||||
|
height: 1.45,
|
||||||
|
color: isUser ? Colors.white : const Color(0xFFF3E5F5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTypingIndicator() {
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF241B3B),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(color: const Color(0xFFBA68C8).withValues(alpha: 0.3)),
|
||||||
|
),
|
||||||
|
child: const Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 14,
|
||||||
|
height: 14,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 1.8,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFE1BEE7)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Оракул внимает и толкует знаки...',
|
||||||
|
style: TextStyle(fontSize: 12, color: Color(0xFFE1BEE7), fontStyle: FontStyle.italic),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showWordDetails(SessionWord word) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
const GradientStar(size: 20),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'#${word.formattedSessionNumber} ${word.word.toUpperCase()}',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${word.partOfSpeech} • ${word.category}',
|
||||||
|
style: TextStyle(color: Theme.of(ctx).colorScheme.outline),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
word.symbolicMeaning,
|
||||||
|
style: const TextStyle(fontSize: 14, height: 1.45),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
child: const Text('Закрыть'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Modal Bottom Sheet for rolling or typing an additional d10 word
|
||||||
|
class _AddWordBottomSheet extends StatefulWidget {
|
||||||
|
final ValueChanged<WordItem> onWordChosen;
|
||||||
|
|
||||||
|
const _AddWordBottomSheet({required this.onWordChosen});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_AddWordBottomSheet> createState() => _AddWordBottomSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AddWordBottomSheetState extends State<_AddWordBottomSheet> {
|
||||||
|
final _repo = WordsRepository();
|
||||||
|
String _digits = '';
|
||||||
|
|
||||||
|
void _onRollCompleted(int finalNumber) {
|
||||||
|
final word = _repo.getWordBySessionNumber(finalNumber);
|
||||||
|
if (word != null) {
|
||||||
|
widget.onWordChosen(word);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onKeypadTap(String val) {
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
|
||||||
|
if (val == 'C') {
|
||||||
|
setState(() => _digits = '');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (val == '⌫') {
|
||||||
|
if (_digits.isNotEmpty) {
|
||||||
|
setState(() => _digits = _digits.substring(0, _digits.length - 1));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String newDigits = _digits.length >= 3 ? val : _digits + val;
|
||||||
|
setState(() => _digits = newDigits);
|
||||||
|
|
||||||
|
if (newDigits.length == 3) {
|
||||||
|
final num = int.tryParse(newDigits);
|
||||||
|
if (num != null && num <= 999) {
|
||||||
|
final word = _repo.getWordBySessionNumber(num);
|
||||||
|
if (word != null) {
|
||||||
|
widget.onWordChosen(word);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isDark ? const Color(0xFF141728) : Theme.of(context).colorScheme.surface,
|
||||||
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(28)),
|
||||||
|
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.35)),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFFFD54F).withValues(alpha: 0.5),
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Row(
|
||||||
|
children: [
|
||||||
|
GradientStar(size: 20),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Докинуть уточняющий знак (3× d10)',
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
D10DiceRollerWidget(
|
||||||
|
digits: _digits,
|
||||||
|
isRolling: false,
|
||||||
|
onRollPressed: () {},
|
||||||
|
onRollCompleted: _onRollCompleted,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
// Keypad for 3 digits
|
||||||
|
_buildKeypadRow(['1', '2', '3']),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_buildKeypadRow(['4', '5', '6']),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_buildKeypadRow(['7', '8', '9']),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_buildKeypadRow(['C', '0', '⌫']),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildKeypadRow(List<String> keys) {
|
||||||
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: keys.map((key) {
|
||||||
|
final isSpecial = key == 'C' || key == '⌫';
|
||||||
|
return Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
child: Material(
|
||||||
|
color: isSpecial ? const Color(0xFF2A2D4A) : (isDark ? const Color(0xFF20233A) : Colors.black12),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => _onKeypadTap(key),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Container(
|
||||||
|
height: 42,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
key,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: isSpecial ? const Color(0xFFFFF9C4) : Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,61 @@ class AiOracleService {
|
|||||||
return _generateOfflineSessionInterpretation(words, question);
|
return _generateOfflineSessionInterpretation(words, question);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Multi-turn chat with the Oracle inside a session, supporting context refinement and additional words
|
||||||
|
Future<String> chatWithOracle({
|
||||||
|
required OracleSession session,
|
||||||
|
required String userMessage,
|
||||||
|
SessionWord? newlyAddedWord,
|
||||||
|
}) async {
|
||||||
|
final wordsNarrative = StringBuffer();
|
||||||
|
for (int i = 0; i < session.words.length; i++) {
|
||||||
|
final w = session.words[i];
|
||||||
|
wordsNarrative.writeln('${i + 1}. Знак #${w.formattedSessionNumber} «${w.word.toUpperCase()}» (${w.partOfSpeech}): ${w.symbolicMeaning}');
|
||||||
|
}
|
||||||
|
|
||||||
|
final chatHistoryBuf = StringBuffer();
|
||||||
|
for (final msg in session.messages.take(8)) {
|
||||||
|
if (msg.sender == 'user') {
|
||||||
|
chatHistoryBuf.writeln('Вопрошающий: ${msg.text}');
|
||||||
|
} else if (msg.sender == 'oracle') {
|
||||||
|
chatHistoryBuf.writeln('Оракул: ${msg.text}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final addedWordContext = newlyAddedWord != null
|
||||||
|
? '\n[ВАЖНО: Вопрошающий только что вытянул ДОПОЛНИТЕЛЬНЫЙ ЗНАК для уточнения деталей: #${newlyAddedWord.formattedSessionNumber} «${newlyAddedWord.word.toUpperCase()}» (${newlyAddedWord.symbolicMeaning}). Обязательно вплети этот новый знак в ответ!]\n'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
final prompt = '''Ты — Священный Оракул Клеромантии 999. Ты ведешь глубокий диалог с вопрошающим в рамках текущей сессии гадания.
|
||||||
|
|
||||||
|
Контекст сессии:
|
||||||
|
Вопрос: «${session.question ?? 'Общий расклад'}»
|
||||||
|
Выпавшие знаки жребия:
|
||||||
|
$wordsNarrative
|
||||||
|
$addedWordContext
|
||||||
|
Предыдущий диалог:
|
||||||
|
$chatHistoryBuf
|
||||||
|
|
||||||
|
Новая реплика/вопрос вопрошающего:
|
||||||
|
«$userMessage»
|
||||||
|
|
||||||
|
Инструкция:
|
||||||
|
Ответь на реплику вопрошающего прямо, мудро и ёмко (2-4 предложения).
|
||||||
|
Опирайся на символизм выпавших знаков (особенно дополнительных, если они появились). Раскрой скрытые детали и дай практичный духовный совет.''';
|
||||||
|
|
||||||
|
try {
|
||||||
|
final res = await _callPollinationsOpenAi(prompt);
|
||||||
|
if (res != null && res.trim().isNotEmpty) return res.trim();
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final res = await _callPollinationsDirect(prompt);
|
||||||
|
if (res != null && res.trim().isNotEmpty) return res.trim();
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
return 'Оракул внимает вашим словам. ${newlyAddedWord != null ? "Дополнительный знак «${newlyAddedWord.word}» уточняет скрытые детали: ${newlyAddedWord.symbolicMeaning}. " : ""}Слушайте голос интуиции и действуйте в согласии с открывшейся истиной.';
|
||||||
|
}
|
||||||
|
|
||||||
/// Single word legacy interpretation helper
|
/// Single word legacy interpretation helper
|
||||||
Future<String> interpret({
|
Future<String> interpret({
|
||||||
required WordItem word,
|
required WordItem word,
|
||||||
|
|||||||
Reference in New Issue
Block a user