Przebudowanie aplikacji, usprawnione AI, dodanie combo buildera
Some checks failed
Build Linux App / build (push) Failing after 1m18s

This commit is contained in:
Kazimierz Ciołek
2026-07-06 23:44:17 +02:00
parent 9dcc4b87de
commit 6dd7213eb0
48 changed files with 3229 additions and 669 deletions

View File

@@ -5,6 +5,7 @@ import 'package:google_fonts/google_fonts.dart';
import 'package:trainhub_flutter/core/constants/ui_constants.dart';
import 'package:trainhub_flutter/core/theme/app_colors.dart';
import 'package:trainhub_flutter/data/services/ai_process_manager.dart';
import 'package:trainhub_flutter/data/services/ai_settings_service.dart';
import 'package:trainhub_flutter/domain/entities/chat_session.dart';
import 'package:trainhub_flutter/presentation/chat/chat_controller.dart';
import 'package:trainhub_flutter/presentation/chat/chat_state.dart';
@@ -63,9 +64,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
final now = DateTime.now();
final hour = dt.hour.toString().padLeft(2, '0');
final minute = dt.minute.toString().padLeft(2, '0');
if (dt.year == now.year &&
dt.month == now.month &&
dt.day == now.day) {
if (dt.year == now.year && dt.month == now.month && dt.day == now.day) {
return '$hour:$minute';
}
return '${dt.day}/${dt.month} $hour:$minute';
@@ -76,14 +75,19 @@ class _ChatPageState extends ConsumerState<ChatPage> {
@override
Widget build(BuildContext context) {
final modelsValidated =
ref.watch(aiModelSettingsControllerProvider).areModelsValidated;
final modelsValidated = ref
.watch(aiModelSettingsControllerProvider)
.areModelsValidated;
// A configured external provider (cloud, Ollama, LM Studio) makes chat
// usable without the built-in local models.
final settings = ref.watch(aiSettingsServiceProvider).settings;
final cloudConfigured =
settings.provider != AiProvider.local && settings.isProviderConfigured;
final state = ref.watch(chatControllerProvider);
final controller = ref.read(chatControllerProvider.notifier);
ref.listen(chatControllerProvider, (prev, next) {
if (next.hasValue &&
(prev?.value?.messages.length ?? 0) <
next.value!.messages.length) {
(prev?.value?.messages.length ?? 0) < next.value!.messages.length) {
_scrollToBottom();
}
if (next.hasValue &&
@@ -92,7 +96,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
_scrollToBottom();
}
});
if (!modelsValidated) {
if (!modelsValidated && !cloudConfigured) {
return const Scaffold(
backgroundColor: AppColors.surface,
body: MissingModelsState(),
@@ -211,14 +215,11 @@ class _ChatPageState extends ConsumerState<ChatPage> {
color: isActive
? AppColors.zinc700.withValues(alpha: 0.7)
: isHovered
? AppColors.zinc800.withValues(alpha: 0.6)
: Colors.transparent,
borderRadius:
BorderRadius.circular(UIConstants.smallBorderRadius),
? AppColors.zinc800.withValues(alpha: 0.6)
: Colors.transparent,
borderRadius: BorderRadius.circular(UIConstants.smallBorderRadius),
border: isActive
? Border.all(
color: AppColors.accent.withValues(alpha: 0.3),
)
? Border.all(color: AppColors.accent.withValues(alpha: 0.3))
: null,
),
child: Row(
@@ -239,8 +240,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
? AppColors.textPrimary
: AppColors.textSecondary,
fontSize: 13,
fontWeight:
isActive ? FontWeight.w500 : FontWeight.normal,
fontWeight: isActive ? FontWeight.w500 : FontWeight.normal,
),
),
),
@@ -260,8 +260,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
Icons.delete_outline_rounded,
color: AppColors.textMuted,
),
onPressed: () =>
controller.deleteSession(session.id),
onPressed: () => controller.deleteSession(session.id),
tooltip: 'Delete',
),
),
@@ -290,8 +289,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
horizontal: UIConstants.spacing24,
vertical: UIConstants.spacing16,
),
itemCount:
data.messages.length + (data.isTyping ? 1 : 0),
itemCount: data.messages.length + (data.isTyping ? 1 : 0),
itemBuilder: (context, index) {
if (index == data.messages.length) {
if (data.thinkingSteps.isNotEmpty ||
@@ -391,21 +389,46 @@ class _ChatPageState extends ConsumerState<ChatPage> {
ChatController controller,
) {
final aiManager = ref.watch(aiProcessManagerProvider);
final aiSettings = ref.watch(aiSettingsServiceProvider).settings;
final isTyping = asyncState.valueOrNull?.isTyping ?? false;
final isStarting = aiManager.status == AiServerStatus.starting;
final isError = aiManager.status == AiServerStatus.error;
final isReady = aiManager.status == AiServerStatus.ready;
// External providers (cloud, Ollama, LM Studio) don't need the built-in
// llama server for chat. The local embedding server is still used for
// the knowledge base when available, but its absence shouldn't block.
final isCloud = aiSettings.provider != AiProvider.local;
final cloudReady = isCloud && aiSettings.isProviderConfigured;
final isReady = cloudReady || aiManager.status == AiServerStatus.ready;
String? statusMessage;
Color statusColor = AppColors.textMuted;
if (isStarting) {
if (isCloud) {
if (!cloudReady) {
statusMessage =
'No API key for ${aiSettings.provider.label} — add one in '
'Settings, or switch back to the local model.';
statusColor = AppColors.warning;
} else {
statusMessage =
'${aiSettings.provider.label} · '
'${aiSettings.modelFor(aiSettings.provider)}';
}
} else if (isStarting) {
statusMessage =
aiManager.statusDetail ??
'Starting AI inference server (this may take a moment)...';
statusColor = AppColors.info;
} else if (isError) {
statusMessage =
'AI Server Error: ${aiManager.errorMessage ?? "Unknown error"}';
// The full error contains multi-line server logs — show only the
// first line here; details land in the debug console.
final firstLine = (aiManager.errorMessage ?? 'Unknown error')
.split('\n')
.first
.trim();
statusMessage = 'AI Server Error: $firstLine';
statusColor = AppColors.destructive;
} else if (!isReady) {
} else if (aiManager.status != AiServerStatus.ready) {
statusMessage = 'AI Server offline. Reconnecting...';
}
return Container(
@@ -424,7 +447,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
padding: const EdgeInsets.only(bottom: UIConstants.spacing8),
child: Row(
children: [
if (isStarting)
if (!isCloud && isStarting)
Container(
margin: const EdgeInsets.only(right: 8),
width: 12,
@@ -434,7 +457,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
color: AppColors.info,
),
)
else if (isError)
else if (!isCloud && isError)
const Padding(
padding: EdgeInsets.only(right: 8),
child: Icon(
@@ -453,7 +476,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
),
),
),
if (isError)
if (!isCloud && isError)
TextButton(
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 8),
@@ -479,10 +502,10 @@ class _ChatPageState extends ConsumerState<ChatPage> {
child: Container(
decoration: BoxDecoration(
color: AppColors.surfaceContainer,
borderRadius:
BorderRadius.circular(UIConstants.borderRadius),
border:
Border.all(color: AppColors.border, width: 1),
borderRadius: BorderRadius.circular(
UIConstants.borderRadius,
),
border: Border.all(color: AppColors.border, width: 1),
),
child: TextField(
controller: _inputController,
@@ -525,22 +548,23 @@ class _ChatPageState extends ConsumerState<ChatPage> {
width: 40,
height: 40,
child: Material(
color: (isTyping || !isReady)
? AppColors.zinc700
: AppColors.accent,
borderRadius:
BorderRadius.circular(UIConstants.borderRadius),
// While generating, the button becomes a stop control.
color: !isReady ? AppColors.zinc700 : AppColors.accent,
borderRadius: BorderRadius.circular(UIConstants.borderRadius),
child: InkWell(
borderRadius:
BorderRadius.circular(UIConstants.borderRadius),
onTap: (isTyping || !isReady)
borderRadius: BorderRadius.circular(
UIConstants.borderRadius,
),
onTap: !isReady
? null
: isTyping
? controller.stopGeneration
: () => _sendMessage(controller),
child: Icon(
Icons.arrow_upward_rounded,
color: (isTyping || !isReady)
? AppColors.textMuted
: AppColors.zinc950,
isTyping
? Icons.stop_rounded
: Icons.arrow_upward_rounded,
color: !isReady ? AppColors.textMuted : AppColors.zinc950,
size: 20,
),
),