Files
qelectrotech-source-mirror/sources/ui/aiassistantdialog.cpp
T
ispyisail c76ef10e0b Add Help > Connect an AI assistant
A dialog showing the configuration an AI assistant needs to start
QElectroTech's MCP server (misc/qet-mcp), with this installation's paths
filled in: pick the assistant (Claude Desktop, Claude Code, GitHub
Copilot in VS Code, Cursor, Gemini CLI, Codex CLI, LM Studio), the
drawings folder the assistant may use, and whether it may edit (off by
default). It says where the text goes, warns that text inside a project
from someone else can steer an assistant, and copies the text. It writes
nothing and starts nothing.

AiAssistantSetup finds the server beside the running program, in the two
places the packages put it (<root>/mcp beside <root>/bin on Windows,
<prefix>/share/qelectrotech/mcp beside <prefix>/bin otherwise), and the
Python the Windows installer can add. When there is none, the dialog says
the server is not installed with this version and links the guide.
The configuration is written with QJsonDocument, so Windows paths are
escaped correctly; Codex CLI gets TOML.

tst_aiassistantsetup (9 cases): both layouts, no server, the bundled
Python on Windows only, valid JSON for every JSON client with paths that
parse back unchanged, the "servers" key for VS Code, "type" only where
wanted, editing off unless allowed, escaped TOML. Each of 8 rules was
removed in turn and the test failed. ctest 32/32.

Checked in the GUI (Xvfb): Help menu opens it; from a build tree it
reports the server missing and disables Copy; from an install layout it
fills the paths, and the text Copy put on the clipboard, used unchanged
as a Claude Code configuration, ran a real qet_project_info call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:29:50 +13:00

199 lines
7.1 KiB
C++

