feat: /iap, reactions on answers, feedback and /ia corrigir

This commit is contained in:
marcos
2026-08-06 03:43:19 +00:00
parent a3f40d1408
commit 5f616d3d99
5 changed files with 139 additions and 13 deletions
@@ -62,6 +62,13 @@ final class Ai {
record Answered(UUID asker, String question, String answer) { record Answered(UUID asker, String question, String answer) {
} }
/**
* How many answers players have flagged wrong with {@code /ia feedback ruim}.
* Shown in {@code /canalhandia status}. Simple in-memory counter — a restart
* resets it, like {@link #askedToday}.
*/
private int feedbackWrong;
/** Package-private so Task 11's {@code /ia corrigir} can read what to correct. */ /** Package-private so Task 11's {@code /ia corrigir} can read what to correct. */
Answered lastAnswer() { Answered lastAnswer() {
return lastAnswer; return lastAnswer;
@@ -77,6 +84,21 @@ final class Ai {
return corrections; return corrections;
} }
/** A player marked the last answer wrong with {@code /ia feedback ruim}. */
boolean flagLastAnswerWrong() {
if (lastAnswer == null) {
return false;
}
feedbackWrong++;
plugin.getLogger().info("IA: resposta marcada como errada — pergunta: " + lastAnswer.question());
return true;
}
/** How many answers players have flagged wrong. Shown in /canalhandia status. */
int feedbackWrong() {
return feedbackWrong;
}
Ai(Canalhandia plugin) { Ai(Canalhandia plugin) {
this.plugin = plugin; this.plugin = plugin;
Settings settings = plugin.settings(); Settings settings = plugin.settings();
@@ -71,7 +71,8 @@ public final class Canalhandia extends JavaPlugin implements Listener {
CanalhandiaCommand root = new CanalhandiaCommand(this); CanalhandiaCommand root = new CanalhandiaCommand(this);
for (String name : List.of("canalhandia", "curiosidade", "adivinha", "enquete", "ranking", for (String name : List.of("canalhandia", "curiosidade", "adivinha", "enquete", "ranking",
"reagir", "reacoes", "palpite", "votar", "legal", "wow", "top", "f", "ia")) { "reagir", "reacoes", "palpite", "votar", "legal", "wow", "top", "f", "ia", "iap",
"errado")) {
register(name, root); register(name, root);
} }
@@ -305,16 +306,35 @@ public final class Canalhandia extends JavaPlugin implements Listener {
} }
/** /**
* Opens the reaction row on a public AI answer. Minimal for now: reuses * Opens the reaction row on a public AI answer: 👍 (reuses the shared
* the shared {@link #openReactions()} machinery. Task 11 wires the * {@code joia}/{@code /legal} reaction so the typed Bedrock twin resolves
* asker-gated feedback path and the typed {@code /reagir} twin. * via {@link Settings#reactionForCommand}) and ❌ ({@code /errado}, new).
*
* <p>The {@code askerId} param is kept in the signature because
* {@link Ai#deliver} passes it, but is not used structurally here.
*/ */
void openAiReactions(UUID askerId) { void openAiReactions(UUID askerId) {
// TODO Task 11: asker-gated feedback and the typed /reagir twin use askerId.
if (!settings.reactionsEnabled()) { if (!settings.reactionsEnabled()) {
return; return;
} }
openReactions(); Reactions reactions = new Reactions(nextId++, List.of(
new ReactionDef("joia", "[👍]", "[+1]", "legal"),
new ReactionDef("errado", "[❌]", "[ERRADO]", "errado")));
liveReactions = reactions;
remember(reactions);
reactions.show();
getServer().getScheduler().runTaskLater(this, () -> {
reactions.hide();
if (liveReactions == reactions) {
liveReactions = null;
}
if (reactions.hasAnyVote()) {
broadcastPerPlatform(bedrock -> Component.text(" ")
.append(reactions.summary(bedrock, settings.summaryNames())));
}
}, settings.reactionWindowSeconds() * 20L);
broadcastPerPlatform(bedrock -> Component.text(" ")
.append(reactions.buttons(bedrock)));
} }
/** /**
@@ -14,6 +14,7 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
/** /**
@@ -46,7 +47,8 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
case "palpite" -> guessLatest(sender, args); case "palpite" -> guessLatest(sender, args);
case "votar" -> voteLatest(sender, args); case "votar" -> voteLatest(sender, args);
case "reacoes" -> whoReacted(sender); case "reacoes" -> whoReacted(sender);
case "ia" -> ia(sender, args); case "ia" -> ia(sender, args, false);
case "iap" -> ia(sender, args, true);
default -> { default -> {
String reaction = plugin.settings().reactionForCommand(command.getName()); String reaction = plugin.settings().reactionForCommand(command.getName());
if (reaction != null) { if (reaction != null) {
@@ -656,10 +658,12 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
} }
Msg.line(sender, "categorias ativas", enabled.isEmpty() ? "nenhuma" : String.join(", ", enabled)); Msg.line(sender, "categorias ativas", enabled.isEmpty() ? "nenhuma" : String.join(", ", enabled));
Msg.line(sender, "ia", plugin.ai().configured() Msg.line(sender, "ia", plugin.ai().configured()
? settings.aiModel() + " · " + plugin.ai().askedToday() + "/" ? settings.aiModel() + " · perfil " + settings.aiProfile().name().toLowerCase(Locale.ROOT)
+ settings.aiDailyLimit() + " hoje · cooldown " + " · " + plugin.ai().askedToday() + "/" + settings.aiDailyLimit() + " hoje"
+ settings.aiCooldownSeconds() + "s · " + " · cooldown " + settings.aiCooldownSeconds() + "s · "
+ (settings.aiPublic() ? "resposta pública" : "resposta privada") + (settings.aiPublic() ? "resposta pública" : "resposta privada")
+ " · " + plugin.ai().corrections().all().size() + " correções"
+ " · " + plugin.ai().feedbackWrong() + " feedback ruim"
: "sem chave configurada"); : "sem chave configurada");
} }
@@ -754,7 +758,7 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
* has it out of the box and LuckPerms can hand it to anyone else with * has it out of the box and LuckPerms can hand it to anyone else with
* {@code lp user <nome> permission set canalhandia.ia true}. * {@code lp user <nome> permission set canalhandia.ia true}.
*/ */
private boolean ia(CommandSender sender, String[] args) { private boolean ia(CommandSender sender, String[] args, boolean isPrivate) {
if (!sender.hasPermission("canalhandia.ia")) { if (!sender.hasPermission("canalhandia.ia")) {
return denied(sender); return denied(sender);
} }
@@ -766,14 +770,74 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
Msg.error(sender, "Só jogadores podem usar /ia."); Msg.error(sender, "Só jogadores podem usar /ia.");
return true; return true;
} }
// Subcommands come before the question. Tradeoff: a question that
// literally starts with "perfil", "corrigir" or "feedback" is hijacked
// — e.g. "/ia perfil do servidor" runs /ia perfil with no profile name
// and shows usage instead of asking. Acceptable on a small server,
// where the operator commands matter more than the edge case.
if (args.length > 0) {
switch (args[0].toLowerCase(Locale.ROOT)) {
case "perfil" -> { iaProfile(sender, args); return true; }
case "corrigir" -> { iaCorrect(sender, args); return true; }
case "feedback" -> { iaFeedback(sender, args); return true; }
default -> { /* fall through: treat all args as the question */ }
}
}
if (args.length == 0) { if (args.length == 0) {
Msg.error(sender, "Uso: /ia <pergunta>"); Msg.error(sender, isPrivate ? "Uso: /iap <pergunta>" : "Uso: /ia <pergunta>");
return true; return true;
} }
plugin.ai().ask(player, String.join(" ", args)); plugin.ai().ask(player, String.join(" ", args), isPrivate);
return true; return true;
} }
private void iaProfile(CommandSender sender, String[] args) {
if (!sender.hasPermission("canalhandia.ia.perfil")) {
denied(sender);
return;
}
if (args.length < 2) {
Msg.error(sender, "Uso: /ia perfil <economico|preciso> (atual: "
+ plugin.settings().aiProfile().name().toLowerCase(Locale.ROOT) + ")");
return;
}
AiProfile profile = AiProfile.byKey(args[1]);
plugin.settings().aiProfile(profile);
Msg.ok(sender, "Perfil da IA: " + profile.name().toLowerCase(Locale.ROOT)
+ (profile == AiProfile.PRECISO ? " (consulta a wiki)" : " (sem wiki, mais rápido)"));
}
private void iaCorrect(CommandSender sender, String[] args) {
if (!sender.hasPermission("canalhandia.ia.corrigir")) {
denied(sender);
return;
}
Ai.Answered last = plugin.ai().lastAnswer();
if (last == null) {
Msg.error(sender, "Nenhuma resposta recente para corrigir.");
return;
}
if (args.length < 2) {
Msg.error(sender, "Uso: /ia corrigir <resposta correta>");
return;
}
String correct = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
plugin.ai().corrections().add(last.question(), correct);
Msg.ok(sender, "Correção registrada para perguntas parecidas com: " + last.question());
}
private void iaFeedback(CommandSender sender, String[] args) {
if (args.length < 2 || !args[1].equalsIgnoreCase("ruim")) {
Msg.error(sender, "Uso: /ia feedback ruim (marca a última resposta como errada)");
return;
}
if (plugin.ai().flagLastAnswerWrong()) {
Msg.ok(sender, "Obrigado pelo feedback. Um operador vai revisar.");
} else {
Msg.error(sender, "Nenhuma resposta recente para marcar.");
}
}
private boolean admin(CommandSender sender) { private boolean admin(CommandSender sender) {
if (sender.hasPermission(ADMIN)) { if (sender.hasPermission(ADMIN)) {
return true; return true;
+4
View File
@@ -93,6 +93,10 @@ reacoes:
java: "[🔥]" java: "[🔥]"
texto: "[TOP]" texto: "[TOP]"
comando: "top" comando: "top"
errado:
java: "[❌]"
texto: "[ERRADO]"
comando: "errado"
# --- Jogos ------------------------------------------------------------------- # --- Jogos -------------------------------------------------------------------
+16
View File
@@ -60,6 +60,13 @@ commands:
description: Faz uma pergunta simples para a IA. description: Faz uma pergunta simples para a IA.
usage: /ia <pergunta> usage: /ia <pergunta>
aliases: [pergunta] aliases: [pergunta]
iap:
description: Pergunta para a IA, resposta só para você.
usage: /iap <pergunta>
aliases: [iaprivado]
errado:
description: Reage com "errado" à última mensagem (resposta da IA).
usage: /errado
permissions: permissions:
# Declared explicitly: an undeclared Bukkit permission falls back to op-only, # Declared explicitly: an undeclared Bukkit permission falls back to op-only,
@@ -82,6 +89,15 @@ permissions:
canalhandia.ia: canalhandia.ia:
description: Permite usar /ia. Padrão op; o LuckPerms pode conceder a outros. description: Permite usar /ia. Padrão op; o LuckPerms pode conceder a outros.
default: op default: op
canalhandia.ia.privado:
description: Permite perguntar em privado com /iap.
default: true
canalhandia.ia.corrigir:
description: Permite registrar correções para as respostas da IA.
default: op
canalhandia.ia.perfil:
description: Permite trocar o perfil da IA entre economico e preciso.
default: op
canalhandia.isento: canalhandia.isento:
description: Quem tem isto nunca é sorteado como assunto. description: Quem tem isto nunca é sorteado como assunto.
default: false default: false