Skip to main content

laya_dart 0.0.2 copy "laya_dart: ^0.0.2" to clipboard
laya_dart: ^0.0.2 copied to clipboard

Run local Laya decision models in Flutter using ONNX Runtime.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:laya_dart/laya_dart.dart';
import 'demo/snake_game.dart';

const _sampleEmails = {
  'Billing dispute + churn risk':
      'Hi, we were billed twice for our March subscription. Please '
      'refund the duplicate charge today or we will have to cancel our plan and move to a '
      'competitor. This is the second billing issue this quarter.',
  'Technical bug report':
      "The export button on the dashboard throws a 500 error every time I "
      "click it, on both Chrome and Safari. This is blocking our end-of-month reporting. Can "
      "someone from engineering take a look?",
  'Sales enquiry':
      'Hello, we are evaluating your platform for a 200-seat rollout next quarter. '
      'Could you send pricing for the enterprise tier and let us know about volume discounts?',
  'Urgent refund threat':
      "I want a refund immediately. Your product does not work as advertised "
      "and I have already wasted three days trying to fix it myself. If I don't hear back by "
      "end of day I'm filing a chargeback and cancelling my account.",
  'Low-key technical question':
      'Quick question - is there a way to export reports as CSV instead '
      'of PDF? Not urgent, just curious if it is on the roadmap.',
  'Happy renewal, no issues':
      "Just wanted to say the new dashboard update is great, our team is "
      "really enjoying it. No issues to report, just renewed for another year.",
};

void main() {
  runApp(const LayaDemoApp());
}

class LayaDemoApp extends StatelessWidget {
  const LayaDemoApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Laya Email Triage Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const LayaDemoPage(),
    );
  }
}

class LayaDemoPage extends StatefulWidget {
  const LayaDemoPage({super.key});

  @override
  State<LayaDemoPage> createState() => _LayaDemoPageState();
}

class _LayaDemoPageState extends State<LayaDemoPage> {
  final _controller = TextEditingController(text: _sampleEmails.values.first);
  String? _selectedSample = _sampleEmails.keys.first;
  Laya? _agent;
  Map<String, Object>? _result;
  bool _modelLoading = false;
  bool _analyzing = false;
  ModelDownloadProgress? _modelProgress;
  String? _modelError;
  String? _error;

  @override
  void dispose() {
    _controller.dispose();
    _agent?.close();
    super.dispose();
  }

  Future<void> _analyze() async {
    setState(() {
      _analyzing = true;
      _error = null;
      _result = null;
    });
    try {
      final result = await _agent!.predictAsync(_controller.text, {
        'department': LayaQuestion.choice(
          instructions: 'Which department should handle this request?',
          criteria: {
            'billing': 'invoices, payments, refunds',
            'technical': 'bugs, outages, system errors',
            'sales': 'pricing, new contracts',
            'other': 'everything else',
          },
        ),
        'urgency': LayaQuestion.score(
          instructions: 'How urgent is this request?',
          criteria: [
            'not urgent',
            'soon',
            'critical deadline or blocking issue',
          ],
        ),
        'churn_risk': LayaQuestion.noul(
          instructions: 'Does the sender threaten to cancel or leave?',
        ),
        'refund_requested': LayaQuestion.noul(
          instructions: 'Does the sender explicitly request a refund?',
        ),
      });
      setState(() => _result = result);
    } catch (e) {
      setState(() => _error = '$e');
    } finally {
      setState(() => _analyzing = false);
    }
  }

