Ctrl K
ComunidadGuías…

webapp/templates/index.html

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
{% extends "base.html" %}
{% block title %}{{ _("Ask · Local SQL Agent") }}{% endblock %}
{% block content %}

{% set sample_questions = [
  _("Top 5 customers by purchase value in 1402"),
  _("Trade volume in the cement ring in Mordad 1405"),
  _("Which ring had the highest sales in 1404?"),
  _("Total purchase value by month in 1405"),
  _("How many active customers exist?"),
  _("Sales on Thursdays in the petrochemical ring"),
  _("Top 5 brokers by purchase value in 1403"),
  _("Sales in the Kish export ring in 1402"),
] %}

<div class="layout">
  <aside class="card sample-panel">
    <h2>{{ _("Sample Questions") }}</h2>
    <p class="muted sample-hint">{{ _("Click a question to ask it instantly.") }}</p>
    <div class="sample-list">
      {% for question in sample_questions %}
      <button type="button" class="sample-btn" data-question="{{ question }}">{{ question }}</button>
      {% endfor %}
    </div>
  </aside>

  <div class="main-panel">

<section class="card ask-card">
  <h2>{{ _("Ask your question") }}</h2>
  <p class="muted">{{ _("English or Persian — the agent will answer against the database.") }}</p>
  <form method="post" action="{{ url_for('index') }}" id="ask-form">
    <!-- Enforced server-side (webapp/app.py's before_request _check_csrf,
         finding 17b) -- a token-less POST here is rejected with 400. -->
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
    <textarea name="question" rows="4" dir="auto" required></textarea>
    <div class="form-row">
      <label class="check">
        <input type="checkbox" name="interpret" checked>
        {{ _("Show plain-language summary") }}
      </label>
      <div class="btn-group">
        <button class="btn btn-primary" type="submit">{{ _("Ask") }}</button>
      </div>
    </div>
  </form>
</section>

<div id="loading" class="loading" hidden>
  <img src="{{ url_for('static', filename='images/spinner.svg') }}" alt="Loading">
  <p class="muted">{{ _("Running your question…") }}</p>
</div>

{% if result %}
<section class="card results-card">
  <div class="results-head">
    <h2>{{ _("Results") }}</h2>
    {% if result.status == "SUCCESS" %}
      <span class="badge badge-ok">{{ _("SUCCESS") }}</span>
    {% else %}
      <span class="badge badge-err">{{ _("ERROR") }}</span>
    {% endif %}
    <span class="muted meta">{{ _("model") }}: {{ result.model or "—" }} · {% if current_lang == "fa" %}<bdi>{{ result.elapsed_seconds }}</bdi> {{ _("s") }}{% else %}{{ result.elapsed_seconds }}{{ _("s") }}{% endif %}</span>
  </div>

  <p class="asked" dir="auto"><strong>{{ _("Question:") }}</strong> {{ result.question }}</p>

  {% if result.sql %}
  <details class="sql-box">
    <summary>{{ _("Generated SQL") }}</summary>
    <div class="sql-body" dir="ltr">
      <pre><code id="sql-code">{{ result.sql }}</code></pre>
      <button type="button" class="btn btn-small btn-copy" id="copy-sql">{{ _("Copy") }}</button>
    </div>
  </details>
  {% endif %}

  {% if result.interpretation %}
  <p class="interpretation" dir="auto">{{ result.interpretation }}</p>
  {% endif %}

  {% if result.columns %}
  <div class="table-wrap">
    <table>
      <thead>
        <tr>{% for col in result.columns %}<th dir="auto">{{ col }}</th>{% endfor %}</tr>
      </thead>
      <tbody id="results-body">
        {% for row in result.rows %}
        <tr>{% for col in result.columns %}<td dir="auto">{{ row[col] }}</td>{% endfor %}</tr>
        {% endfor %}
      </tbody>
    </table>
  </div>
  <nav class="pagination" id="pagination" hidden aria-label="{{ _('Results') }}">
    <button type="button" class="btn btn-small" id="page-prev">{{ _("Previous") }}</button>
    <span class="page-info" id="page-info" dir="auto"></span>
    <button type="button" class="btn btn-small" id="page-next">{{ _("Next") }}</button>
  </nav>
  <p class="meta">
    {% if current_lang == "fa" %}
    {{ result.row_count }} ردیف
    {% else %}
    {{ result.row_count }} row{{ "s" if result.row_count != 1 }}
    {% endif %}
  </p>
  {% elif result.status == "SUCCESS" %}
  <p class="muted">{{ _("No rows returned for this question.") }}</p>
  {% endif %}

  {% if output_filename %}
  <div class="results-download">
    <a class="btn btn-download btn-download-icon" href="{{ url_for('download', filename=output_filename) }}" title="{{ _('Report') }}" aria-label="{{ _('Report') }}"><svg class="icon-dl" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg></a>
  </div>
  {% endif %}
