Ajoute l'assistant IA "Ruby" (Claude + Scenario) et corrige plusieurs bugs de la scène
Intègre un chat IA capable de manipuler la scène via les mêmes fonctions que l'éditeur manuel (objets, variables, déclencheurs, images générées), avec conversations multiples par écran façon Claude. Corrige au passage le rafraîchissement pjax hors-ordre, l'onglet IA/déclencheurs vide après sélection d'un objet, la comparaison de booléens dans les conditions, et le blocage du glisser-déposer hors du cadre caméra après un redimensionnement de fond par l'IA. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
ab6635eee0
commit
b07b231a61
@@ -87,6 +87,7 @@ from .scenes.set_scene_object_name import set_scene_object_name
|
||||
from .scenes.set_scene_object_dialogue_box_style import set_scene_object_dialogue_box_style
|
||||
from .scenes.render_scene_object import render_scene_object
|
||||
from .scenes.build_collision_rule_objects import build_collision_rule_objects
|
||||
from .scenes.append_action_to_trigger import append_action_to_trigger
|
||||
from .rendering.trigger_graph import (
|
||||
list_all_triggers, list_all_collision_objects, collect_all_dialogue_lines,
|
||||
list_completable_dialogue_ids,
|
||||
@@ -128,3 +129,11 @@ from .custom_events.list_custom_event_usages import list_custom_event_usages
|
||||
from .custom_events.delete_custom_event import delete_custom_event
|
||||
|
||||
from .payload.full_game_payload import full_game_payload
|
||||
|
||||
from .ia.ensure_ia_chat_schema import ensure_ia_chat_schema
|
||||
from .ia.list_ia_chat_messages import list_ia_chat_messages
|
||||
from .ia.add_ia_chat_message import add_ia_chat_message
|
||||
from .ia.create_ia_conversation import create_ia_conversation
|
||||
from .ia.list_ia_conversations import list_ia_conversations
|
||||
from .ia.get_ia_conversation import get_ia_conversation
|
||||
from .ia.delete_ia_conversation import delete_ia_conversation
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import db
|
||||
|
||||
from .ensure_ia_chat_schema import ensure_ia_chat_schema
|
||||
|
||||
_DEFAULT_TITLE = "Nouvelle conversation"
|
||||
_TITLE_MAX_LENGTH = 50
|
||||
|
||||
|
||||
def add_ia_chat_message(slug, conversation_id, role, content):
|
||||
"""Ajoute un message (role "user" ou "assistant") à une conversation
|
||||
— voir routes/ia/ia_chat.py, seul appelant.
|
||||
|
||||
Titre automatique (comme claude.ai) : le premier message "user" d'une
|
||||
conversation encore au titre par défaut renomme la conversation avec
|
||||
ses ~50 premiers caractères, pour que la colonne de gauche affiche
|
||||
autre chose que "Nouvelle conversation" pour toutes les entrées."""
|
||||
ensure_ia_chat_schema(slug)
|
||||
conn = db.connect(slug)
|
||||
conn.execute(
|
||||
"INSERT INTO _ia_chat_messages (conversation_id, role, content) VALUES (?, ?, ?)",
|
||||
(conversation_id, role, content),
|
||||
)
|
||||
message_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
|
||||
if role == "user":
|
||||
conversation = conn.execute(
|
||||
"SELECT title FROM _ia_conversations WHERE id = ?", (conversation_id,)
|
||||
).fetchone()
|
||||
if conversation and conversation["title"] == _DEFAULT_TITLE:
|
||||
title = content.strip()[:_TITLE_MAX_LENGTH]
|
||||
conn.execute("UPDATE _ia_conversations SET title = ? WHERE id = ?", (title, conversation_id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return message_id
|
||||
@@ -0,0 +1,16 @@
|
||||
import db
|
||||
|
||||
from .ensure_ia_chat_schema import ensure_ia_chat_schema
|
||||
|
||||
|
||||
def create_ia_conversation(slug, screen_id):
|
||||
""""+ Nouvelle conversation" (voir static/js/ia/ia-chat.js) : créée
|
||||
IMMÉDIATEMENT, vide — visible tout de suite dans la colonne, pas
|
||||
besoin d'attendre un premier message."""
|
||||
ensure_ia_chat_schema(slug)
|
||||
conn = db.connect(slug)
|
||||
conn.execute("INSERT INTO _ia_conversations (screen_id) VALUES (?)", (screen_id,))
|
||||
conversation_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return conversation_id
|
||||
@@ -0,0 +1,13 @@
|
||||
import db
|
||||
|
||||
from .ensure_ia_chat_schema import ensure_ia_chat_schema
|
||||
|
||||
|
||||
def delete_ia_conversation(slug, conversation_id):
|
||||
"""Cascade sur les messages (ON DELETE CASCADE, voir
|
||||
ensure_ia_chat_schema.py)."""
|
||||
ensure_ia_chat_schema(slug)
|
||||
conn = db.connect(slug)
|
||||
conn.execute("DELETE FROM _ia_conversations WHERE id = ?", (conversation_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -0,0 +1,38 @@
|
||||
import db
|
||||
|
||||
|
||||
def ensure_ia_chat_schema(slug):
|
||||
"""Chat "Générer avec l'IA" — plusieurs CONVERSATIONS par écran
|
||||
(demande explicite : gérer plusieurs fils, comme claude.ai), chacune
|
||||
portant sa propre liste de messages. ON DELETE CASCADE à chaque
|
||||
niveau (écran supprimé -> ses conversations -> leurs messages),
|
||||
même patron que _scene_objects (screens/scenes/ensure_scene_schema.py)."""
|
||||
conn = db.connect(slug)
|
||||
# _ia_chat_messages existait déjà (phase précédente, une seule
|
||||
# conversation par écran, colonne screen_id directe) sur les projets
|
||||
# déjà testés avant ce chantier — CREATE TABLE IF NOT EXISTS ne
|
||||
# migrerait pas son schéma. Aucune donnée à préserver (fonctionnalité
|
||||
# tout juste testée manuellement) : on repart d'une table neuve plutôt
|
||||
# que d'écrire une vraie migration pour quelques messages de test.
|
||||
cols = {row["name"] for row in conn.execute("PRAGMA table_info(_ia_chat_messages)").fetchall()}
|
||||
if cols and "conversation_id" not in cols:
|
||||
conn.execute("DROP TABLE _ia_chat_messages")
|
||||
conn.executescript(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS _ia_conversations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
screen_id INTEGER NOT NULL REFERENCES _screens(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL DEFAULT 'Nouvelle conversation',
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS _ia_chat_messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
conversation_id INTEGER NOT NULL REFERENCES _ia_conversations(id) ON DELETE CASCADE,
|
||||
role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
|
||||
content TEXT NOT NULL,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
"""
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -0,0 +1,16 @@
|
||||
import db
|
||||
|
||||
from .ensure_ia_chat_schema import ensure_ia_chat_schema
|
||||
|
||||
|
||||
def get_ia_conversation(slug, conversation_id):
|
||||
"""Renvoie aussi screen_id — indispensable pour vérifier
|
||||
l'appartenance à CE jeu avant de supprimer/lire/écrire (voir
|
||||
routes/ia/ia_chat.py), même patron que auth.get_user_asset."""
|
||||
ensure_ia_chat_schema(slug)
|
||||
conn = db.connect(slug)
|
||||
row = conn.execute(
|
||||
"SELECT id, screen_id, title, created_at FROM _ia_conversations WHERE id = ?", (conversation_id,)
|
||||
).fetchone()
|
||||
conn.close()
|
||||
return dict(row) if row else None
|
||||
@@ -0,0 +1,16 @@
|
||||
import db
|
||||
|
||||
from .ensure_ia_chat_schema import ensure_ia_chat_schema
|
||||
|
||||
|
||||
def list_ia_chat_messages(slug, conversation_id):
|
||||
"""Historique d'UNE conversation, dans l'ordre chronologique (voir
|
||||
static/js/ia/ia-chat.js, qui les affiche tels quels)."""
|
||||
ensure_ia_chat_schema(slug)
|
||||
conn = db.connect(slug)
|
||||
rows = conn.execute(
|
||||
"SELECT id, role, content, created_at FROM _ia_chat_messages WHERE conversation_id = ? ORDER BY id ASC",
|
||||
(conversation_id,),
|
||||
).fetchall()
|
||||
conn.close()
|
||||
return [dict(row) for row in rows]
|
||||
@@ -0,0 +1,16 @@
|
||||
import db
|
||||
|
||||
from .ensure_ia_chat_schema import ensure_ia_chat_schema
|
||||
|
||||
|
||||
def list_ia_conversations(slug, screen_id):
|
||||
"""Conversations d'un écran, plus récente en premier (voir la
|
||||
colonne de gauche de l'onglet "Générer avec l'IA")."""
|
||||
ensure_ia_chat_schema(slug)
|
||||
conn = db.connect(slug)
|
||||
rows = conn.execute(
|
||||
"SELECT id, title, created_at FROM _ia_conversations WHERE screen_id = ? ORDER BY id DESC",
|
||||
(screen_id,),
|
||||
).fetchall()
|
||||
conn.close()
|
||||
return [dict(row) for row in rows]
|
||||
@@ -15,7 +15,7 @@ _QUIZ_BOX_DEFAULT_SIZE = (340, 240)
|
||||
_SCORE_WIDGET_DEFAULT_SIZE = (140, 50)
|
||||
|
||||
|
||||
def add_scene_object(slug, scene_id, kind="personnage", forge_character=None, background_slug=None):
|
||||
def add_scene_object(slug, scene_id, kind="personnage", forge_character=None, background_slug=None, image_url=None):
|
||||
"""Pose un nouvel objet sur une scène (voir ensure_scene_schema.py).
|
||||
kind="personnage" : attributes._personnage_data, MÊME structure/mêmes
|
||||
fonctions resolve_personnage_* que le widget "personnage" de l'éditeur
|
||||
@@ -28,7 +28,14 @@ def add_scene_object(slug, scene_id, kind="personnage", forge_character=None, ba
|
||||
static/style.css .sceneBackground) et à la taille RÉELLE de l'image
|
||||
choisie (voir screens/labels/background_library.py) par défaut — une
|
||||
image plus grande que la scène devient alors le "monde" que la caméra
|
||||
suit en défilant (voir static/js/play/personnage-controller.js)."""
|
||||
suit en défilant (voir static/js/play/personnage-controller.js).
|
||||
|
||||
image_url (kind "fond"/"decor" uniquement) : une image ARBITRAIRE
|
||||
(voir "Mes assets", auth/user_assets/) plutôt qu'une clé de
|
||||
BACKGROUND_LIBRARY — prioritaire sur background_slug si les deux sont
|
||||
fournis. Pas de largeur/hauteur connues d'avance ici (contrairement à
|
||||
BACKGROUND_LIBRARY qui les documente) : retombe sur les colonnes par
|
||||
défaut de _scene_objects, exactement comme un "decor" ordinaire."""
|
||||
ensure_scene_schema(slug)
|
||||
kind = kind if kind in _KINDS else "personnage"
|
||||
attributes = {}
|
||||
@@ -52,6 +59,8 @@ def add_scene_object(slug, scene_id, kind="personnage", forge_character=None, ba
|
||||
# et disparaîtrait à tort de "🧩 Collision" (voir scene_edit_view.py,
|
||||
# qui exclut toujours le joueur).
|
||||
attributes["_personnage_role"] = "pnj"
|
||||
elif kind in ("fond", "decor") and image_url:
|
||||
attributes["src"] = image_url
|
||||
elif kind == "fond" and background_slug:
|
||||
from ..labels.background_library import BACKGROUND_LIBRARY
|
||||
background = BACKGROUND_LIBRARY.get(background_slug)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
from .get_scene_object import get_scene_object
|
||||
from .set_scene_object_collision_rules import set_scene_object_collision_rules
|
||||
from ..rendering.collision_rules import resolve_collision_rules, sanitize_collision_rules, set_then_action_in_rules
|
||||
|
||||
|
||||
def append_action_to_trigger(slug, object_id, after_id, raw_action):
|
||||
""""+ Ajouter une action" en bout de chaîne — extrait de
|
||||
routes/triggers/triggers.py::trigger_append_action pour être
|
||||
l'UNIQUE chemin d'écriture, partagé par la route HTTP ET par l'agent
|
||||
IA (voir ai/tools.py) : ni l'un ni l'autre n'écrit `_collision_rules`
|
||||
directement, tous deux passent par ici. `after_id` : dialogue.id
|
||||
(feuille "dialogue") ou id (feuille "variable") de la DERNIÈRE
|
||||
feuille d'une chaîne. Renvoie False si l'objet, `after_id`, ou
|
||||
`raw_action` sont invalides, ou si la feuille visée a déjà un "then"
|
||||
(jamais écrasé par cette fonction — set_then_action_in_rules ne fait
|
||||
qu'ajouter en bout de chaîne réelle)."""
|
||||
obj = get_scene_object(slug, object_id)
|
||||
if obj is None:
|
||||
return False
|
||||
rules = resolve_collision_rules(obj)
|
||||
if not set_then_action_in_rules(rules, after_id, raw_action):
|
||||
return False
|
||||
sanitized = sanitize_collision_rules(rules)
|
||||
set_scene_object_collision_rules(slug, object_id, sanitized)
|
||||
return True
|
||||
Reference in New Issue
Block a user