genui e il protocollo A2UIDati INGV veri, ma il modello risponde solo a parole.
Una sola UI possibile: la bolla di testo. Tutto diventa prosa.
markdown · liste · tabelle ASCII
L’AI sceglie anche la
forma, con i mattoncini generici di genui.
Text · Card · Column · Button
Catalogo arricchito con i tuoi widget. Componenti pensati per i tuoi dati.
EventCard · MapCard · TimelineChart
L’agente sceglie anche la presentazione, non solo il contenuto.
Decide che cosa dire.
risposta : String
Decide anche come mostrarlo.
risposta : albero di widget
Niente eval.
Niente compilatore a bordo. Non esiste in Dart, e non lo vuoi.
È dati, non codice.
EventCard esiste perché l’hai scritta tu e
l’hai messa nel catalogo.
genui), Angular.
React in arrivo, SwiftUI e Jetpack Compose in roadmap.Sicuro: sono dati, non c’è niente da eseguire.
Veloce: gli LLM lo scrivono bene, i client lo leggono subito.
Ovunque: lo legge qualunque linguaggio. Il tuo catalogo resta l’unica cosa specifica della piattaforma.
genui| Package | genui —
non più
flutter_genui |
|---|---|
| Versione | 0.10.3 alpha
· ultima 0.10.4 |
| Riscrittura | maggio 2026, sopra A2UI v0.9 |
| In arrivo | spezzato in a2ui_core,
a2ui_agent, a2ui_flutter |
| Piattaforme | Android · iOS · macOS · web |
ContentGenerator non esiste
più: metà dei tutorial che trovate online è già
obsoleta.
Pinnate la versione. In alpha un minor può cambiarvi l’API sotto i piedi. E il prossimo cambio di nome è già annunciato.
main metti
questo albero».surface
// chat_page.dart — una surface per risposta
for (final surfaceId in turn.surfaceIds)
Surface(
key: ValueKey(surfaceId),
surfaceContext: bloc.agent.controller
.contextFor(surfaceId),
),
// map_card_item.dart — il tap su un epicentro
onEventTap: (event) => itemContext.dispatchEvent(
UserActionEvent(
name: 'map_epicenter_tapped',
surfaceId: itemContext.surfaceId,
sourceComponentId: itemContext.id,
context: {'eventId': event.eventId,
'place': event.place},
),
),
Catalog e CatalogItemIl vocabolario che il modello ha il permesso di usare.
catalog
abstract final class EtnaCatalog {
static const String catalogId = 'flutter-catania.dev:etna';
static final List<CatalogItem> items = [
eventCardItem,
mapCardItem,
timelineChartItem,
comparisonCardItem,
];
static Catalog asCatalog() => BasicCatalogItems.asNoAssetCatalog().copyWith(
newItems: items, catalogId: catalogId, systemPromptFragments: [_rules]);
}
CatalogItem sopra i basic.
Il modello non può nominare nient’altro.json_schema_builderschema
final Schema seismicEventSchema = S.object(
properties: {
'time': S.string(description: 'Istante dell\'evento in formato '
'ISO 8601 UTC, es. 2026-09-18T03:14:00Z.'),
'magnitude': S.number(description: 'Magnitudo, es. 3.4.'),
'place': S.string(description: 'Località di riferimento, es. '
'"1 km N Ragalna (CT)".'),
}, // ... eventId, magnitudeType, latitude, longitude, depthKm
required: ['eventId', 'time', 'magnitude', 'magnitudeType', 'place',
'latitude', 'longitude', 'depthKm'],
);
repository
// lib/data/ingv_repository.dart
static const double minLatitude = 37.4;
static const double maxLatitude = 37.9;
static const double minLongitude = 14.7;
static const double maxLongitude = 15.3;
final uri = Uri.https(_host, _path, {
'starttime': _formatTime(startTime),
if (endTime != null)
'endtime': _formatTime(endTime),
'minmagnitude': minMagnitude.toString(),
'minlat': EtnaBounds.minLatitude.toString(),
'maxlat': EtnaBounds.maxLatitude.toString(),
'minlon': EtnaBounds.minLongitude.toString(),
'maxlon': EtnaBounds.maxLongitude.toString(),
'format': 'geojson',
'limit': limit.toString(),
});
Access-Control-Allow-Origin: * → funziona anche
dal browser.tool
static final FunctionDeclaration searchEarthquakes = FunctionDeclaration(
searchEarthquakesName, // 'searchEarthquakes'
'Cerca i terremoti registrati dall\'INGV nell\'area dell\'Etna. '
'Usa questa funzione ogni volta che servono dati sismici reali: '
'non inventare mai magnitudo, date o località.',
parameters: {
'daysBack': Schema.integer(description: 'Quanti giorni indietro cercare a partire da oggi. '
'Es. 7 per "questa settimana", 30 per "questo mese".'),
// ... minMagnitude, limit, offsetDays
},
optionalParameters: ['minMagnitude', 'limit', 'offsetDays'],
);
// gli handler, nello stesso file, indicizzati per nome:
Map<String, ToolHandler> get handlers => {searchEarthquakesName: _handleSearchEarthquakes};
event_card.dart
class EventCard extends StatelessWidget {
const EventCard({super.key, required this.event, this.onTap});
final SeismicEvent event;
/// Tap opzionale: in app lo collega l'adapter, nel previewer resta null.
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
/* ... */
}
}
@Preview(name: 'EventCard — scossa forte', group: 'Catalogo Etna',
size: Size(460, 220))
Widget eventCardStrong() => EventCard(event: _strongQuake);
Un solo pannello con tutto il catalogo: le quattro card, in light, dark e casi limite, senza lanciare l’app e senza Firebase.
CatalogItemevent_card_item.dart
final CatalogItem eventCardItem = CatalogItem(
name: 'EventCard',
dataSchema: S.object(
description: 'Il dettaglio di UNA singola scossa. Usala quando l\'utente '
'chiede di un evento specifico, per esempio "la scossa più forte".',
properties: {'event': seismicEventSchema}, required: ['event']),
widgetBuilder: (itemContext) {
final data = itemContext.data as Map<String, Object?>;
final event = parseSeismicEvent(data['event']! as Map<String, Object?>);
return EventCard(event: event, onTap: () => itemContext.dispatchEvent(
UserActionEvent(name: 'event_card_tapped',
surfaceId: itemContext.surfaceId, /* ... */)));
},
);
il nome che il modello può scrivere
il contratto: cosa può passare
la traduzione JSON → pixel
prompt
PromptBuilder.chat(catalog: catalog).systemPromptJoined() // response_mode.dart
systemInstruction: Content.system(mode.systemPrompt), // etna_agent.dart
// lib/catalog/etna_catalog.dart — le TUE regole, iniettate nel prompt
static const String _rules = '''
Sei l'assistente sismico dell'Etna. Rispondi in italiano.
SCELTA DELLA CARD — è la decisione più importante che prendi:
- EventCard: una singola scossa in dettaglio ("la scossa più forte").
- MapCard: DOVE sono avvenute le scosse, o il quadro di uno sciame.
- TimelineChart: QUANDO sono avvenute, l'andamento nel tempo.
- ComparisonCard: il paragone fra due periodi ("confronta con agosto").
DATI:
- Chiama sempre searchEarthquakes prima di parlare di scosse.
- Non inventare MAI magnitudo, date, località o identificativi: ...
''';
“cosa è successo sull’Etna questa settimana?”
↓
MapCard + TimelineChart
“dimmi della scossa più forte”
↓
EventCard dettagliata
“confronta con il mese scorso”
↓
ComparisonCard
Prima con i mattoncini generici di genui, poi con
le card Etna.
Se il layout è un
requisito e non un’opinione,
non è un caso da GenUI.