192 lines
5.7 KiB
Dart
192 lines
5.7 KiB
Dart
import "package:flutter/widgets.dart" hide Tooltip;
|
|
import "package:google_fonts/google_fonts.dart";
|
|
import "package:shadcn_flutter/shadcn_flutter.dart" hide Row, Expanded;
|
|
|
|
import "package:provider/provider.dart";
|
|
import "../../providers/chat_provider.dart";
|
|
import "../../providers/cost_provider.dart";
|
|
import "../../providers/settings_provider.dart";
|
|
import "ana_text.dart";
|
|
|
|
|
|
|
|
String _fmtTokens(int n) {
|
|
final s = n.toString();
|
|
final buf = StringBuffer();
|
|
for (var i = 0; i < s.length; i++) {
|
|
if (i > 0 && (s.length - i) % 3 == 0) buf.write(",");
|
|
buf.write(s[i]);
|
|
}
|
|
return buf.toString();
|
|
}
|
|
|
|
|
|
class FooterBar extends StatelessWidget {
|
|
const FooterBar({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final mutedFg = theme.colorScheme.mutedForeground;
|
|
final borderColor = theme.colorScheme.border;
|
|
final bg = theme.colorScheme.background;
|
|
|
|
final costProvider = context.watch<CostProvider>();
|
|
final settingsProvider = context.watch<SettingsProvider>();
|
|
final chatProvider = context.watch<ChatProvider>();
|
|
|
|
final model = settingsProvider.settings.model ?? "unknown";
|
|
final costUsd = costProvider.getTotalCostUsd();
|
|
final cost = "\$${costUsd.toStringAsFixed(4)}";
|
|
final inputToks = costProvider.getTotalInputTokens();
|
|
final outputToks = costProvider.getTotalOutputTokens();
|
|
final isLoading = chatProvider.isLoading;
|
|
final isCompacting = chatProvider.isCompacting;
|
|
final contextTokens = chatProvider.contextTokens;
|
|
final warningState = chatProvider.tokenWarningState;
|
|
|
|
final textStyle = GoogleFonts.ibmPlexMono(
|
|
fontSize: 11,
|
|
height: 1,
|
|
fontWeight: FontWeight.w600,
|
|
color: mutedFg,
|
|
);
|
|
|
|
Widget divider() => const Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 5),
|
|
child: SizedBox(height: 12, child: VerticalDivider(width: 1)),
|
|
);
|
|
|
|
Widget copyrightBlock() {
|
|
return AnaText(
|
|
"© 2026 IMBENJI.NET LTD - The Agency",
|
|
style: textStyle,
|
|
);
|
|
}
|
|
|
|
Widget statusBlock() {
|
|
final label = isCompacting
|
|
? "compacting..."
|
|
: isLoading
|
|
? "running..."
|
|
: "idle";
|
|
|
|
final labelColor = isCompacting
|
|
? theme.colorScheme.primary.withAlpha(180)
|
|
: isLoading
|
|
? theme.colorScheme.primary
|
|
: mutedFg;
|
|
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
|
|
AnaText(label, style: textStyle.copyWith(color: labelColor)),
|
|
|
|
divider(),
|
|
|
|
AnaText(model.split("/").last, style: textStyle),
|
|
|
|
],
|
|
);
|
|
}
|
|
|
|
Color? _contextWarningColor() {
|
|
if (warningState == null) return null;
|
|
if (warningState.isAboveErrorThreshold) return const Color(0xFFEF4444); // red
|
|
if (warningState.isAboveWarningThreshold) return const Color(0xFFF59E0B); // amber
|
|
return null;
|
|
}
|
|
|
|
Widget statsBlock() {
|
|
final warnColor = _contextWarningColor();
|
|
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
|
|
// compact button — shown when we're near the threshold
|
|
if (warningState != null && warningState.isAboveWarningThreshold && !isLoading && !isCompacting) ...[
|
|
GestureDetector(
|
|
onTap: () => chatProvider.runCompact(),
|
|
child: MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: AnaText(
|
|
"compact",
|
|
style: textStyle.copyWith(
|
|
color: warnColor ?? mutedFg,
|
|
decoration: TextDecoration.underline,
|
|
decorationColor: warnColor ?? mutedFg,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
divider(),
|
|
],
|
|
|
|
if (contextTokens > 0) ...[
|
|
Tooltip(
|
|
tooltip: (_) => TooltipContainer(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
|
child: AnaText(
|
|
warningState != null
|
|
? "${warningState.percentLeft}% context remaining"
|
|
: _fmtTokens(contextTokens),
|
|
style: GoogleFonts.ibmPlexMono(fontSize: 11, height: 1.2),
|
|
),
|
|
),
|
|
child: AnaText(
|
|
_fmtTokens(contextTokens),
|
|
style: textStyle.copyWith(color: warnColor ?? mutedFg),
|
|
),
|
|
),
|
|
AnaText(" tokens", style: textStyle.copyWith(color: warnColor ?? mutedFg)),
|
|
divider(),
|
|
],
|
|
|
|
Tooltip(
|
|
tooltip: (_) => TooltipContainer(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
|
child: AnaText(
|
|
"In: $inputToks\nOut: $outputToks",
|
|
style: GoogleFonts.ibmPlexMono(fontSize: 11, height: 1.2),
|
|
),
|
|
),
|
|
child: AnaText(cost, style: textStyle),
|
|
),
|
|
|
|
],
|
|
);
|
|
}
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
width: double.infinity,
|
|
decoration: BoxDecoration(
|
|
color: bg,
|
|
border: Border(top: BorderSide(color: borderColor, width: 1)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
|
|
Expanded(child: Row(children: [copyrightBlock()])),
|
|
|
|
Expanded(
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [statusBlock()],
|
|
),
|
|
),
|
|
|
|
Expanded(
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [statsBlock()],
|
|
),
|
|
),
|
|
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|