flux_ai_ui 1.0.0
flux_ai_ui: ^1.0.0 copied to clipboard
Open Flutter primitives for AI-native mobile interfaces.
import 'package:flutter/material.dart';
import 'package:flux_ai_ui/flux_ai_ui.dart';
void main() => runApp(const FluxAiDemoApp());
class FluxAiDemoApp extends StatelessWidget {
const FluxAiDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flux AI UI',
theme: FluxAiTheme.materialLight(),
home: Uri.base.queryParameters['preview'] == 'all'
? const AllComponentsPreviewScreen()
: const ComponentIndexScreen(),
);
}
}
class ComponentIndexScreen extends StatelessWidget {
const ComponentIndexScreen({super.key});
@override
Widget build(BuildContext context) {
final tokens = FluxAiTheme.of(context);
return Scaffold(
body: SafeArea(
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 18, 20, 14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Flux AI UI',
style: Theme.of(context).textTheme.headlineLarge,
),
const SizedBox(height: 8),
Text(
'Open Flutter primitives for agentic mobile interfaces.',
style: TextStyle(color: tokens.mutedInk, fontSize: 16),
),
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: const [
FluxPill(label: '17 components', icon: Icons.widgets),
FluxPill(
label: 'iOS + Android',
icon: Icons.phone_iphone,
),
FluxPill(label: 'MIT', icon: Icons.lock_open),
],
),
],
),
),
),
SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
sliver: SliverLayoutBuilder(
builder: (context, constraints) {
final crossAxisCount = constraints.crossAxisExtent >= 720
? 2
: 1;
return SliverGrid.builder(
itemCount: _sections.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
mainAxisExtent: 154,
),
itemBuilder: (context, index) {
final section = _sections[index];
return _ComponentCard(
index: index,
section: section,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => _ComponentDetailScreen(
index: index,
section: section,
),
),
);
},
);
},
);
},
),
),
],
),
),
);
}
}
class AllComponentsPreviewScreen extends StatelessWidget {
const AllComponentsPreviewScreen({super.key});
@override
Widget build(BuildContext context) {
final tokens = FluxAiTheme.of(context);
return Scaffold(
backgroundColor: tokens.canvas,
body: SafeArea(
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(28, 24, 28, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Flux AI UI',
style: Theme.of(context).textTheme.displaySmall,
),
const SizedBox(height: 8),
Text(
'AI-native Flutter components rendered from the live example app.',
style: TextStyle(color: tokens.mutedInk, fontSize: 18),
),
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: const [
FluxPill(label: '17 components', icon: Icons.widgets),
FluxPill(label: 'Live Flutter Web', icon: Icons.public),
FluxPill(label: 'Themeable', icon: Icons.palette),
],
),
],
),
),
),
SliverPadding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 28),
sliver: SliverLayoutBuilder(
builder: (context, constraints) {
final width = constraints.crossAxisExtent;
final crossAxisCount = width >= 1320
? 3
: width >= 860
? 2
: 1;
return SliverGrid.builder(
itemCount: _sections.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
crossAxisSpacing: 14,
mainAxisSpacing: 14,
mainAxisExtent: 348,
),
itemBuilder: (context, index) {
final section = _sections[index];
return _PreviewComponentTile(
index: index,
section: section,
);
},
);
},
),
),
],
),
),
);
}
}
class _PreviewComponentTile extends StatelessWidget {
const _PreviewComponentTile({required this.index, required this.section});
final int index;
final _DemoSection section;
@override
Widget build(BuildContext context) {
final tokens = FluxAiTheme.of(context);
return FluxPanel(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: tokens.accent.withValues(alpha: .12),
borderRadius: BorderRadius.circular(10),
),
child: Icon(section.icon, color: tokens.accent, size: 19),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
section.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium,
),
Text(
'Component ${(index + 1).toString().padLeft(2, '0')}',
style: TextStyle(
color: tokens.mutedInk,
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
],
),
),
],
),
const SizedBox(height: 10),
Text(
section.description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: tokens.mutedInk, height: 1.25),
),
const SizedBox(height: 12),
Expanded(
child: ClipRect(child: _PreviewComponentBody(section: section)),
),
],
),
);
}
}
class _PreviewComponentBody extends StatelessWidget {
const _PreviewComponentBody({required this.section});
final _DemoSection section;
@override
Widget build(BuildContext context) {
if (section.title == 'Prompt Bar') {
return OverflowBox(
alignment: Alignment.bottomCenter,
minHeight: 0,
maxHeight: 384,
child: SizedBox(height: 384, child: section.child),
);
}
return SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: section.child,
);
}
}
class _ComponentCard extends StatelessWidget {
const _ComponentCard({
required this.index,
required this.section,
required this.onTap,
});
final int index;
final _DemoSection section;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final tokens = FluxAiTheme.of(context);
return Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(tokens.radius),
onTap: onTap,
child: FluxPanel(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 38,
height: 38,
decoration: BoxDecoration(
color: tokens.accent.withValues(alpha: .12),
borderRadius: BorderRadius.circular(12),
),
child: Icon(section.icon, color: tokens.accent, size: 20),
),
const Spacer(),
Text(
(index + 1).toString().padLeft(2, '0'),
style: TextStyle(
color: tokens.mutedInk,
fontWeight: FontWeight.w800,
),
),
],
),
const Spacer(),
Text(
section.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 6),
Text(
section.description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: tokens.mutedInk, height: 1.25),
),
],
),
),
),
);
}
}
class _ComponentDetailScreen extends StatelessWidget {
const _ComponentDetailScreen({required this.index, required this.section});
final int index;
final _DemoSection section;
@override
Widget build(BuildContext context) {
final tokens = FluxAiTheme.of(context);
return Scaffold(
appBar: AppBar(
title: Text(section.title),
backgroundColor: tokens.canvas,
surfaceTintColor: tokens.canvas,
),
body: SafeArea(
child: ListView(
padding: const EdgeInsets.fromLTRB(16, 6, 16, 28),
children: [
Row(
children: [
Container(
width: 46,
height: 46,
decoration: BoxDecoration(
color: tokens.accent.withValues(alpha: .12),
borderRadius: BorderRadius.circular(14),
),
child: Icon(section.icon, color: tokens.accent),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Component ${(index + 1).toString().padLeft(2, '0')}',
style: TextStyle(
color: tokens.mutedInk,
fontWeight: FontWeight.w800,
),
),
Text(
section.title,
style: Theme.of(context).textTheme.headlineSmall,
),
],
),
),
],
),
const SizedBox(height: 10),
Text(
section.description,
style: TextStyle(color: tokens.mutedInk, fontSize: 16),
),
const SizedBox(height: 18),
section.child,
const SizedBox(height: 18),
FluxPanel(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Usage', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
Text(
section.usage,
style: TextStyle(color: tokens.mutedInk, height: 1.35),
),
],
),
),
],
),
),
);
}
}
class _DemoSection {
const _DemoSection({
required this.title,
required this.description,
required this.icon,
required this.child,
required this.usage,
});
final String title;
final String description;
final IconData icon;
final Widget child;
final String usage;
}
final _sections = <_DemoSection>[
const _DemoSection(
title: 'Loading State',
description: 'Pixel-grid loader with shimmer and elapsed time.',
icon: Icons.hourglass_empty,
usage: 'Use FluxLoadingState when an agent is actively producing a result.',
child: FluxLoadingState(),
),
const _DemoSection(
title: 'Thinking',
description:
'Expandable-style traces for steps, reasoning, search, and coding.',
icon: Icons.psychology_alt_outlined,
usage:
'Use FluxThinkingTrace to show visible agent progress and tool work.',
child: FluxThinkingTrace(
steps: [
FluxTraceStep(
title: 'Read signals',
detail: 'Inspecting sources and constraints.',
duration: '2s',
),
FluxTraceStep(
title: 'Search catalog',
detail: 'Looking for related supply records.',
icon: Icons.search,
duration: '3s',
),
FluxTraceStep(
title: 'Prepare patch',
detail: 'Writing a small focused change.',
icon: Icons.code,
state: FluxTaskState.running,
),
],
),
),
const _DemoSection(
title: 'Streaming Text',
description:
'Streamed answer with inline sources, actions, and follow-ups.',
icon: Icons.notes,
usage:
'Use FluxStreamingText for generated answers that need sources and next actions.',
child: FluxStreamingText(
text:
'Pistachio is your fastest-growing flavor — sales are up 23% this month and margins beat vanilla by 8 points.',
sources: [
FluxSource(title: 'Scoop Data', subtitle: 'scoopdata.io'),
FluxSource(title: 'Trends Index', subtitle: 'trends.google.com'),
FluxSource(title: 'Market Basket', subtitle: 'marketbasket.io'),
],
followUps: [
'Which flavors sell best in winter',
'Compare gelato and soft serve margins',
],
),
),
const _DemoSection(
title: 'Approval Card',
description: 'Human-in-the-loop questions before the agent acts.',
icon: Icons.fact_check_outlined,
usage:
'Use FluxApprovalCard before irreversible, costly, or ambiguous agent actions.',
child: FluxApprovalCard(
question: 'How many flavors should we launch?',
options: ['Three (core line)', 'Five (full case)', 'Just one hero'],
),
),
const _DemoSection(
title: 'Tool Chips',
description: 'Code edits and tool calls as compact chips.',
icon: Icons.terminal,
usage:
'Use FluxToolChips to summarize tools, messages, patches, and external calls.',
child: FluxToolChips(
tools: [
FluxChipData('4 tool calls', icon: Icons.terminal, selected: true),
FluxChipData('2 messages', icon: Icons.chat_bubble_outline),
FluxChipData('1 patch', icon: Icons.edit_note),
],
),
),
const _DemoSection(
title: 'Task Rows',
description:
'Live agent task status for running, failed, and completed work.',
icon: Icons.task_alt,
usage:
'Use FluxTaskRows for multi-step jobs, background runs, and task queues.',
child: FluxTaskRows(
tasks: [
FluxTaskItem(
title: 'Verified vendor records',
subtitle: '12 suppliers completed',
state: FluxTaskState.completed,
),
FluxTaskItem(
title: 'Build reorder task list',
subtitle: '7 SKUs scored',
state: FluxTaskState.running,
progress: .68,
),
FluxTaskItem(
title: 'Draft supplier emails',
subtitle: '2 messages',
state: FluxTaskState.completed,
),
],
),
),
const _DemoSection(
title: 'Chat',
description: 'Tabbed chat panel with reasoning replies and a composer.',
icon: Icons.chat_bubble_outline,
usage:
'Use FluxChatPanel as a compact chat surface inside a task or workspace.',
child: FluxChatPanel(),
),
const _DemoSection(
title: 'Prompt Bar',
description:
'Composer with @ sources, commands, model picker, and send action.',
icon: Icons.keyboard_command_key,
usage:
'Use FluxPromptBar as the primary input for agentic mobile workflows.',
child: FluxPromptBar(),
),
const _DemoSection(
title: 'Recommendation Card',
description: 'Agent suggestion with confidence and actions.',
icon: Icons.recommend_outlined,
usage:
'Use FluxRecommendationCard when the agent proposes a decision and confidence.',
child: FluxRecommendationCard(),
),
const _DemoSection(
title: 'Context Cards',
description: 'Retrieved knowledge chunks with their sources.',
icon: Icons.article_outlined,
usage:
'Use FluxContextCards to explain where an answer or decision came from.',
child: FluxContextCards(
sources: [
FluxSource(
title: 'Vendor onboarding rule',
subtitle:
'Cold-chain certification must be verified before a new dairy can be added.',
),
FluxSource(
title: 'Seasonal demand row',
subtitle:
'Q4 velocity table: pistachio +18%, vanilla +6%, rocky road -11%.',
),
],
),
),
const _DemoSection(
title: 'Diff Table',
description: 'AI-proposed edits sweeping through tabular data.',
icon: Icons.difference_outlined,
usage:
'Use FluxDiffTable to preview AI-proposed row additions before applying table edits.',
child: FluxDiffTable(),
),
const _DemoSection(
title: 'Records Table',
description: 'CRM-style grid with tags, sorting, and relationship status.',
icon: Icons.table_chart_outlined,
usage:
'Use FluxRecordsTable for compact CRM-style records and entity lists.',
child: FluxRecordsTable(),
),
const _DemoSection(
title: 'Filter Table',
description: 'Status chips that reorganize live data.',
icon: Icons.filter_alt_outlined,
usage: 'Use FluxFilterTable to communicate active status filters.',
child: FluxFilterTable(),
),
const _DemoSection(
title: 'Search',
description: 'Command search with live filtering and an empty state.',
icon: Icons.search,
usage: 'Use FluxSearchPanel for command palettes and source/action lookup.',
child: FluxSearchPanel(
commands: [
'Forecast summer demand',
'Find waffle cone suppliers',
'Compare seasonal flavors',
'Draft flavor launch plan',
'Check cold-chain status',
],
),
),
const _DemoSection(
title: 'Insight Cards',
description: 'Paged agent insights with scrub-ready live charts.',
icon: Icons.insights_outlined,
usage:
'Use FluxInsightCards to present AI-discovered changes and opportunities.',
child: FluxInsightCards(),
),
const _DemoSection(
title: 'Code Block',
description: 'Agent-written code streaming in line by line.',
icon: Icons.code,
usage:
'Use FluxCodeBlock when generated code needs readable mobile presentation.',
child: FluxCodeBlock(
filename: 'churn.ts',
code:
'export async function churnBatch() {\n'
' const flavor = await getFlavor("pistachio");\n'
' const base = await dairy.fetch({ flavor });\n'
' await freezer.store(base, { temp: "-14C" });\n'
' return base.gallons;\n'
'}',
),
),
const _DemoSection(
title: 'Fine-tune Card',
description: 'Agent adjusts design properties in an inspector.',
icon: Icons.tune,
usage: 'Use FluxFineTuneCard for editable AI parameters and live previews.',
child: FluxFineTuneCard(),
),
];