  Future<void> _loadModel() async {
    setState(() {
      _modelLoading = true;
      _modelError = null;
      _modelProgress = null;
    });
    try {
      _agent = await Laya.load(null, (progress) {
        if (mounted) setState(() => _modelProgress = progress);
      });
    } catch (e) {
      setState(() => _modelError = '$e');
    } finally {
      if (mounted) setState(() => _modelLoading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    final answers = _result?['answers'] as Map<String, Object>?;
    return Scaffold(
      appBar: AppBar(
        title: const Text('Laya Email Triage Demo'),
        actions: [
          IconButton(
            tooltip: 'Open Laya Snake',
            icon: const Icon(Icons.sports_esports),
            onPressed: () => Navigator.of(
              context,
            ).push(MaterialPageRoute(builder: (_) => const SnakeGamePage())),
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: ListView(
          children: [
            const Text(
              'Sample emails',
              style: TextStyle(fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: [
                for (final name in _sampleEmails.keys)
                  ChoiceChip(
                    label: Text(name),
                    selected: _selectedSample == name,
                    onSelected: (_) => setState(() {
                      _selectedSample = name;
                      _controller.text = _sampleEmails[name]!;
                    }),
                  ),
              ],
            ),
            const SizedBox(height: 16),
            const Text(
              'State (email body)',
              style: TextStyle(fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            TextField(
              controller: _controller,
              maxLines: 6,
              onChanged: (_) => setState(() => _selectedSample = null),
              decoration: const InputDecoration(
                border: OutlineInputBorder(),
                hintText: 'Paste an email...',
              ),
            ),
            const SizedBox(height: 16),
            FilledButton.icon(
              onPressed: _modelLoading || _agent != null ? null : _loadModel,
              icon: _modelLoading
                  ? const SizedBox(
                      width: 16,
                      height: 16,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  : Icon(_agent == null ? Icons.download : Icons.check),
              label: Text(
                _modelLoading
                    ? 'Loading model…'
                    : _agent == null
                    ? 'Load model'
                    : 'Model ready',
              ),
            ),
            if (_modelLoading && _modelProgress != null) ...[
              const SizedBox(height: 8),
              _ModelProgress(progress: _modelProgress!),
            ],
            if (_modelError != null) ...[
              const SizedBox(height: 8),
              Text(_modelError!, style: const TextStyle(color: Colors.red)),
            ],
            const SizedBox(height: 12),
            FilledButton.icon(
              onPressed: _agent == null || _analyzing ? null : _analyze,
              icon: _analyzing
                  ? const SizedBox(
                      width: 16,
                      height: 16,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  : const Icon(Icons.psychology),
              label: Text(_analyzing ? 'Running Laya…' : 'Analyze with Laya'),
            ),
            const SizedBox(height: 24),
            if (_error != null)
              Text(_error!, style: const TextStyle(color: Colors.red)),
            if (answers != null) ...[
              _ChoiceResultCard(
                title: 'Department',
                answer: answers['department'] as Map<String, Object>,
              ),
              const SizedBox(height: 12),
              _ScoreResultCard(
                title: 'Urgency',
                answer: answers['urgency'] as Map<String, Object>,
              ),
              const SizedBox(height: 12),
              _NoulResultCard(
                title: 'Churn risk',
                flaggedLabel: 'Customer may cancel',
                safeLabel: 'No churn signal',
                answer: answers['churn_risk'] as Map<String, Object>,
              ),
              const SizedBox(height: 12),
              _NoulResultCard(
                title: 'Refund requested',
                flaggedLabel: 'Refund requested',
                safeLabel: 'No refund requested',
                answer: answers['refund_requested'] as Map<String, Object>,
              ),
            ],
          ],
        ),
      ),
    );
  }
}

class _ModelProgress extends StatelessWidget {
  const _ModelProgress({required this.progress});

  final ModelDownloadProgress progress;

  @override
  Widget build(BuildContext context) {
    final totalBytes = progress.totalBytes;
    final fraction = totalBytes == null || totalBytes == 0
        ? null
        : progress.bytesReceived / totalBytes;
    final byteStatus = totalBytes == null
        ? _formatBytes(progress.bytesReceived)
        : '${_formatBytes(progress.bytesReceived)} / ${_formatBytes(totalBytes)}';
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        LinearProgressIndicator(value: fraction),
        const SizedBox(height: 6),
        Text(
          'Downloading ${progress.fileName} '
          '(${progress.completedFiles + 1}/${progress.totalFiles}) · $byteStatus',
        ),
      ],
    );
  }

  String _formatBytes(int bytes) {
    if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(0)} KB';
    return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
  }
}

class _ChoiceResultCard extends StatelessWidget {
  const _ChoiceResultCard({required this.title, required this.answer});

  final String title;
  final Map<String, Object> answer;

  @override
  Widget build(BuildContext context) {
    final choice = answer['choice'] as String;
    final probabilities = Map<String, double>.from(
      (answer['probabilities'] as Map).map(
        (k, v) => MapEntry(k as String, (v as num).toDouble()),
      ),
    );
    final confidence = (answer['confidence'] as num).toDouble();
    final sortedLabels = probabilities.keys.toList()
      ..sort((a, b) => probabilities[b]!.compareTo(probabilities[a]!));
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(title, style: Theme.of(context).textTheme.titleMedium),
            const SizedBox(height: 4),
            Row(
              children: [
                Chip(label: Text(choice.toUpperCase())),
                const SizedBox(width: 8),
                Text('confidence ${(confidence * 100).toStringAsFixed(0)}%'),
              ],
            ),
            const SizedBox(height: 8),
            for (final label in sortedLabels)
              _ProbabilityBar(label: label, value: probabilities[label]!),
          ],
        ),
      ),
    );
  }
}

class _NoulResultCard extends StatelessWidget {
  const _NoulResultCard({
    required this.title,
    required this.answer,
    required this.flaggedLabel,
    required this.safeLabel,
  });

  final String title;
  final Map<String, Object> answer;
  final String flaggedLabel;
  final String safeLabel;

  @override
  Widget build(BuildContext context) {
    final noul = (answer['noul'] as num).toDouble();
    final flagged = noul > 0.5;
    return Card(
      color: flagged ? Colors.orange.shade50 : Colors.green.shade50,
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(title, style: Theme.of(context).textTheme.titleMedium),
            const SizedBox(height: 4),
            Row(
              children: [
                Icon(
                  flagged ? Icons.warning_amber : Icons.check_circle,
                  color: flagged ? Colors.orange : Colors.green,
                ),
                const SizedBox(width: 8),
                Text(flagged ? flaggedLabel : safeLabel),
              ],
            ),
            const SizedBox(height: 8),
            _ProbabilityBar(label: 'P(true)', value: noul),
          ],
        ),
      ),
    );
  }
}

