Bien cool ce petit monitoring, veux-tu bien partager ton mode opératoire avec Claude et Cursor ? Je découvre.
Je vois que tu es en train de répondre, mais du coup j’ai testé Cursor à l’instant, en littéralement 2 minutes j’ai produit cette petite application qui montre le bloc courant de la ĞDev (en temps réel) :
Le prompt donné à Cursor :
I would like you to initiate a Nuxt 3 based project, with a single screen showing in real time the current block of a Polkadot based blockchain whose RPC API is located at wss://gdev.cgeek.fr.
Please also:
- generate the package.json file
- typescript configuration file
Cursor est un IDE, fork de vs-codium qui focus sur l’intégration poussé de LLMs.
Donc je sais que comme tu es habitué aux IDE de JetBrains, ce n’est peut être pas parfaitement adapté à ton usage.
Globalement j’ai ouvert une nouvelle fenêtre, ctrl + i
pour ouvrir le composer, et prompté ce que je voulais:
Mon prompt initial (avec ses fautes, imperfections et imprécisions)
Ta mission est de créer une page web ayant pour but de monitorer les noeuds du Réseau Duniter listés dans ce json: https://git.duniter.org/nodes/networks/-/raw/master/gdev.json?ref_type=heads
le bloc rpc étant les noeud Duniter, le bloc squid étant les instances de l’indexer.
Il faut quelque de sobre, simple mais moderne, pour voir si les noeuds et isntances sont synchronisés.
Pour l’indexer squid c’est du graphql, tu peux t’inspirer de ce code Dart pour check la sync:
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gecko/globals.dart';
import 'package:gecko/models/queries_indexer.dart';
import 'package:gecko/providers/home.dart';
import 'package:gecko/providers/substrate_sdk.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:provider/provider.dart';
import 'package:gecko/models/transaction.dart';
class DuniterIndexer with ChangeNotifier {
Map<String, String?> walletNameIndexer = {};
String? fetchMoreCursor;
Map? pageInfo;
List<Transaction>? transBC;
List listIndexerEndpoints = [];
bool isLoadingIndexer = false;
Future<QueryResult<Object?>?> Function()? refetch;
late GraphQLClient indexerClient;
void reload() {
notifyListeners();
}
Future<bool> checkIndexerEndpoint(String endpoint) async {
isLoadingIndexer = true;
notifyListeners();
final client = HttpClient();
client.connectionTimeout = const Duration(milliseconds: 4000);
try {
final request = await client.postUrl(Uri.parse('https://$endpoint/v1beta1/relay'));
final response = await request.close();
if (response.statusCode != 200) {
log.w('Indexer $endpoint is offline');
indexerEndpoint = '';
isLoadingIndexer = false;
notifyListeners();
return false;
} else {
final isSynced = await isIndexerSynced('https://$endpoint/v1/graphql');
if (!isSynced) {
log.e('This endpoint is not synced, next');
return false;
}
indexerEndpoint = endpoint;
await configBox.put('indexerEndpoint', endpoint);
// await configBox.put('customEndpoint', endpoint);
isLoadingIndexer = false;
notifyListeners();
final cache = HiveStore();
cache.reset();
return true;
}
} catch (e) {
log.w('Indexer $endpoint is offline');
indexerEndpoint = '';
isLoadingIndexer = false;
notifyListeners();
return false;
}
}
Future<String> getValidIndexerEndpoint() async {
final homeProvider = Provider.of<HomeProvider>(homeContext, listen: false);
// await configBox.delete('indexerEndpoint');
listIndexerEndpoints = await rootBundle.loadString('config/indexer_endpoints.json').then((jsonStr) => jsonDecode(jsonStr));
// _listEndpoints.shuffle();
listIndexerEndpoints.add('Personnalisé');
if (configBox.containsKey('customIndexer')) {
if (await checkIndexerEndpoint(configBox.get('customIndexer'))) {
succesConnection(indexerEndpoint);
return indexerEndpoint;
}
}
if (configBox.containsKey('indexerEndpoint') && listIndexerEndpoints.contains(configBox.get('indexerEndpoint'))) {
if (await checkIndexerEndpoint(configBox.get('indexerEndpoint'))) {
succesConnection(indexerEndpoint);
return indexerEndpoint;
}
}
int i = 0;
// String _endpoint = '';
int statusCode = 0;
final client = HttpClient();
client.connectionTimeout = const Duration(milliseconds: 3000);
do {
final listLenght = listIndexerEndpoints.length - 1;
if (i >= listLenght) {
log.e('NO VALID INDEXER ENDPOINT FOUND');
indexerEndpoint = '';
break;
}
log.d('${i + 1}n indexer endpoint try: ${listIndexerEndpoints[i]}');
if (i != 0) {
await Future.delayed(const Duration(milliseconds: 300));
}
try {
final endpointPath = 'https://${listIndexerEndpoints[i]}/v1beta1/relay';
final request = await client.postUrl(Uri.parse(endpointPath));
final response = await request.close();
final isSynced = await isIndexerSynced('https://${listIndexerEndpoints[i]}/v1/graphql');
if (!isSynced) {
log.e('This endpoint is not synced, next');
statusCode = 40;
i++;
continue;
}
indexerEndpoint = listIndexerEndpoints[i];
await configBox.put('indexerEndpoint', listIndexerEndpoints[i]);
statusCode = response.statusCode;
i++;
} on TimeoutException catch (_) {
log.e('This endpoint is timeout, next');
statusCode = 50;
i++;
continue;
} on SocketException catch (_) {
log.e('This endpoint is a bad endpoint, next');
statusCode = 70;
i++;
continue;
} on Exception {
log.e('Unknown error');
statusCode = 60;
i++;
continue;
}
} while (statusCode != 200);
if (indexerEndpoint == '') {
log.e('NO VALID INDEXER ENDPOINT FOUND');
homeProvider.changeMessage("No valid indexer found =(".tr());
return '';
}
succesConnection(indexerEndpoint);
return indexerEndpoint;
}
void succesConnection(String endpoint) {
final homeProvider = Provider.of<HomeProvider>(homeContext, listen: false);
final wsLinkIndexer = WebSocketLink(
'wss://$endpoint/v1beta1/relay',
);
indexerClient = GraphQLClient(
cache: GraphQLCache(),
link: wsLinkIndexer,
);
// Indexer Blockchain start
getBlockStart();
homeProvider.changeMessage("Node and indexer synced !".tr(), true);
log.i('Indexer: $indexerEndpoint');
}
Future<bool> isIndexerSynced(String endpoint) async {
try {
final sub = Provider.of<SubstrateSdk>(homeContext, listen: false);
var duniterFinilizedHash = await sub.getLastFinilizedHash();
final duniterFinilizedNumber = await sub.getBlockNumberByHash(duniterFinilizedHash);
duniterFinilizedHash = "\\x${duniterFinilizedHash.substring(2)}";
final indexerLink = HttpLink(endpoint);
final iClient = GraphQLClient(
cache: GraphQLCache(),
link: indexerLink,
);
final result = await iClient.query(QueryOptions(document: gql(getBlockByHash), variables: <String, dynamic>{'hash': duniterFinilizedHash}));
if (result.hasException || result.data == null || result.data!['block'].isEmpty) {
log.e('Indexer is not synced: ${result.exception} -- ${result.data}');
return false;
}
final indexerFinilizedNumber = result.data!['block'][0]['height'] as int;
if (duniterFinilizedNumber != indexerFinilizedNumber) {
log.e('Indexer is not synced');
return false;
}
return true;
} catch (e) {
log.e('An error occured while checking indexer sync: $e');
return false;
}
}
List<Transaction> parseHistory(List blockchainTX, String address) {
// Create a list to store Transaction objects
List<Transaction> transactions = [];
for (final transactionNode in blockchainTX) {
final transaction = transactionNode['node'];
final isReceived = transaction['fromId'] != address;
// Calculate amount
final amount = transaction['amount'] as int;
final comment = transaction['comment']?['remark'] ?? '';
final commentType = transaction['comment']?['type'] ?? '';
// Determine counterparty based on direction
final String counterPartyId;
final String counterPartyName;
if (isReceived) {
counterPartyId = transaction['fromId'];
counterPartyName = transaction['from']['identity']?['name'] ?? '';
} else {
counterPartyId = transaction['toId'];
counterPartyName = transaction['to']['identity']?['name'] ?? '';
}
// Create and add new Transaction object
transactions.add(
Transaction(
timestamp: DateTime.parse(transaction['timestamp']),
address: counterPartyId,
username: counterPartyName,
amount: amount,
comment: commentType == 'ASCII' || commentType == 'UNICODE' ? comment : '',
isReceived: isReceived,
),
);
}
return transactions;
}
FetchMoreOptions? mergeQueryResult(QueryResult result, FetchMoreOptions? opts, String address, int nRepositories) {
final List<dynamic> blockchainTX = (result.data!['transferConnection']['edges'] as List<dynamic>);
pageInfo = result.data!['transferConnection']['pageInfo'];
fetchMoreCursor = pageInfo!['endCursor'];
// final hasNextPage = pageInfo!['hasNextPage'];
// log.d('endCursor: $fetchMoreCursor $hasNextPage');
if (fetchMoreCursor != null) {
opts = FetchMoreOptions(
variables: {'after': fetchMoreCursor, 'first': nRepositories},
updateQuery: (previousResultData, fetchMoreResultData) {
final List<dynamic> repos = [
...previousResultData!['transferConnection']['edges'] as List<dynamic>,
...fetchMoreResultData!['transferConnection']['edges'] as List<dynamic>
];
fetchMoreResultData['transferConnection']['edges'] = repos;
return fetchMoreResultData;
},
);
}
if (fetchMoreCursor != null) {
transBC = parseHistory(blockchainTX, address);
} else {
log.d("Activity start of $address");
}
return opts;
}
//// Manuals queries
Future<bool> isIdtyExist(String name) async {
final variables = <String, dynamic>{
'name': name,
};
final result = await _execQuery(isIdtyExistQ, variables);
return result.data?['identityConnection']['edges']?.isNotEmpty ?? false;
}
Future<DateTime> getBlockStart() async {
final result = await _execQuery(getBlockchainStartQ, {});
if (!result.hasException) {
startBlockchainTime = DateTime.parse(result.data!['blockConnection']['edges'][0]['node']['timestamp']);
startBlockchainInitialized = true;
return startBlockchainTime;
}
return DateTime(0, 0, 0, 0, 0);
}
Future<QueryResult> _execQuery(String query, Map<String, dynamic> variables) async {
final options = QueryOptions(document: gql(query), variables: variables);
// 5GMyvKsTNk9wDBy9jwKaX6mhSzmFFtpdK9KNnmrLoSTSuJHv
return await indexerClient.query(options);
}
Stream<QueryResult> subscribeHistoryIssued(String address) {
final variables = <String, dynamic>{
'address': address,
};
final options = SubscriptionOptions(
document: gql(subscribeHistoryIssuedQ),
variables: variables,
);
return indexerClient.subscribe(options);
}
Map computeHistoryView(Transaction transaction, String address) {
final DateTime date = transaction.timestamp;
final dateForm = "${date.day} ${monthsInYear[date.month]!.substring(0, {1, 2, 7, 9}.contains(date.month) ? 4 : 3)}";
DateTime normalizeDate(DateTime inputDate) {
return DateTime(inputDate.year, inputDate.month, inputDate.day);
}
String getDateDelimiter() {
DateTime now = DateTime.now();
final transactionDate = normalizeDate(date.toLocal());
final todayDate = normalizeDate(now);
final yesterdayDate = normalizeDate(now.subtract(const Duration(days: 1)));
final isSameWeek = weekNumber(transactionDate) == weekNumber(now) && transactionDate.year == now.year;
final isTodayOrYesterday = transactionDate == todayDate || transactionDate == yesterdayDate;
if (transactionDate == todayDate) {
return "today".tr();
} else if (transactionDate == yesterdayDate) {
return "yesterday".tr();
} else if (isSameWeek && !isTodayOrYesterday) {
return "thisWeek".tr();
} else if (!isSameWeek && !isTodayOrYesterday) {
if (transactionDate.year == now.year) {
return monthsInYear[transactionDate.month]!;
} else {
return "${monthsInYear[transactionDate.month]} ${transactionDate.year}";
}
} else {
return '';
}
}
final dateDelimiter = getDateDelimiter();
final amount = transaction.isReceived ? transaction.amount : transaction.amount * -1;
bool isMigrationTime = startBlockchainInitialized && date.compareTo(startBlockchainTime) < 0;
return {
'finalAmount': amount,
'isMigrationTime': isMigrationTime,
'dateDelimiter': dateDelimiter,
'dateForm': dateForm,
};
}
int weekNumber(DateTime date) {
int dayOfYear = int.parse(DateFormat("D").format(date));
return ((dayOfYear - date.weekday + 10) / 7).floor();
}
}
Concernant les noeuds Duniter, il s’agit de noeud blockchain Substrate, comme Polkadot, donc utilisant une API RPC, je te laisse chercher sur le web comment faire pour check la sync facilement, la dernier blocknumber.
Je lui ai donné du code par flemme de lui expliquer l’architecture, mais ce n’était pas du tout nécessaire, il aurait finit pas trouver de lui même.
Sans donner de directive concernant la stack technologique souhaité (je m’en fiche), il est partie sur une stack React: nodes / v2s-monitor · GitLab
Il m’a créé tous les fichier de config nécessaires à la stack en quelques secondes, il a itéré 2 ou 3 fois sur des erreurs de hints qu’il a généré sans même que j’ai besoin de lui dire, puis m’a laissé la main avec quelque chose de fonctionnel techniquement, mais qui ne chargeais pas la liste des nœuds à cause d’un problème de CORS. Mais les cas d’erreurs étant déjà bien gérés, j’avais quand même une interface avec des listes vides.
J’ai donc simplement interagie avec le composer pour lui donner les erreurs javascript que j’avais (il avait bien accès aux hint de l’IDE, mais pas à la console javascript du navigateur pour le coup), il comprenait alors ce qui n’allait pas et le corrigeait. Je n’avais qu’a accepter ou non par shard ces changements présentés dans mon IDE au format git diff (ou tout accepter d’un coup).
Je n’ai donnée aucune directive d’UI hormis un code couleur (celui du fond de g1-stats, lui même venu de Duniter…) car par defaut il part sur des bleus foncés en général.
Le plus long ayant été de le faire itérer sur les bugs au début, notamment lorsque je lui ait demandé de souscrire en websockets aux noeuds duniter et aux indexer pour afficher en live les numéros de bloc.
Je n’ai quasiment rien codé, et par expérience (ça fait bien 4 mois que je l’utilise quotidiennement comme IDE principal au boulot), que je pars sur une session assisté en mode agent comme ça, je m’efforce même de ne pas corrigé les coquilles à la main pour le laisser faire, pour qu’il comprenne les corrections pour la suite, sans quoi il peut être capable de me les écrasés aux prochains passages (suite à quoi on s’énerve on lui dit que ce n’est qu’une merde, puis il s’excuse et se corrige).
On se retrouve plus dans une position de PO/PM que de dev.
Mais je précise bien que ce résultat n’est évidemment pas le premier jet, ça a été des dizaines de messages d’échanges avec le composer pour en arriver là.
Je voudrais aussi casser le mythe de l’art de savoir bien prompter: On peut lui parler n’importe comment, peu importe, il corrigera le tir très vite après quelques retours. Le tout étant simplement d’être le plus précis possible sur ce qu’on veut, mais bon, tu vois mon prompt de base, il est extrêmement simple et basique, direct, faut pas non plus perdre trop de temps en détail et lui laisser un peu de mou.
Seul une bonne compréhension je dirais dev ops est requise pour bien s’en sortir, et encore si ce n’est pas le cas, il vous apprendra.
Je dirais que cet outil m’a pris entre 3h et 4h de mon temps entre l’idée et le déploiement, sur une techno que je ne maîtrise absolument pas (je prends presque autant de temps à t’écrire ceci qu’a avoir réalisé l’outil).
De loin le meilleurs outil que j’ai pu tester pour coder avec des LLMs (pour 20$/mois, accès aux LLMs de sont choix parmi les plus performant du marché, une bouchée de pain. On peut aussi ne pas payer de forfait un insérer des clés API de providers LLM, ou encore paramétrer des modèles locaux, au choix).
Mais le marché évolue vite, et je suis sûr que JetBrains ne tardera pas à faire des intégrations aussi poussés.
Voilà pour mon retour d’expérience.
PS: Pour qu’il itère tous seul sur les hints de la console comme un grand, il faut cocher un case beta en paramètre.
C’est hyper impressionnant. Tu viens de me mettre un gros jouet entre les mains, j’ai déjà testé un peu plus et il réalise déjà des miracles.
Si déjà il pouvait me délester de toutes les tâches pénibles de dev, ça me libérera pour faire les choses les plus poussées.
Si tu veux tu peux remplacer la mention Pini par cgeek, moi je veux bien de ta pub
A la décharge de Pini, sa réaction fait suite à des échanges qu’on a eu en MP axiom, où il est vrai que je peux être un peu taquin, voir très con…