File size: 19,247 Bytes
d6cfd69
697d9fb
d6cfd69
 
43a9aea
d6cfd69
 
 
 
 
 
 
 
 
 
 
43a9aea
d6cfd69
 
43a9aea
 
d6cfd69
 
43a9aea
 
d6cfd69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43a9aea
d6cfd69
 
 
 
 
 
 
 
 
 
43a9aea
d6cfd69
 
 
 
 
 
697d9fb
d6cfd69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
/*!
 * Khazna AI Assistant — embeddable widget
 * Usage (LibGuides "Media / Widget" box, or any page):
 *   <div data-khazna-assistant data-height="680"></div>
 *   <script src="https://nikeshn-kulibrary.hf.space/widget.js" defer></script>
 * Options (data-* on the div):
 *   data-api     backend base URL (default: origin this script was loaded from)
 *   data-scope   initial tab: all | publications | persons | organisations | datasets
 *   data-height  box height in px (default 680; "auto" = grow with page, used by the Space's own page)
 *   data-scopes  comma list of tabs to show (default all five)
 *   data-model   gpt | claude
 */
(function () {
  "use strict";
  var SCRIPT = document.currentScript;
  var DEFAULT_API = SCRIPT ? new URL(SCRIPT.src, location.href).origin : "";
  var SCOPE_LABELS = { all: "Everything", publications: "Publications", theses: "Theses", persons: "Profiles", organisations: "Departments", projects: "Projects", equipment: "Equipment", collaborations: "Collaborations", datasets: "Datasets" };
  var EXAMPLES = {
    all: ["KU research on hydrogen storage since 2022", "Who at KU works on agricultural robotics?", "How do I submit my thesis to Khazna?"],
    publications: ["Machine learning for corrosion prediction", "KU journal articles on 6G networks", "How many KU publications on COVID-19?"],
    theses: ["PhD theses on solar-powered desalination", "Master's theses on cybersecurity", "Theses supervised in robotics since 2022"],
    persons: ["Experts in lithium-ion batteries", "Who works on Arabic natural language processing?", "Find a supervisor for 6G wireless research"],
    organisations: ["Research centres working on space technology", "Department of Mechanical Engineering", "Labs focused on water and environment"],
    projects: ["Funded projects on water security", "Research projects on autonomous vehicles", "Projects funded by ASPIRE on AI"],
    equipment: ["Which labs have an electron microscope?", "Mass spectrometry facilities", "3D printing equipment"],
    datasets: ["Datasets on air quality in the UAE", "Seismic or geophysics datasets", "Medical imaging datasets"]
  };

  var CSS = [
    ".khz{--b:#0b3a63;--a:#b98a2e;--ink:#1b2430;--mu:#5d6876;--ln:#e1dfd8;--bg:#f7f6f2;--pn:#fff;--oa:#1f7a4d;--ch:#eef2f6;",
    "font:15px/1.55 'IBM Plex Sans',-apple-system,'Segoe UI',Roboto,Arial,sans-serif;color:var(--ink);background:var(--bg);",
    "border:1px solid var(--ln);border-radius:12px;display:flex;flex-direction:column;overflow:hidden;text-align:left;box-sizing:border-box}",
    ".khz *,.khz *:before,.khz *:after{box-sizing:border-box}",
    ".khz p,.khz ul{margin:0 0 8px}.khz ul{padding-left:20px}.khz li{margin:2px 0}",
    ".khz a{color:var(--b)}",
    ".khz button{font:inherit;line-height:1.3;cursor:pointer;text-transform:none;letter-spacing:normal;box-shadow:none}",
    ".khz-hd{display:flex;align-items:center;gap:10px;padding:12px 14px;background:var(--pn);border-bottom:1px solid var(--ln)}",
    ".khz-mk{flex:0 0 32px;height:32px;border-radius:8px;background:var(--b);color:#fff;display:flex;align-items:center;justify-content:center;font:600 16px Georgia,serif}",
    ".khz-hd b{display:block;font-size:15px}.khz-hd small{color:var(--mu);font-size:12px}",
    ".khz-tabs{display:flex;gap:4px;padding:8px 10px 0;background:var(--pn);border-bottom:1px solid var(--ln);overflow-x:auto;scrollbar-width:none}",
    ".khz-tab{background:none;border:0;border-bottom:2px solid transparent;color:var(--mu);padding:7px 10px;white-space:nowrap;border-radius:0}",
    ".khz-tab[aria-selected=true]{color:var(--b);border-bottom-color:var(--b);font-weight:600}",
    ".khz-log{flex:1;overflow-y:auto;padding:14px}",
    ".khz-intro{color:var(--mu);margin:0 0 12px}",
    ".khz-ex{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:6px}",
    ".khz-ex button{background:var(--pn);border:1px solid var(--ln);color:var(--ink);border-radius:999px;padding:5px 11px;font-size:13px}",
    ".khz-ex button:hover{border-color:var(--b)}",
    ".khz-m{margin:12px 0}.khz-u{display:flex;justify-content:flex-end}",
    ".khz-u span{background:var(--b);color:#fff;border-radius:14px 14px 4px 14px;padding:8px 13px;max-width:85%;white-space:pre-wrap}",
    ".khz-ans{background:var(--pn);border:1px solid var(--ln);border-radius:4px 14px 14px 14px;padding:12px 14px}",
    ".khz-ans p:last-child,.khz-ans ul:last-child{margin-bottom:0}",
    ".khz-wait{color:var(--mu);font-style:italic}",
    ".khz-cite{text-decoration:none;font-weight:600;font-size:11px;vertical-align:super;margin-left:1px}",
    ".khz-sr{margin:7px 2px 0;font-size:12px;color:var(--mu);display:flex;flex-wrap:wrap;gap:5px;align-items:center}",
    ".khz-chip{background:var(--ch);border-radius:5px;padding:1px 7px;font:12px ui-monospace,Menlo,Consolas,monospace;color:var(--ink)}",
    ".khz-cards{display:grid;gap:8px;margin-top:9px}",
    ".khz-c{background:var(--pn);border:1px solid var(--ln);border-left:3px solid var(--b);border-radius:8px;padding:10px 13px}",
    ".khz-c.p{border-left-color:var(--a)}.khz-c.o{border-left-color:#5b6b7d}.khz-c.d{border-left-color:var(--oa)}",
    ".khz-c.fl{outline:2px solid var(--a)}",
    ".khz-c .t{font-weight:600;color:var(--ink);text-decoration:none}.khz-c .t:hover{text-decoration:underline}",
    ".khz-c .n{color:var(--mu);font-weight:500;margin-right:5px}",
    ".khz-c .k{font-size:10.5px;text-transform:uppercase;letter-spacing:.05em;color:var(--mu);margin-left:6px}",
    ".khz-c .mt{color:var(--mu);font-size:13px;margin-top:2px}.khz-c .mt b{color:var(--ink);font-weight:500}",
    ".khz-c .ln{margin-top:5px;display:flex;gap:10px;flex-wrap:wrap;font-size:12.5px}",
    ".khz-bd{font-size:11px;border-radius:4px;padding:0 6px;border:1px solid currentColor;color:var(--oa)}",
    ".khz-more{background:none;border:1px dashed var(--ln);color:var(--mu);border-radius:8px;padding:7px;width:100%}",
    ".khz-fb{margin-top:6px;font-size:12px;color:var(--mu);display:flex;gap:6px;align-items:center}",
    ".khz-fb button{background:none;border:1px solid var(--ln);border-radius:6px;padding:1px 8px;color:var(--mu)}",
    ".khz-ft{border-top:1px solid var(--ln);background:var(--pn);padding:10px}",
    ".khz-f{display:flex;gap:8px}",
    ".khz-f textarea{flex:1;resize:none;border:1px solid var(--ln);background:var(--bg);color:var(--ink);border-radius:10px;padding:9px 11px;font:inherit;min-height:42px;max-height:120px;margin:0}",
    ".khz-f textarea:focus{outline:2px solid var(--b);outline-offset:-1px}",
    ".khz-f button{background:var(--b);color:#fff;border:0;border-radius:10px;padding:0 16px;font-weight:600}",
    ".khz-f button:disabled{opacity:.5;cursor:default}",
    ".khz-nt{color:var(--mu);font-size:11px;margin:6px 2px 0}",
    ".khz-nt a{color:inherit}",
    "@media (max-width:520px){.khz-u span{max-width:94%}.khz-hd small{display:none}}"
  ].join("");

  function esc(s) { return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) { return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]; }); }
  function safeUrl(u) { return /^https?:\/\//i.test(u || "") ? esc(u) : "#"; }

  function md(text, t) {
    var h = esc(text)
      .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
      .replace(/(https?:\/\/[^\s<)]+)/g, '<a href="$1" target="_blank" rel="noopener">$1</a>')
      .replace(/\[(\d+(?:\s*,\s*\d+)*)\]/g, function (m, g) {
        return g.split(",").map(function (n) { n = n.trim(); return '<a class="khz-cite" href="#" data-c="' + t + "-" + n + '">[' + n + "]</a>"; }).join("");
      });
    var out = [], list = false;
    h.split("\n").forEach(function (line) {
      var l = line.trim();
      if (/^[-•*]\s+/.test(l)) { if (!list) { out.push("<ul>"); list = true; } out.push("<li>" + l.replace(/^[-•*]\s+/, "") + "</li>"); return; }
      if (list) { out.push("</ul>"); list = false; }
      if (l) out.push("<p>" + l + "</p>");
    });
    if (list) out.push("</ul>");
    return out.join("");
  }

  function card(r, t) {
    var id = ' data-id="' + t + "-" + r.n + '"', num = r.n ? '<span class="n">' + r.n + "</span>" : "";
    var link = '<a class="t" href="' + safeUrl(r.portal_url) + '" target="_blank" rel="noopener">';
    if (r.kind === "person") {
      var units = (r.organisations || []).map(function (o) { return o.name; }).filter(Boolean).join("; ");
      var ev = (r.evidence || []).map(function (e) { return '<a href="' + safeUrl(e.portal_url) + '" target="_blank" rel="noopener">' + esc(e.title) + "</a>" + (e.year ? " (" + e.year + ")" : ""); }).join("<br>");
      return '<div class="khz-c p"' + id + "><div>" + num + link + esc(r.name) + '</a><span class="k">Profile</span></div>' +
        ((r.job_titles || []).length || units ? '<div class="mt">' + esc([(r.job_titles || []).join(", "), units].filter(Boolean).join(" · ")) + "</div>" : "") +
        ((r.keywords || []).length ? '<div class="mt">Expertise: ' + esc(r.keywords.slice(0, 6).join(", ")) + "</div>" : "") +
        (r.matching_outputs ? '<div class="mt"><b>' + r.matching_outputs + " matching output" + (r.matching_outputs > 1 ? "s" : "") + "</b>" + (ev ? "<br>" + ev : "") + "</div>" : "") +
        '<div class="ln"><a href="' + safeUrl(r.portal_url) + '" target="_blank" rel="noopener">View profile in Khazna</a>' +
        (r.orcid ? '<a href="https://orcid.org/' + esc(r.orcid) + '" target="_blank" rel="noopener">ORCID</a>' : "") + "</div></div>";
    }
    if (r.kind === "organisation") {
      return '<div class="khz-c o"' + id + "><div>" + num + link + esc(r.name) + '</a><span class="k">' + esc(r.type || "Unit") + "</span></div>" +
        ((r.parents || []).length ? '<div class="mt">Part of ' + esc(r.parents.join(", ")) + "</div>" : "") +
        '<div class="ln"><a href="' + safeUrl(r.portal_url) + '" target="_blank" rel="noopener">View unit, people &amp; outputs</a></div></div>';
    }
    var ku = {}; (r.ku_authors || []).forEach(function (a) { ku[a.name] = 1; });
    var au = (r.authors || []).slice(0, 8).map(function (a) { return ku[a] ? "<b>" + esc(a) + "</b>" : esc(a); }).join(", ") + ((r.authors || []).length > 8 ? " et al." : "");
    var meta = [r.year, r.type, r.venue].filter(Boolean).map(esc).join(" · ");
    return '<div class="khz-c' + (r.kind === "dataset" ? " d" : "") + '"' + id + "><div>" + num + link + esc(r.title) + "</a>" +
      (r.kind === "dataset" ? '<span class="k">Dataset</span>' : r.kind === "thesis" ? '<span class="k">Thesis</span>' : "") + "</div>" +
      '<div class="mt">' + (au || "—") + "</div>" + (meta ? '<div class="mt">' + meta + "</div>" : "") +
      '<div class="ln"><a href="' + safeUrl(r.portal_url) + '" target="_blank" rel="noopener">View in Khazna</a>' +
      (r.doi_url ? '<a href="' + safeUrl(r.doi_url) + '" target="_blank" rel="noopener">DOI</a>' : "") +
      (r.open_access ? '<span class="khz-bd">Open access</span>' : "") + "</div></div>";
  }

  function mount(root) {
    if (root.__khz) return; root.__khz = 1;
    var api = (root.getAttribute("data-api") || DEFAULT_API).replace(/\/$/, "");
    var height = root.getAttribute("data-height") || "680";
    var scopes = (root.getAttribute("data-scopes") || "all,publications,theses,persons,organisations,projects,equipment").split(",").map(function (s) { return s.trim(); }).filter(function (s) { return SCOPE_LABELS[s]; });
    var scope = root.getAttribute("data-scope") || scopes[0] || "all";
    var model = root.getAttribute("data-model") || "gpt";
    var history = [], turn = 0, busy = false, uid = Math.random().toString(36).slice(2, 7);

    root.innerHTML =
      '<div class="khz" style="' + (height === "auto" ? "min-height:100%;height:100%" : "height:" + parseInt(height, 10) + "px") + '">' +
      '<div class="khz-hd"><div class="khz-mk">K</div><div><b>Khazna AI Assistant</b><small>Ask in plain language — searches KU\'s research repository</small></div></div>' +
      '<div class="khz-tabs" role="tablist">' + scopes.map(function (s) { return '<button class="khz-tab" role="tab" data-s="' + s + '" aria-selected="' + (s === scope) + '">' + SCOPE_LABELS[s] + "</button>"; }).join("") + "</div>" +
      '<div class="khz-log" aria-live="polite"><p class="khz-intro">I turn your question into searches on <a href="https://khazna.ku.ac.ae/" target="_blank" rel="noopener">Khazna</a> and summarise what KU has published, who works on it, and where, citing every claim.</p><div class="khz-ex"></div></div>' +
      '<div class="khz-ft"><form class="khz-f"><label for="khz-q-' + uid + '" style="position:absolute;left:-9999px">Your question</label><textarea id="khz-q-' + uid + '" rows="1" placeholder="e.g. What has KU published on 6G networks?"></textarea><button type="submit">Ask</button></form>' +
      '<p class="khz-nt">AI-generated from public Khazna records; answers may be incomplete. Need help? <a href="https://library.ku.ac.ae/" target="_blank" rel="noopener">KU Library Research Support</a></p></div></div>';

    var log = root.querySelector(".khz-log"), ex = root.querySelector(".khz-ex"), ta = root.querySelector("textarea"), btn = root.querySelector(".khz-f button");
    function drawExamples() {
      if (!ex.isConnected) return;
      ex.innerHTML = (EXAMPLES[scope] || EXAMPLES.all).map(function (e) { return "<button type=\"button\">" + esc(e) + "</button>"; }).join("");
      ex.querySelectorAll("button").forEach(function (b) { b.onclick = function () { ask(b.textContent); }; });
    }
    drawExamples();
    root.querySelectorAll(".khz-tab").forEach(function (b) {
      b.onclick = function () {
        scope = b.getAttribute("data-s");
        root.querySelectorAll(".khz-tab").forEach(function (x) { x.setAttribute("aria-selected", x === b); });
        ta.placeholder = { persons: "e.g. Who at KU works on desalination membranes?", organisations: "e.g. Which centres work on robotics?", datasets: "e.g. Datasets on UAE groundwater" }[scope] || "e.g. What has KU published on 6G networks?";
        drawExamples(); ta.focus();
      };
    });

    function add(html, cls) { var d = document.createElement("div"); d.className = "khz-m " + cls; d.innerHTML = html; log.appendChild(d); log.scrollTop = log.scrollHeight; return d; }

    // Free HF Spaces sleep when idle; the first request after a sleep can take ~30-60 s or return 502/503.
    function post(body, onWake) {
      var tries = 0;
      function go() {
        tries++;
        return fetch(api + "/agent", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) })
          .then(function (r) {
            if ((r.status === 502 || r.status === 503 || r.status === 504) && tries < 6) { onWake(); return new Promise(function (ok) { setTimeout(ok, 8000); }).then(go); }
            return r.json();
          })
          .catch(function (e) { if (tries < 6) { onWake(); return new Promise(function (ok) { setTimeout(ok, 8000); }).then(go); } throw e; });
      }
      return go();
    }

    function ask(question) {
      if (busy) return; busy = true; btn.disabled = true;
      if (ex.parentNode) ex.parentNode.removeChild(ex);
      var t = uid + (++turn);
      add("<span>" + esc(question) + "</span>", "khz-u");
      var p = add('<div class="khz-ans khz-wait">Searching Khazna…</div>', "khz-b");
      post({ question: question, history: history, model: model, scope: scope }, function () {
        p.innerHTML = '<div class="khz-ans khz-wait">Waking up the assistant — this can take up to a minute after a quiet period…</div>';
      }).then(function (d) {
        var h = '<div class="khz-ans">' + md(d.answer || "No answer.", t) + "</div>";
        var seenQ = {}, qs = (d.trace || []).filter(function (x) { var k = (x.query || "").toLowerCase(); if (!k || seenQ[k]) return false; seenQ[k] = 1; return true; }).map(function (x) { return '<span class="khz-chip">' + esc(x.query) + "</span>"; }).join("");
        if (qs) h += '<div class="khz-sr">Searched: ' + qs + (d.portal_search_url ? ' · <a href="' + safeUrl(d.portal_search_url) + '" target="_blank" rel="noopener">open in Khazna</a>' : "") + "</div>";
        var cards = (d.person ? [card(d.person, t)] : []).concat((d.search_results || []).map(function (r) { return card(r, t); }));
        if (cards.length) h += '<div class="khz-cards">' + cards.join("") + "</div>";
        if ((d.more_results || []).length) h += '<div class="khz-cards"><button type="button" class="khz-more">Show ' + d.more_results.length + ' more</button><div class="khz-cards" hidden>' + d.more_results.map(function (r) { return card(r, t); }).join("") + "</div></div>";
        if (d.intent && !/social|rate_limited|empty/.test(d.intent)) h += '<div class="khz-fb">Helpful? <button type="button" data-r="up" aria-label="Yes">👍</button><button type="button" data-r="down" aria-label="No">👎</button></div>';
        p.innerHTML = h;
        var more = p.querySelector(".khz-more");
        if (more) more.onclick = function () { more.nextElementSibling.hidden = false; more.parentNode.removeChild(more); };
        p.querySelectorAll(".khz-fb button").forEach(function (b) {
          b.onclick = function () {
            fetch(api + "/feedback", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ question: question, rating: b.getAttribute("data-r") }) });
            b.parentNode.textContent = "Thanks for the feedback.";
          };
        });
        p.querySelectorAll(".khz-cite").forEach(function (a) {
          a.onclick = function (e) {
            e.preventDefault();
            var el = root.querySelector('[data-id="' + a.getAttribute("data-c") + '"]'); if (!el) return;
            if (el.parentNode.hidden) el.parentNode.hidden = false;
            el.scrollIntoView({ behavior: "smooth", block: "center" }); el.classList.add("fl"); setTimeout(function () { el.classList.remove("fl"); }, 1400);
          };
        });
        history.push({ role: "user", content: question }, { role: "assistant", content: d.answer || "" });
        history = history.slice(-8);
      }).catch(function () {
        p.innerHTML = '<div class="khz-ans">Sorry — the assistant is unavailable right now. You can search directly at <a href="https://khazna.ku.ac.ae/" target="_blank" rel="noopener">khazna.ku.ac.ae</a>.</div>';
      }).then(function () { busy = false; btn.disabled = false; log.scrollTop = log.scrollHeight; });
    }

    root.querySelector("form").onsubmit = function (e) { e.preventDefault(); var q = ta.value.trim(); if (!q) return; ta.value = ""; ask(q); };
    ta.onkeydown = function (e) { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); root.querySelector("form").requestSubmit ? root.querySelector("form").requestSubmit() : root.querySelector("form").onsubmit(e); } };
    fetch(api + "/health").catch(function () {}); // wake a sleeping Space as soon as the guide loads
  }

  function init() {
    if (!document.getElementById("khz-css")) {
      var st = document.createElement("style"); st.id = "khz-css"; st.textContent = CSS; document.head.appendChild(st);
    }
    document.querySelectorAll("[data-khazna-assistant]").forEach(mount);
  }
  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init); else init();
})();