</section>
{% endif %}

  </div>
</div>

<script src="{{ url_for('static', filename='vendor/sql-formatter.min.js') }}"></script>
<script src="{{ url_for('static', filename='vendor/prism.min.js') }}"></script>
<script src="{{ url_for('static', filename='vendor/prism-sql.min.js') }}"></script>
<script>
  // Guards against double submission — Enter (requestSubmit) bypasses a
  // disabled button, so the flag is checked in the handler itself.
  let formSubmitting = false;
  document.getElementById("ask-form").addEventListener("submit", (e) => {
    if (formSubmitting) {
      e.preventDefault();
      return;
    }
    formSubmitting = true;
    document.getElementById("loading").hidden = false;
    document.querySelector("#ask-form button[type=submit]").disabled = true;
    document.querySelectorAll(".sample-btn").forEach((btn) => (btn.disabled = true));
    document.querySelectorAll(".results-card, .btn-download").forEach((el) => el.remove());
  });

  // Enter submits the question; Shift+Enter inserts a newline (IME-safe so
  // confirming a Persian composition candidate never submits mid-word).
  const questionInput = document.querySelector("#ask-form textarea[name=question]");
  if (questionInput) {
    questionInput.addEventListener("keydown", (e) => {
      if (e.key !== "Enter" || e.shiftKey || e.isComposing || e.keyCode === 229) return;
      e.preventDefault();
      questionInput.form.requestSubmit();
    });
  }

  // Clicking a sample question fills the box and submits it immediately
  // (the ASK button is disabled while the request is processing).
  document.querySelectorAll(".sample-btn").forEach((btn) => {
    btn.addEventListener("click", () => {
      if (formSubmitting || !questionInput) return;
      questionInput.value = btn.dataset.question;
      questionInput.focus();
      questionInput.form.requestSubmit();
    });
  });

  // Beautify the generated SQL and colour its keywords (Prism) so it is
  // easier to read. Falls back to the raw SQL if the libraries are missing.
  const sqlCode = document.getElementById("sql-code");
  if (sqlCode) {
    const rawSql = sqlCode.textContent;
    try {
      const formatted = window.sqlFormatter.format(rawSql, {
        language: "tsql",
        keywordCase: "upper",
      });
      sqlCode.innerHTML = window.Prism.highlight(formatted, window.Prism.languages.sql, "sql");
    } catch {
      sqlCode.textContent = rawSql;
    }
  }

  const copyBtn = document.getElementById("copy-sql");
  if (copyBtn) {
    copyBtn.addEventListener("click", async () => {
      const sql = document.querySelector(".sql-body code").innerText;
      try {
        await navigator.clipboard.writeText(sql);
      } catch {
        const ta = document.createElement("textarea");
        ta.value = sql;
        ta.style.position = "fixed";
        ta.style.opacity = "0";
        document.body.appendChild(ta);
        ta.select();
        document.execCommand("copy");
        ta.remove();
      }
      const original = copyBtn.textContent;
      copyBtn.textContent = "{{ _('Copied') }}";
      setTimeout(() => { copyBtn.textContent = original; }, 2000);
    });
  }

  // Paginate the results table at 20 rows per page.
  const resultsBody = document.getElementById("results-body");
  if (resultsBody) {
    const ROWS_PER_PAGE = 10;
    const rows = Array.from(resultsBody.querySelectorAll("tr"));
    const pageCount = Math.max(1, Math.ceil(rows.length / ROWS_PER_PAGE));
    const nav = document.getElementById("pagination");
    const prevBtn = document.getElementById("page-prev");
    const nextBtn = document.getElementById("page-next");
    const pageInfo = document.getElementById("page-info");
    const pageLabel = "{{ _('Page %(page)s of %(pages)s') }}";
    let page = 1;

    const renderPage = () => {
      const start = (page - 1) * ROWS_PER_PAGE;
      rows.forEach((row, i) => {
        row.hidden = i < start || i >= start + ROWS_PER_PAGE;
      });
      prevBtn.disabled = page <= 1;
      nextBtn.disabled = page >= pageCount;
      pageInfo.textContent = pageLabel
        .replace("%(page)s", String(page))
        .replace("%(pages)s", String(pageCount));
    };

    if (pageCount > 1) {
      nav.hidden = false;
      prevBtn.addEventListener("click", () => { if (page > 1) { page--; renderPage(); } });
      nextBtn.addEventListener("click", () => { if (page < pageCount) { page++; renderPage(); } });
      renderPage();
    }
  }
</script>

{% endblock %}