Initial release — APIClient - Agent v2.0.0

AI-first API testing desktop client built with Python + PyQt6.

Features:
- Multi-tab HTTP request editor with params/headers/body/auth/tests
- KeyValueTable with per-row enable/disable checkboxes and 36px rows
- Format JSON button, syntax highlighting, pre-request & test scripts
- Collections, environments, history, import/export (Postman v2.1, cURL)
- OpenAPI 3.x / Swagger 2.0 local parser (no AI tokens)
- EKIKA Odoo API Framework generator — JSON-API, REST JSON, GraphQL,
  Custom REST JSON with all auth types (instant, no AI tokens)
- Persistent AI chat sidebar (Claude-powered co-pilot) with streaming,
  context-aware suggestions, and one-click Apply to request editor
- AI collection generator from any docs URL or pasted spec
- WebSocket client, Mock server, Collection runner, Code generator
- Dark/light theme engine (global QSS, object-name selectors)
- SSL error detection with actionable hints
- MIT License

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-28 17:34:18 +05:30
parent 1dbbb4320b
commit 01662f7e0e
37 changed files with 7822 additions and 1 deletions

699
app/ui/ai_panel.py Normal file
View File

@@ -0,0 +1,699 @@
"""APIClient - Agent — AI Assistant Dialog."""
import json
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QTextEdit, QWidget, QTabWidget, QMessageBox,
QProgressBar, QCheckBox, QFormLayout, QComboBox, QScrollArea,
QGroupBox, QGridLayout, QSizePolicy
)
from PyQt6.QtCore import QThread, pyqtSignal, Qt
from PyQt6.QtGui import QFont
from app.core import storage, ai_client, openapi_parser
from app.core.ekika_odoo_generator import (
generate_collection, API_KINDS, AUTH_TYPES, OPERATIONS
)
from app.models import HttpRequest, Environment
# ── Generic analysis worker ───────────────────────────────────────────────────
class AnalysisWorker(QThread):
progress = pyqtSignal(str)
finished = pyqtSignal(dict)
error = pyqtSignal(str)
def __init__(self, url: str = "", raw_text: str = "",
base_url: str = "", models: list = None):
super().__init__()
self.url = url
self.raw_text = raw_text
self.base_url = base_url
self.models = models or []
def run(self):
try:
content = self.raw_text
if self.url and not content:
self.progress.emit("Fetching documentation…")
content = ai_client.fetch_url_content(self.url)
self.progress.emit("Checking for OpenAPI/Swagger spec…")
spec = openapi_parser.detect_spec(content)
if spec:
self.progress.emit("OpenAPI spec detected — parsing directly…")
result = openapi_parser.parse_spec(spec)
if self.base_url:
result["base_url"] = self.base_url
result.setdefault("environment_variables", {})["base_url"] = self.base_url
result["_source"] = "openapi"
self.finished.emit(result)
return
prompt = self._build_prompt(content)
result = ai_client.analyze_docs(prompt, progress_cb=self.progress.emit)
result["_source"] = "ai"
if self.base_url and not result.get("base_url"):
result["base_url"] = self.base_url
if self.base_url:
result.setdefault("environment_variables", {})["base_url"] = self.base_url
self.finished.emit(result)
except ai_client.AIError as e:
self.error.emit(str(e))
except Exception as e:
self.error.emit(f"Unexpected error: {e}")
def _build_prompt(self, content: str) -> str:
parts = ["Analyze the following API documentation and generate the JSON collection.\n"]
if self.base_url:
parts.append(f"The user's API instance base URL is: {self.base_url}")
if self.models:
parts.append(
f"The user wants endpoints for these specific models/resources: "
f"{', '.join(self.models)}\n"
f"Generate a full CRUD set for each model."
)
parts.append("\n--- DOCUMENTATION ---\n")
parts.append(content)
return "\n".join(parts)
# ── Main Dialog ───────────────────────────────────────────────────────────────
class AIAssistantDialog(QDialog):
collection_imported = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("AI Assistant")
self.setMinimumSize(900, 680)
self._worker: AnalysisWorker | None = None
self._result: dict | None = None
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# ── Header ────────────────────────────────────────────────────────────
header = QWidget()
header.setObjectName("panelHeader")
header.setFixedHeight(52)
hl = QHBoxLayout(header)
hl.setContentsMargins(16, 0, 16, 0)
title = QLabel("AI Assistant")
title.setObjectName("panelTitle")
hl.addWidget(title)
hl.addStretch()
sub = QLabel("EKIKA Odoo API Framework · OpenAPI · Any REST API")
sub.setObjectName("hintText")
hl.addWidget(sub)
layout.addWidget(header)
# ── Tabs ──────────────────────────────────────────────────────────────
self.tabs = QTabWidget()
self.tabs.addTab(self._build_ekika_tab(), " EKIKA Odoo API ")
self.tabs.addTab(self._build_generic_tab(), " Import from Docs ")
self.tabs.addTab(self._build_settings_tab(), " Settings ")
layout.addWidget(self.tabs, 1)
# ── Footer ────────────────────────────────────────────────────────────
footer = QWidget()
footer.setObjectName("panelFooter")
footer.setFixedHeight(52)
fl = QHBoxLayout(footer)
fl.setContentsMargins(16, 0, 16, 0)
self.status_label = QLabel("Ready")
self.status_label.setObjectName("aiStatusLabel")
fl.addWidget(self.status_label)
fl.addStretch()
close_btn = QPushButton("Close")
close_btn.setFixedWidth(80)
close_btn.clicked.connect(self.accept)
fl.addWidget(close_btn)
layout.addWidget(footer)
# ══════════════════════════════════════════════════════════════════════════
# Tab 1 — EKIKA Odoo API Framework (dedicated, no AI tokens needed)
# ══════════════════════════════════════════════════════════════════════════
def _build_ekika_tab(self) -> QWidget:
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(scroll.Shape.NoFrame)
w = QWidget()
w.setObjectName("panelBody")
layout = QVBoxLayout(w)
layout.setContentsMargins(20, 16, 20, 16)
layout.setSpacing(14)
# ── Connection ────────────────────────────────────────────────────────
conn_group = QGroupBox("Connection")
cg = QFormLayout(conn_group)
cg.setSpacing(8)
self.ek_instance_url = QLineEdit()
self.ek_instance_url.setObjectName("urlBar")
self.ek_instance_url.setPlaceholderText("https://mycompany.odoo.com")
self.ek_instance_url.setText("https://api_framework-18.demo.odoo-apps.ekika.co")
self.ek_endpoint = QLineEdit()
self.ek_endpoint.setPlaceholderText("/user-jsonapi-apikey")
self.ek_endpoint.setText("/user-jsonapi-apikey")
self.ek_api_kind = QComboBox()
self.ek_api_kind.addItems(API_KINDS)
cg.addRow("Instance URL:", self.ek_instance_url)
cg.addRow("API Endpoint:", self.ek_endpoint)
cg.addRow("API Kind:", self.ek_api_kind)
layout.addWidget(conn_group)
# ── Authentication ────────────────────────────────────────────────────
auth_group = QGroupBox("Authentication")
ag = QVBoxLayout(auth_group)
auth_top = QHBoxLayout()
auth_top.addWidget(QLabel("Auth Type:"))
self.ek_auth_type = QComboBox()
self.ek_auth_type.addItems(AUTH_TYPES)
self.ek_auth_type.currentTextChanged.connect(self._on_ek_auth_changed)
auth_top.addWidget(self.ek_auth_type)
auth_top.addStretch()
ag.addLayout(auth_top)
# Auth fields stack (we show/hide rows as needed)
self.ek_auth_form = QFormLayout()
self.ek_auth_form.setSpacing(8)
self.ek_api_key_input = QLineEdit()
self.ek_api_key_input.setPlaceholderText("Your API key value")
self.ek_api_key_input.setEchoMode(QLineEdit.EchoMode.Password)
show_ak = QCheckBox("Show")
show_ak.toggled.connect(lambda on: self.ek_api_key_input.setEchoMode(
QLineEdit.EchoMode.Normal if on else QLineEdit.EchoMode.Password))
ak_row = QHBoxLayout()
ak_row.addWidget(self.ek_api_key_input)
ak_row.addWidget(show_ak)
self._ek_ak_label = QLabel("API Key:")
self.ek_auth_form.addRow(self._ek_ak_label, ak_row)
self.ek_username = QLineEdit()
self.ek_username.setPlaceholderText("admin")
self._ek_user_label = QLabel("Username:")
self.ek_auth_form.addRow(self._ek_user_label, self.ek_username)
self.ek_password = QLineEdit()
self.ek_password.setEchoMode(QLineEdit.EchoMode.Password)
self.ek_password.setPlaceholderText("password")
show_pw = QCheckBox("Show")
show_pw.toggled.connect(lambda on: self.ek_password.setEchoMode(
QLineEdit.EchoMode.Normal if on else QLineEdit.EchoMode.Password))
pw_row = QHBoxLayout()
pw_row.addWidget(self.ek_password)
pw_row.addWidget(show_pw)
self._ek_pw_label = QLabel("Password:")
self.ek_auth_form.addRow(self._ek_pw_label, pw_row)
ag.addLayout(self.ek_auth_form)
layout.addWidget(auth_group)
self._on_ek_auth_changed("API Key") # set initial visibility
# ── Models ────────────────────────────────────────────────────────────
models_group = QGroupBox("Models")
mg = QVBoxLayout(models_group)
models_hint = QLabel(
"Enter Odoo model technical names (comma-separated).\n"
"Examples: sale.order, res.partner, account.move, product.template"
)
models_hint.setObjectName("hintText")
models_hint.setWordWrap(True)
mg.addWidget(models_hint)
self.ek_models = QLineEdit()
self.ek_models.setPlaceholderText("sale.order, res.partner, product.template")
self.ek_models.setText("sale.order")
mg.addWidget(self.ek_models)
layout.addWidget(models_group)
# ── Operations ────────────────────────────────────────────────────────
ops_group = QGroupBox("Operations to Generate")
og = QGridLayout(ops_group)
og.setSpacing(6)
self.ek_op_checks: dict[str, QCheckBox] = {}
default_ops = {"List Records", "Get Single Record", "Create Record",
"Update Record", "Delete Record"}
cols = 3
for i, op in enumerate(OPERATIONS):
cb = QCheckBox(op)
cb.setChecked(op in default_ops)
self.ek_op_checks[op] = cb
og.addWidget(cb, i // cols, i % cols)
layout.addWidget(ops_group)
# ── Collection name ───────────────────────────────────────────────────
name_row = QHBoxLayout()
name_label = QLabel("Collection Name:")
name_label.setObjectName("fieldLabel")
self.ek_col_name = QLineEdit()
self.ek_col_name.setPlaceholderText("Leave blank for auto-name")
name_row.addWidget(name_label)
name_row.addWidget(self.ek_col_name, 1)
layout.addLayout(name_row)
# ── Generate button ───────────────────────────────────────────────────
gen_row = QHBoxLayout()
self.ek_generate_btn = QPushButton("Generate Collection")
self.ek_generate_btn.setObjectName("accent")
self.ek_generate_btn.setFixedHeight(36)
self.ek_generate_btn.clicked.connect(self._ekika_generate)
gen_row.addWidget(self.ek_generate_btn)
gen_row.addStretch()
self.ek_import_btn = QPushButton("Import Collection")
self.ek_import_btn.setFixedHeight(36)
self.ek_import_btn.setEnabled(False)
self.ek_import_btn.clicked.connect(self._ekika_import)
gen_row.addWidget(self.ek_import_btn)
self.ek_env_btn = QPushButton("Create Environment")
self.ek_env_btn.setFixedHeight(36)
self.ek_env_btn.setEnabled(False)
self.ek_env_btn.clicked.connect(self._ekika_create_env)
gen_row.addWidget(self.ek_env_btn)
self.ek_both_btn = QPushButton("Import Both")
self.ek_both_btn.setObjectName("accent")
self.ek_both_btn.setFixedHeight(36)
self.ek_both_btn.setEnabled(False)
self.ek_both_btn.clicked.connect(self._ekika_import_both)
gen_row.addWidget(self.ek_both_btn)
layout.addLayout(gen_row)
# ── Preview ───────────────────────────────────────────────────────────
preview_label = QLabel("Preview:")
preview_label.setObjectName("fieldLabel")
layout.addWidget(preview_label)
self.ek_preview = QTextEdit()
self.ek_preview.setObjectName("aiOutput")
self.ek_preview.setReadOnly(True)
self.ek_preview.setFont(QFont("JetBrains Mono, Fira Code, Consolas", 10))
self.ek_preview.setPlaceholderText(
"Fill in the form above and click Generate Collection to preview.\n\n"
"No API key required — collection is generated instantly from the\n"
"EKIKA Odoo API Framework documentation."
)
self.ek_preview.setMaximumHeight(180)
layout.addWidget(self.ek_preview)
scroll.setWidget(w)
return scroll
def _on_ek_auth_changed(self, auth_type: str):
show_key = auth_type == "API Key"
show_user = auth_type in ("Basic Auth", "User Credentials")
show_pw = auth_type in ("Basic Auth", "User Credentials")
self._ek_ak_label.setVisible(show_key)
self.ek_api_key_input.setVisible(show_key)
# find the show checkbox (parent widget)
self._ek_user_label.setVisible(show_user)
self.ek_username.setVisible(show_user)
self._ek_pw_label.setVisible(show_pw)
self.ek_password.setVisible(show_pw)
def _ekika_generate(self):
instance_url = self.ek_instance_url.text().strip()
endpoint = self.ek_endpoint.text().strip()
api_kind = self.ek_api_kind.currentText()
auth_type = self.ek_auth_type.currentText()
models_raw = self.ek_models.text().strip()
models = [m.strip() for m in models_raw.split(",") if m.strip()]
operations = [op for op, cb in self.ek_op_checks.items() if cb.isChecked()]
col_name = self.ek_col_name.text().strip()
if not instance_url:
QMessageBox.warning(self, "Missing", "Enter the Odoo Instance URL.")
return
if not models:
QMessageBox.warning(self, "Missing", "Enter at least one model name.")
return
if not operations:
QMessageBox.warning(self, "Missing", "Select at least one operation.")
return
auth_creds = {
"api_key": self.ek_api_key_input.text().strip(),
"username": self.ek_username.text().strip(),
"password": self.ek_password.text().strip(),
}
self._result = generate_collection(
instance_url = instance_url,
endpoint = endpoint,
api_kind = api_kind,
auth_type = auth_type,
auth_creds = auth_creds,
models = models,
operations = operations,
collection_name = col_name,
)
eps = self._result["endpoints"]
envs = self._result["environment_variables"]
lines = [
f"✓ Collection: {self._result['collection_name']}",
f"✓ API Kind: {api_kind}",
f"✓ Auth: {auth_type}",
f"✓ Endpoints: {len(eps)} generated",
f"✓ Env vars: {list(envs.keys())}",
"",
"── Endpoints ─────────────────────────────────────",
]
for ep in eps:
lines.append(f" {ep['method']:<8} {ep['path']}")
self.ek_preview.setPlainText("\n".join(lines))
self.ek_import_btn.setEnabled(True)
self.ek_env_btn.setEnabled(True)
self.ek_both_btn.setEnabled(True)
self.status_label.setText(f"{len(eps)} endpoint(s) ready — click Import to save")
def _ekika_import(self):
if not self._result:
return
self._do_import(self._result)
def _ekika_create_env(self):
if not self._result:
return
self._do_create_env(self._result)
def _ekika_import_both(self):
if not self._result:
return
self._do_import(self._result)
self._do_create_env(self._result)
# ══════════════════════════════════════════════════════════════════════════
# Tab 2 — Generic AI analysis (OpenAPI / any docs URL)
# ══════════════════════════════════════════════════════════════════════════
def _build_generic_tab(self) -> QWidget:
w = QWidget()
w.setObjectName("panelBody")
layout = QVBoxLayout(w)
layout.setContentsMargins(16, 12, 16, 12)
layout.setSpacing(10)
url_row = QHBoxLayout()
url_label = QLabel("Docs URL:")
url_label.setObjectName("fieldLabel")
url_label.setFixedWidth(80)
self.url_input = QLineEdit()
self.url_input.setObjectName("urlBar")
self.url_input.setPlaceholderText(
"https://api.example.com/openapi.json or https://docs.example.com"
)
self.url_input.returnPressed.connect(self._analyze)
self.analyze_btn = QPushButton("Analyze")
self.analyze_btn.setObjectName("accent")
self.analyze_btn.setFixedWidth(100)
self.analyze_btn.clicked.connect(self._analyze)
url_row.addWidget(url_label)
url_row.addWidget(self.url_input, 1)
url_row.addWidget(self.analyze_btn)
layout.addLayout(url_row)
ctx_row = QHBoxLayout()
base_label = QLabel("Base URL:")
base_label.setObjectName("fieldLabel")
base_label.setFixedWidth(80)
self.base_url_input = QLineEdit()
self.base_url_input.setPlaceholderText("https://myapi.example.com (optional)")
models_label = QLabel("Models:")
models_label.setObjectName("fieldLabel")
models_label.setFixedWidth(55)
self.models_input = QLineEdit()
self.models_input.setPlaceholderText("res.partner, sale.order (optional)")
ctx_row.addWidget(base_label)
ctx_row.addWidget(self.base_url_input, 2)
ctx_row.addSpacing(8)
ctx_row.addWidget(models_label)
ctx_row.addWidget(self.models_input, 3)
layout.addLayout(ctx_row)
paste_hint = QLabel("Or paste raw documentation / OpenAPI JSON / YAML:")
paste_hint.setObjectName("hintText")
layout.addWidget(paste_hint)
self.paste_editor = QTextEdit()
self.paste_editor.setObjectName("codeEditor")
self.paste_editor.setPlaceholderText("Paste OpenAPI JSON, Swagger YAML, or raw API docs…")
self.paste_editor.setMaximumHeight(110)
self.paste_editor.setFont(QFont("JetBrains Mono, Fira Code, Consolas", 10))
layout.addWidget(self.paste_editor)
self.progress_bar = QProgressBar()
self.progress_bar.setRange(0, 0)
self.progress_bar.setFixedHeight(4)
self.progress_bar.setVisible(False)
layout.addWidget(self.progress_bar)
results_label = QLabel("Analysis Result:")
results_label.setObjectName("fieldLabel")
layout.addWidget(results_label)
self.result_view = QTextEdit()
self.result_view.setObjectName("aiOutput")
self.result_view.setReadOnly(True)
self.result_view.setFont(QFont("JetBrains Mono, Fira Code, Consolas", 10))
self.result_view.setPlaceholderText(
"Results will appear here.\n\n"
"• OpenAPI/Swagger specs are parsed instantly (no API key needed)\n"
"• Other documentation is analyzed by Claude AI"
)
layout.addWidget(self.result_view, 1)
action_row = QHBoxLayout()
self.import_btn = QPushButton("Import Collection")
self.import_btn.setObjectName("accent")
self.import_btn.setFixedWidth(160)
self.import_btn.setEnabled(False)
self.import_btn.clicked.connect(self._generic_import)
self.env_btn = QPushButton("Create Environment")
self.env_btn.setFixedWidth(160)
self.env_btn.setEnabled(False)
self.env_btn.clicked.connect(self._generic_create_env)
self.both_btn = QPushButton("Import Both")
self.both_btn.setObjectName("accent")
self.both_btn.setFixedWidth(120)
self.both_btn.setEnabled(False)
self.both_btn.clicked.connect(self._generic_import_both)
action_row.addWidget(self.import_btn)
action_row.addWidget(self.env_btn)
action_row.addWidget(self.both_btn)
action_row.addStretch()
layout.addLayout(action_row)
return w
def _analyze(self):
url = self.url_input.text().strip()
raw_text = self.paste_editor.toPlainText().strip()
base_url = self.base_url_input.text().strip()
models_raw = self.models_input.text().strip()
models = [m.strip() for m in models_raw.split(",") if m.strip()]
if not url and not raw_text:
QMessageBox.warning(self, "Input Required", "Enter a URL or paste documentation text.")
return
self._generic_result = None
self.analyze_btn.setEnabled(False)
self.progress_bar.setVisible(True)
self.result_view.clear()
self._set_generic_action_buttons(False)
self._worker = AnalysisWorker(url=url, raw_text=raw_text,
base_url=base_url, models=models)
self._worker.progress.connect(lambda m: self.status_label.setText(m))
self._worker.finished.connect(self._on_generic_finished)
self._worker.error.connect(self._on_generic_error)
self._worker.start()
def _on_generic_finished(self, result: dict):
self._generic_result = result
self.analyze_btn.setEnabled(True)
self.progress_bar.setVisible(False)
self._set_generic_action_buttons(True)
source = result.pop("_source", "ai")
src_label = {"openapi": "OpenAPI spec (local)", "ai": "Claude AI"}.get(source, source)
endpoints = result.get("endpoints", [])
env_vars = result.get("environment_variables", {})
notes = result.get("notes", "")
lines = [
f"✓ Parsed via: {src_label}",
f"✓ Collection: {result.get('collection_name', 'Unnamed')}",
f"✓ Base URL: {result.get('base_url', '')}",
f"✓ Auth type: {result.get('auth_type', 'none')}",
f"✓ Endpoints: {len(endpoints)} found",
f"✓ Env vars: {list(env_vars.keys()) or ''}",
]
if notes:
lines += ["", "── Notes ─────────────────", notes]
lines += ["", "── Endpoints ─────────────────────────────────────"]
for ep in endpoints:
lines.append(f" {ep['method']:<8} {ep['path']} ({ep.get('name','')})")
if env_vars:
lines += ["", "── Environment Variables ──────────────────────────"]
for k, v in env_vars.items():
lines.append(f" {k} = {v!r}")
self.result_view.setPlainText("\n".join(lines))
self.status_label.setText(f"✓ Found {len(endpoints)} endpoint(s)")
def _on_generic_error(self, msg: str):
self.analyze_btn.setEnabled(True)
self.progress_bar.setVisible(False)
self.result_view.setPlainText(f"✗ Error:\n\n{msg}")
self.status_label.setText("Error — see results panel")
def _set_generic_action_buttons(self, enabled: bool):
self.import_btn.setEnabled(enabled)
self.env_btn.setEnabled(enabled)
self.both_btn.setEnabled(enabled)
def _generic_import(self):
if hasattr(self, "_generic_result") and self._generic_result:
self._do_import(self._generic_result)
def _generic_create_env(self):
if hasattr(self, "_generic_result") and self._generic_result:
self._do_create_env(self._generic_result)
def _generic_import_both(self):
if hasattr(self, "_generic_result") and self._generic_result:
self._do_import(self._generic_result)
self._do_create_env(self._generic_result)
# ══════════════════════════════════════════════════════════════════════════
# Tab 3 — Settings
# ══════════════════════════════════════════════════════════════════════════
def _build_settings_tab(self) -> QWidget:
w = QWidget()
w.setObjectName("panelBody")
outer = QVBoxLayout(w)
outer.setContentsMargins(24, 20, 24, 20)
outer.setSpacing(16)
hint = QLabel(
"EKIKA AI Assistant uses Claude by Anthropic to analyze plain-text API documentation.\n"
"OpenAPI/Swagger specs and EKIKA Odoo Framework collections are generated locally — "
"no API key required for those."
)
hint.setObjectName("hintText")
hint.setWordWrap(True)
outer.addWidget(hint)
form = QFormLayout()
form.setSpacing(10)
self.api_key_input = QLineEdit()
self.api_key_input.setEchoMode(QLineEdit.EchoMode.Password)
self.api_key_input.setPlaceholderText("sk-ant-…")
self.api_key_input.setText(ai_client.get_api_key())
show_key = QCheckBox("Show")
show_key.toggled.connect(lambda on: self.api_key_input.setEchoMode(
QLineEdit.EchoMode.Normal if on else QLineEdit.EchoMode.Password))
key_row = QHBoxLayout()
key_row.addWidget(self.api_key_input)
key_row.addWidget(show_key)
form.addRow("Anthropic API Key:", key_row)
outer.addLayout(form)
save_key_btn = QPushButton("Save API Key")
save_key_btn.setObjectName("accent")
save_key_btn.setFixedWidth(130)
save_key_btn.clicked.connect(self._save_api_key)
outer.addWidget(save_key_btn)
outer.addStretch()
info = QLabel(
"Get your key at console.anthropic.com\n"
"Keys are stored locally in the EKIKA database only."
)
info.setObjectName("hintText")
info.setWordWrap(True)
outer.addWidget(info)
return w
def _save_api_key(self):
ai_client.set_api_key(self.api_key_input.text().strip())
QMessageBox.information(self, "Saved", "Anthropic API key saved.")
# ══════════════════════════════════════════════════════════════════════════
# Shared import helpers
# ══════════════════════════════════════════════════════════════════════════
def _do_import(self, result: dict):
col_name = result.get("collection_name", "AI Import")
endpoints = result.get("endpoints", [])
base_url = result.get("base_url", "")
col_id = storage.add_collection(col_name)
for ep in endpoints:
# Build full URL with {{base_url}} variable
path = ep.get("path", "")
if not path.startswith("http") and base_url:
url = f"{{{{base_url}}}}{path}"
else:
url = ep.get("url", path)
req = HttpRequest(
name = ep.get("name", ""),
method = ep.get("method", "GET"),
url = url,
headers = ep.get("headers", {}),
params = ep.get("params", {}),
body = ep.get("body", ""),
body_type = ep.get("body_type", "raw"),
content_type = ep.get("content_type", ""),
test_script = ep.get("test_script", ""),
)
storage.save_request(col_id, req)
self.collection_imported.emit()
QMessageBox.information(
self, "Collection Imported",
f"✓ Imported '{col_name}'\n"
f" {len(endpoints)} request(s) added to the sidebar."
)
def _do_create_env(self, result: dict):
env_vars = result.get("environment_variables", {})
col_name = result.get("collection_name", "AI Import")
env_name = f"{col_name} — Environment"
if not env_vars:
QMessageBox.information(self, "No Variables", "No environment variables detected.")
return
env = Environment(name=env_name, variables=env_vars)
env_id = storage.save_environment(env)
QMessageBox.information(
self, "Environment Created",
f"✓ Created environment '{env_name}'\n"
f" Variables: {', '.join(env_vars.keys())}"
)