Przebudowanie aplikacji, usprawnione AI, dodanie combo buildera
Some checks failed
Build Linux App / build (push) Failing after 1m18s
Some checks failed
Build Linux App / build (push) Failing after 1m18s
This commit is contained in:
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user