/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "aiassistantdialog.h"
#include <QApplication>
#include <QCheckBox>
#include <QClipboard>
#include <QComboBox>
#include <QDialogButtonBox>
#include <QDir>
#include <QFileDialog>
#include <QFontDatabase>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QVBoxLayout>
using AiAssistantSetup::Client;
namespace {
const char *const GUIDE_URL =
"https://github.com/qelectrotech/qelectrotech-source-mirror/wiki/ai_assistants";
}
AiAssistantDialog::AiAssistantDialog(QWidget *parent) :
QDialog(parent)
{
setWindowTitle(tr("Connecter un assistant IA"));
resize(720, 560);
#ifdef Q_OS_WIN
const bool windows = true;
#else
const bool windows = false;
#endif
m_paths = AiAssistantSetup::detect(QCoreApplication::applicationDirPath(),
QCoreApplication::applicationFilePath(),
windows);
auto *layout = new QVBoxLayout(this);
auto *intro = new QLabel(
tr("Un assistant IA (Claude, GitHub Copilot, Gemini…) peut ouvrir, "
"vérifier et modifier vos schémas grâce au serveur MCP de "
"QElectroTech, qui fonctionne sur cet ordinateur. Copiez le texte "
"ci-dessous dans la configuration de votre assistant. "
"<a href=\"%1\">Guide détaillé</a>").arg(QLatin1String(GUIDE_URL)),
this);
intro->setWordWrap(true);
intro->setOpenExternalLinks(true);
layout->addWidget(intro);
if (m_paths.server.isEmpty()) {
auto *missing = new QLabel(
tr("<b>Le serveur MCP n'est pas installé avec cette version de "
"QElectroTech.</b> Le guide explique comment l'obtenir."),
this);
missing->setWordWrap(true);
layout->addWidget(missing);
}
auto *form = new QFormLayout();
m_client = new QComboBox(this);
m_client->addItem(QStringLiteral("Claude Desktop"), int(Client::ClaudeDesktop));
m_client->addItem(QStringLiteral("Claude Code"), int(Client::ClaudeCode));
m_client->addItem(QStringLiteral("GitHub Copilot (VS Code)"), int(Client::VsCode));
m_client->addItem(QStringLiteral("Cursor"), int(Client::Cursor));
m_client->addItem(QStringLiteral("Gemini CLI"), int(Client::GeminiCli));
m_client->addItem(QStringLiteral("Codex CLI"), int(Client::CodexCli));
m_client->addItem(QStringLiteral("LM Studio"), int(Client::LmStudio));
form->addRow(tr("Assistant :"), m_client);
auto *workspace_row = new QHBoxLayout();
m_workspace = new QLineEdit(this);
m_workspace->setPlaceholderText(tr("Le dossier de vos schémas"));
auto *browse = new QPushButton(tr("Parcourir…"), this);
workspace_row->addWidget(m_workspace);
workspace_row->addWidget(browse);
form->addRow(tr("Dossier accessible :"), workspace_row);
m_allow_edit = new QCheckBox(tr("Autoriser l'assistant à modifier les schémas"), this);
form->addRow(QString(), m_allow_edit);
layout->addLayout(form);
auto *scope = new QLabel(
tr("L'assistant ne peut lire et écrire que dans ce dossier. Sans "
"modification autorisée, il peut seulement lire, comparer et "
"exporter. Un assistant lit le texte des projets (repères, "
"notes…) : un texte écrit comme une instruction peut "
"l'influencer. Laissez les modifications désactivées pour les "
"schémas reçus d'autres personnes."),
this);
scope->setWordWrap(true);
layout->addWidget(scope);
m_where = new QLabel(this);
m_where->setWordWrap(true);
m_where->setTextInteractionFlags(Qt::TextSelectableByMouse);
layout->addWidget(m_where);
m_text = new QPlainTextEdit(this);
m_text->setReadOnly(true);
m_text->setLineWrapMode(QPlainTextEdit::NoWrap);
m_text->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
layout->addWidget(m_text);
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Close, this);
m_copy = buttons->addButton(tr("Copier"), QDialogButtonBox::ActionRole);
layout->addWidget(buttons);
connect(m_client, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &AiAssistantDialog::refresh);
connect(m_workspace, &QLineEdit::textChanged, this, &AiAssistantDialog::refresh);
connect(m_allow_edit, &QCheckBox::toggled, this, &AiAssistantDialog::refresh);
connect(browse, &QPushButton::clicked, this, &AiAssistantDialog::chooseWorkspace);
connect(m_copy, &QPushButton::clicked, this, &AiAssistantDialog::copy);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
refresh();
}
void AiAssistantDialog::refresh()
{
const auto client = Client(m_client->currentData().toInt());
m_where->setText(whereItGoes(client) + QLatin1Char(' ')
+ tr("Si le fichier contient déjà d'autres serveurs, "
"ajoutez seulement l'entrée « qet »."));
const QString workspace = m_workspace->text().trimmed();
const bool ready = !m_paths.server.isEmpty() && !workspace.isEmpty();
m_copy->setEnabled(ready);
if (m_paths.server.isEmpty())
m_text->setPlainText(QString());
else if (workspace.isEmpty())
m_text->setPlainText(tr("Choisissez d'abord le dossier de vos schémas."));
else
m_text->setPlainText(AiAssistantSetup::configuration(
client, m_paths, workspace, m_allow_edit->isChecked()));
}
void AiAssistantDialog::chooseWorkspace()
{
const QString dir = QFileDialog::getExistingDirectory(
this, tr("Dossier accessible à l'assistant"), m_workspace->text());
if (!dir.isEmpty())
m_workspace->setText(QDir::toNativeSeparators(dir));
}
void AiAssistantDialog::copy()
{
QApplication::clipboard()->setText(m_text->toPlainText());
m_copy->setText(tr("Copié"));
}
QString AiAssistantDialog::whereItGoes(Client client) const
{
switch (client) {
case Client::ClaudeDesktop:
return tr("Dans Claude Desktop : Paramètres → Développeur → "
"Modifier la configuration. Puis quittez et relancez Claude.");
case Client::ClaudeCode:
return tr("Enregistrez-le sous le nom .mcp.json dans le dossier de vos schémas.");
case Client::VsCode:
return tr("Enregistrez-le sous le nom .vscode/mcp.json dans le dossier "
"ouvert dans VS Code. Copilot utilise les outils en mode agent.");
case Client::Cursor:
return tr("Ajoutez-le au fichier .cursor/mcp.json de votre dossier "
"personnel.");
case Client::GeminiCli:
return tr("Ajoutez-le au fichier .gemini/settings.json de votre dossier "
"personnel. Gemini CLI demande de faire confiance au dossier "
"la première fois.");
case Client::CodexCli:
return tr("Ajoutez-le au fichier .codex/config.toml de votre dossier "
"personnel.");
case Client::LmStudio:
return tr("Dans LM Studio : onglet Program → Install → Edit mcp.json.");
}
return QString();
}