class _ScoreResultCard extends StatelessWidget {
  const _ScoreResultCard({required this.title, required this.answer});

  final String title;
  final Map<String, Object> answer;

  @override
  Widget build(BuildContext context) {
    final score = (answer['score'] as num).toDouble();
    final legend = Map<String, String>.from(
      (answer['legend'] as Map).map(
        (k, v) => MapEntry(k as String, v as String),
      ),
    );
    final maxLevel = legend.length - 1;
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(title, style: Theme.of(context).textTheme.titleMedium),
            const SizedBox(height: 4),
            Text('${score.toStringAsFixed(2)} / $maxLevel'),
            const SizedBox(height: 8),
            LinearProgressIndicator(
              value: maxLevel == 0 ? 0 : (score / maxLevel).clamp(0, 1),
            ),
          ],
        ),
      ),
    );
  }
}

class _ProbabilityBar extends StatelessWidget {
  const _ProbabilityBar({required this.label, required this.value});

  final String label;
  final double value;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 2),
      child: Row(
        children: [
          SizedBox(width: 70, child: Text(label)),
          Expanded(child: LinearProgressIndicator(value: value.clamp(0, 1))),
          const SizedBox(width: 8),
          SizedBox(
            width: 44,
            child: Text('${(value * 100).toStringAsFixed(0)}%'),
          ),
        ],
      ),
    );
  }
}
2
likes
0
points
493
downloads

Publisher

unverified uploader

Weekly Downloads

Run local Laya decision models in Flutter using ONNX Runtime.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

crypto, flutter, http, onnxruntime_v2, path_provider, unorm_dart

More

Packages that depend on laya_dart