Reimplement compose and add tiling windows
This commit is contained in:
@@ -4,17 +4,29 @@
|
||||
{% load accessibility %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-GB">
|
||||
<html lang="en-GB" class="{% block html_class %}{% endblock %}">
|
||||
<head>
|
||||
<script>
|
||||
(function () {
|
||||
var storedTheme = null;
|
||||
var resolvedTheme = null;
|
||||
try {
|
||||
storedTheme = localStorage.getItem("theme");
|
||||
} catch (error) {
|
||||
}
|
||||
if (storedTheme === "dark" || storedTheme === "light") {
|
||||
document.documentElement.dataset.theme = storedTheme;
|
||||
resolvedTheme = storedTheme;
|
||||
} else {
|
||||
try {
|
||||
resolvedTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
if (resolvedTheme === "dark" || resolvedTheme === "light") {
|
||||
document.documentElement.dataset.theme = resolvedTheme;
|
||||
document.documentElement.style.colorScheme = resolvedTheme;
|
||||
} else {
|
||||
document.documentElement.removeAttribute("data-theme");
|
||||
}
|
||||
@@ -30,23 +42,11 @@
|
||||
<link rel="preload" href="{% static 'vendor/fontawesome/webfonts/fa-solid-900.woff2' %}" as="font" type="font/woff2" integrity="sha512-Ph1xTLhfMycYSW+wUN8oL3Ggl56nGIS95EHiKWggcL/GbMNjPdib1Hreb1D4COlMxdiGCkk43nspQnpDuTjgQg==" crossorigin="anonymous">
|
||||
<link rel="preload" href="{% static 'vendor/fontawesome/webfonts/fa-regular-400.woff2' %}" as="font" type="font/woff2" integrity="sha512-qioT43fXB5q4Bbpn8sPQE9OIZLjKD0c0lVmpm6KmT8k34LM6gkRcOOMi1BOl2lohFG/7p9tzKfTP5G563BQq1g==" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="{% static 'css/bulma.min.css' %}" integrity="sha512-yh2RE0wZCVZeysGiqTwDTO/dKelCbS9bP2L94UvOFtl/FKXcNAje3Y2oBg/ZMZ3LS1sicYk4dYVGtDex75fvvA==" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="{% static 'css/bulma-tooltip.min.css' %}" integrity="sha512-SNDNIUvSYhnqDV9FFXaH/e0xZ6NzkG4Qm5dafLLf0PCMkzICKaOmMTgI3y2t2jZK+hAtP6A7UBcFqjWMhsujIg==" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="{% static 'vendor/fontawesome/css/all.css' %}" integrity="sha512-UKBBxJ5N3/MYiSsYTlEsARsp4vELKVRIklED4Mb6wpuVFOgy5Blt+sXUdz1TDReqWsm64xxBA2QoBJRCxI0x5Q==" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="{% static 'css/bulma-slider.min.css' %}" integrity="sha512-9o5SkCRCA9thttRH3Gb5QXLxKdRiuRLdO6ToEPwRHGLXjrhTZwFj0rEHjrCcJvDN9/aNaWMpGOIEA2vZsHmEqw==" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="{% static 'css/bulma-calendar.min.css' %}" integrity="sha512-IOnJQkgQpezPDPTJcRiWD7YVI3sF2RYzYDl4isbDT2geSaEHRQ615UN/8GhJbSkvqkKRZu8SBCQ7XwKMqsqLFQ==" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="{% static 'css/bulma-tagsinput.min.css' %}" integrity="sha512-NWTkcDRubZ3pyXbZZLQBILuVsRFs8c6QGgnfe4dm5/d6yp50U+xdoCDLIcSo51fFy/GXH0O2Oed1Z1sF1faxDA==" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="{% static 'css/bulma-switch.min.css' %}" integrity="sha512-zjrHYubQoNgDVqVKTyGjKcvIeQlduZTvXCvcBwQ0iqJYKLKiz9cuFAN7e98zfKqCTpI/EgFRBRcTwJw20yAFuw==" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="{% static 'css/gridstack.min.css' %}" integrity="sha512-ttQfsDTO64bamkJHeLDf0kzMP1NKfkootudPWS2V8Pwy+9z1wexSYjIT6/HXGg/bmtD+DRwsUnQoYEB0yePjbw==" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="{% static 'css/gia-theme.css' %}" integrity="sha512-6BpiCie64b2f+fUAgX2EWY95FVBcGsSSnSJchQPwV5zFOR/A4sklIPC4HNXcT/NyOAY7PpNol6efClbh/NSGIA==" crossorigin="anonymous">
|
||||
<script src="{% static 'js/bulma-calendar.min.js' %}" integrity="sha512-kkEtEtypXzruevjkoxhyEkqkZBtlhK7s8zt7IV2yPabgBwy5xbKL9uWeCS37ldS9AaNTSnveWTu4ivUvGMJUWA==" crossorigin="anonymous"></script>
|
||||
<script src="{% static 'js/bulma-slider.min.js' %}" integrity="sha512-WLKXHCsMXTSIPsmQShJRE6K4IzwvNkhwxr/Oo8N3z+kzjhGleHibspmWLTawNMdl2z9E23XK20+yvUTDZ+zeNQ==" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/gia-theme.css' %}" integrity="sha512-ySzeXoQreOo29Fv+kBiggvr2yLJEj4LO+Srcdw6rAENl1cAl6fzjVITZld1grkwMb+Hxe1jczo623coGQr0jsw==" crossorigin="anonymous">
|
||||
{% block extra_head_assets %}{% endblock %}
|
||||
<script src="{% static 'js/htmx.min.js' %}" integrity="sha512-CGXFnDNv5q48ciFeIyWFcfZhqYW0sSBiPO+HZDO3XLM+p8xjhezz5CCxtkXVDKfCbvF+iUhel7xoeSp19o7x7g==" crossorigin="anonymous"></script>
|
||||
<script defer src="{% static 'js/remove-me.js' %}" integrity="sha512-uhE4kDw2+tXdJPDKSttOEYhVnwYq310+d5DMQnTjafJ58QLlYPyx0RTCNbjcrTiGfCjAhaQob4AumEUa2m3TaQ==" crossorigin="anonymous"></script>
|
||||
<script defer src="{% static 'js/hyperscript.min.js' %}" integrity="sha512-l43sZzpnAddmYhJyfPrgv46XhJvA95gsA28/+eW4XZLSekQ8wlP68i9f22KGkRjY0HNiZrLc5MXGo4z/tM2QNA==" crossorigin="anonymous"></script>
|
||||
<script src="{% static 'js/bulma-tagsinput.min.js' %}" integrity="sha512-Je6J++MjmmpxF30JCmRwM2KiK3uWQBQtqiNCjwzEMJKExLaa0BqerlYNa/fJAl5Rra4hMgRZF2fzg+V2vjE4Kw==" crossorigin="anonymous"></script>
|
||||
<script src="{% static 'js/jquery.min.js' %}" integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" crossorigin="anonymous"></script>
|
||||
<script src="{% static 'js/gridstack-all.js' %}" integrity="sha512-djBPxwvBhDep1SvOhliatweHMORhVO3HabrfBjaW6nYsa7UcJYHty31x42m4HBSJXcJSQdoEgRPLVYGGIuIaDQ==" crossorigin="anonymous"></script>
|
||||
<script defer src="{% static 'js/magnet.min.js' %}" integrity="sha512-aoQ3V4iCM8zTcdMDSUTRG1K9wqZzmDSisuaCLQexk9DdFy92oWvTUoAfCVLnGzzJClst8PmtasZg219REwyNkw==" crossorigin="anonymous"></script>
|
||||
<script>
|
||||
(function () {
|
||||
var FAVICON_VERSION = "sq3";
|
||||
@@ -110,6 +110,7 @@
|
||||
function applyTheme(mode, shouldPersist) {
|
||||
var validMode = mode === THEME_DARK ? THEME_DARK : THEME_LIGHT;
|
||||
root.dataset.theme = validMode;
|
||||
root.style.colorScheme = validMode;
|
||||
applyFavicon();
|
||||
updateToggleUI(validMode);
|
||||
if (shouldPersist !== false) {
|
||||
@@ -159,7 +160,40 @@
|
||||
if (target) {
|
||||
target.classList.toggle("is-active");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.body.addEventListener("htmx:afterRequest", function (event) {
|
||||
const detail = (event && event.detail) || null;
|
||||
const source = detail && detail.elt ? detail.elt : null;
|
||||
if (!detail || !detail.successful || !source || !source.dataset) {
|
||||
return;
|
||||
}
|
||||
const queueAfter = String(source.dataset.queueAfter || "");
|
||||
if (queueAfter === "remove-card") {
|
||||
const cardId = String(source.dataset.queueCardId || "").trim();
|
||||
const refreshEvent = String(source.dataset.queueRefreshEvent || "").trim();
|
||||
if (cardId) {
|
||||
const card = document.getElementById(cardId);
|
||||
if (card) {
|
||||
card.remove();
|
||||
}
|
||||
}
|
||||
if (refreshEvent) {
|
||||
htmx.trigger(document.body, refreshEvent);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (queueAfter === "show-inline-editor") {
|
||||
const editorId = String(source.dataset.queueEditorId || "").trim();
|
||||
if (!editorId) {
|
||||
return;
|
||||
}
|
||||
const editor = document.getElementById(editorId);
|
||||
if (editor) {
|
||||
editor.style.display = "block";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var composeLink = document.getElementById("nav-compose-link");
|
||||
@@ -201,14 +235,14 @@
|
||||
</script>
|
||||
</head>
|
||||
{% get_accessibility_settings request.user as a11y_settings %}
|
||||
<body{% if a11y_settings and a11y_settings.disable_animations %} class="reduced-motion"{% endif %}>
|
||||
<body class="{% if a11y_settings and a11y_settings.disable_animations %}reduced-motion {% endif %}{% block body_class %}{% endblock %}">
|
||||
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
<div class="navbar-brand">
|
||||
<div class="navbar-item">
|
||||
<span class="gia-brand-shell">
|
||||
{% if user.is_authenticated %}
|
||||
<button class="button is-light theme-toggle-button gia-brand-logo brand-theme-toggle js-theme-toggle" type="button" data-theme-mode="light" aria-label="Theme toggle">
|
||||
<button class="gia-brand-logo brand-theme-toggle js-theme-toggle" type="button" data-theme-mode="light" aria-label="Theme toggle">
|
||||
<svg class="brand-theme-logo" viewBox="0 0 213.35 150.85" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
|
||||
<g transform="translate(38.831 -7.4316)">
|
||||
<g transform="matrix(.99287 0 0 .99911 1.2367 -30.308)">
|
||||
@@ -291,7 +325,7 @@
|
||||
Services
|
||||
</a>
|
||||
|
||||
<div class="navbar-dropdown">
|
||||
<div class="navbar-dropdown is-right gia-navbar-dropdown">
|
||||
<a class="navbar-item" href="{% url 'signal' %}">
|
||||
Signal
|
||||
</a>
|
||||
@@ -309,7 +343,7 @@
|
||||
Data
|
||||
</a>
|
||||
|
||||
<div class="navbar-dropdown">
|
||||
<div class="navbar-dropdown is-right gia-navbar-dropdown">
|
||||
<a class="navbar-item" href="{% url 'sessions' type='page' %}">
|
||||
Sessions
|
||||
</a>
|
||||
@@ -324,7 +358,7 @@
|
||||
Settings
|
||||
</a>
|
||||
|
||||
<div class="navbar-dropdown">
|
||||
<div class="navbar-dropdown is-right gia-navbar-dropdown">
|
||||
<div class="navbar-item has-text-weight-semibold is-size-7 has-text-grey">
|
||||
General
|
||||
</div>
|
||||
@@ -391,28 +425,29 @@
|
||||
<a class="navbar-item{% if request.resolver_match.url_name == 'osint_workspace' %} is-current-route{% endif %}" href="{% url 'osint_workspace' %}">
|
||||
OSINT Workspace
|
||||
</a>
|
||||
<hr class="navbar-divider">
|
||||
<a class="navbar-item" href="{% url 'logout' %}">
|
||||
Logout
|
||||
</a>
|
||||
<button class="navbar-item button is-light add-button" type="button" style="display:none;">
|
||||
Install App
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="navbar-item">
|
||||
<div class="buttons">
|
||||
{% if not user.is_authenticated %}
|
||||
{% if not user.is_authenticated %}
|
||||
<div class="navbar-item">
|
||||
<div class="buttons">
|
||||
<a class="button is-info" href="{% url 'signup' %}">
|
||||
<strong>Sign up</strong>
|
||||
</a>
|
||||
<a class="button" href="{% url 'two_factor:login' %}">
|
||||
Log in
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<button class="button is-light add-button" type="button" style="display:none;">Install App</button>
|
||||
<a class="button is-dark" href="{% url 'logout' %}">Logout</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -449,218 +484,26 @@
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
window.giaPrepareWidgetTarget = function () {
|
||||
const target = document.getElementById("widgets-here");
|
||||
if (target) {
|
||||
target.style.display = "block";
|
||||
}
|
||||
};
|
||||
|
||||
window.giaCanSpawnWidgets = function () {
|
||||
return !!(
|
||||
window.grid &&
|
||||
typeof window.grid.addWidget === "function" &&
|
||||
document.getElementById("grid-stack-main") &&
|
||||
document.getElementById("widgets-here")
|
||||
);
|
||||
};
|
||||
|
||||
window.giaEnableWidgetSpawnButtons = function (root) {
|
||||
const scope = root && root.querySelectorAll ? root : document;
|
||||
const canSpawn = window.giaCanSpawnWidgets();
|
||||
scope.querySelectorAll(".js-widget-spawn-trigger").forEach(function (button) {
|
||||
const widgetUrl = String(
|
||||
button.getAttribute("data-widget-url")
|
||||
|| button.getAttribute("hx-get")
|
||||
|| ""
|
||||
).trim();
|
||||
const visible = canSpawn && !!widgetUrl;
|
||||
button.classList.toggle("is-hidden", !visible);
|
||||
button.setAttribute("aria-hidden", visible ? "false" : "true");
|
||||
});
|
||||
};
|
||||
|
||||
window.giaPrepareWindowAnchor = function (trigger) {
|
||||
if (!trigger || !trigger.getBoundingClientRect) {
|
||||
window.giaWindowAnchor = null;
|
||||
return;
|
||||
}
|
||||
const rect = trigger.getBoundingClientRect();
|
||||
window.giaWindowAnchor = {
|
||||
left: rect.left,
|
||||
right: rect.right,
|
||||
top: rect.top,
|
||||
bottom: rect.bottom,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
ts: Date.now(),
|
||||
};
|
||||
};
|
||||
|
||||
window.giaPositionFloatingWindow = function (windowEl) {
|
||||
if (!windowEl) {
|
||||
return;
|
||||
}
|
||||
const isMobile = window.matchMedia("(max-width: 768px)").matches;
|
||||
const margin = 12;
|
||||
const rect = windowEl.getBoundingClientRect();
|
||||
const anchor = window.giaWindowAnchor || null;
|
||||
windowEl.style.position = "fixed";
|
||||
|
||||
if (isMobile) {
|
||||
const centeredLeftViewport = Math.max(
|
||||
margin,
|
||||
Math.round((window.innerWidth - rect.width) / 2)
|
||||
);
|
||||
const centeredTopViewport = Math.max(
|
||||
margin,
|
||||
Math.round((window.innerHeight - rect.height) / 2)
|
||||
);
|
||||
windowEl.style.left = centeredLeftViewport + "px";
|
||||
windowEl.style.top = centeredTopViewport + "px";
|
||||
windowEl.style.right = "auto";
|
||||
windowEl.style.bottom = "auto";
|
||||
windowEl.style.transform = "none";
|
||||
windowEl.setAttribute("tabindex", "-1");
|
||||
if (typeof windowEl.focus === "function") {
|
||||
windowEl.focus({preventScroll: true});
|
||||
}
|
||||
if (typeof windowEl.scrollIntoView === "function") {
|
||||
windowEl.scrollIntoView({block: "center", inline: "center", behavior: "smooth"});
|
||||
}
|
||||
window.giaWindowAnchor = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!anchor || (Date.now() - anchor.ts) > 10000) {
|
||||
return;
|
||||
}
|
||||
|
||||
const desiredLeftViewport = anchor.left;
|
||||
const desiredTopViewport = anchor.bottom + 6;
|
||||
const maxLeftViewport = window.innerWidth - rect.width - margin;
|
||||
const maxTopViewport = window.innerHeight - rect.height - margin;
|
||||
const boundedLeftViewport = Math.max(
|
||||
margin,
|
||||
Math.min(desiredLeftViewport, maxLeftViewport)
|
||||
);
|
||||
const boundedTopViewport = Math.max(
|
||||
margin,
|
||||
Math.min(desiredTopViewport, maxTopViewport)
|
||||
);
|
||||
windowEl.style.left = boundedLeftViewport + "px";
|
||||
windowEl.style.top = boundedTopViewport + "px";
|
||||
windowEl.style.right = "auto";
|
||||
windowEl.style.bottom = "auto";
|
||||
windowEl.style.transform = "none";
|
||||
window.giaWindowAnchor = null;
|
||||
};
|
||||
|
||||
window.giaEnableFloatingWindowInteractions = function (windowEl) {
|
||||
if (!windowEl || windowEl.dataset.giaWindowInteractive === "1") {
|
||||
return;
|
||||
}
|
||||
windowEl.dataset.giaWindowInteractive = "1";
|
||||
|
||||
// Disable magnet-block global drag so text inputs remain editable.
|
||||
windowEl.setAttribute("unmovable", "");
|
||||
|
||||
const heading = windowEl.querySelector(".panel-heading");
|
||||
if (!heading) {
|
||||
return;
|
||||
}
|
||||
|
||||
let dragging = false;
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
let startLeft = 0;
|
||||
let startTop = 0;
|
||||
|
||||
const onMove = function (event) {
|
||||
if (!dragging) {
|
||||
return;
|
||||
}
|
||||
const deltaX = event.clientX - startX;
|
||||
const deltaY = event.clientY - startY;
|
||||
windowEl.style.left = (startLeft + deltaX) + "px";
|
||||
windowEl.style.top = (startTop + deltaY) + "px";
|
||||
windowEl.style.right = "auto";
|
||||
windowEl.style.bottom = "auto";
|
||||
};
|
||||
|
||||
const stopDrag = function () {
|
||||
dragging = false;
|
||||
document.removeEventListener("pointermove", onMove);
|
||||
document.removeEventListener("pointerup", stopDrag);
|
||||
};
|
||||
|
||||
heading.addEventListener("pointerdown", function (event) {
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
const interactive = event.target.closest(
|
||||
"button, a, input, textarea, select, label, .delete, .icon"
|
||||
);
|
||||
if (interactive) {
|
||||
return;
|
||||
}
|
||||
const rect = windowEl.getBoundingClientRect();
|
||||
windowEl.style.position = "fixed";
|
||||
startLeft = rect.left;
|
||||
startTop = rect.top;
|
||||
startX = event.clientX;
|
||||
startY = event.clientY;
|
||||
dragging = true;
|
||||
document.addEventListener("pointermove", onMove);
|
||||
document.addEventListener("pointerup", stopDrag);
|
||||
event.preventDefault();
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener("click", function (event) {
|
||||
const trigger = event.target.closest(".js-widget-spawn-trigger");
|
||||
if (!trigger) {
|
||||
return;
|
||||
}
|
||||
window.giaPrepareWidgetTarget();
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
window.giaEnableWidgetSpawnButtons(document);
|
||||
});
|
||||
|
||||
document.body.addEventListener("htmx:afterSwap", function (event) {
|
||||
const target = (event && event.target) || document;
|
||||
window.giaEnableWidgetSpawnButtons(target);
|
||||
const targetId = (target && target.id) || "";
|
||||
if (targetId === "windows-here") {
|
||||
const floatingWindows = target.querySelectorAll(".floating-window");
|
||||
floatingWindows.forEach(function (floatingWindow) {
|
||||
window.setTimeout(function () {
|
||||
window.giaPositionFloatingWindow(floatingWindow);
|
||||
window.giaEnableFloatingWindowInteractions(floatingWindow);
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{% block outer_content %}
|
||||
{% endblock %}
|
||||
<div>
|
||||
<div class="container">
|
||||
{% include "partials/settings-hierarchy-nav.html" %}
|
||||
{% block content_wrapper %}
|
||||
{% block content %}
|
||||
{% block standard_page_shell %}
|
||||
<div class="gia-standard-page-shell">
|
||||
<div class="container">
|
||||
{% include "partials/settings-hierarchy-nav.html" %}
|
||||
{% block content_wrapper %}
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
<div id="modals-here">
|
||||
</div>
|
||||
<div id="windows-here" style="z-index: 120;">
|
||||
</div>
|
||||
<div id="widgets-here" style="display: none;">
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
<div id="modals-here">
|
||||
</div>
|
||||
<div id="windows-here" style="z-index: 120;">
|
||||
</div>
|
||||
<div id="widgets-here" style="display: none;">
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,100 +1,67 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load joinsep %}
|
||||
{% block html_class %}gia-has-workspace-root{% endblock %}
|
||||
{% block body_class %}gia-has-workspace{% endblock %}
|
||||
{% block extra_head_assets %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" href="{% static 'css/gridstack.min.css' %}" integrity="sha512-ttQfsDTO64bamkJHeLDf0kzMP1NKfkootudPWS2V8Pwy+9z1wexSYjIT6/HXGg/bmtD+DRwsUnQoYEB0yePjbw==" crossorigin="anonymous">
|
||||
<script defer src="{% static 'js/gridstack-all.js' %}" integrity="sha512-djBPxwvBhDep1SvOhliatweHMORhVO3HabrfBjaW6nYsa7UcJYHty31x42m4HBSJXcJSQdoEgRPLVYGGIuIaDQ==" crossorigin="anonymous"></script>
|
||||
<script defer src="{% static 'js/workspace-shell.js' %}"></script>
|
||||
{% endblock %}
|
||||
{% block standard_page_shell %}{% endblock %}
|
||||
{% block outer_content %}
|
||||
|
||||
<div class="grid-stack" id="grid-stack-main">
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var grid = GridStack.init({
|
||||
cellHeight: 20,
|
||||
cellWidth: 45,
|
||||
cellHeightUnit: 'px',
|
||||
auto: true,
|
||||
float: true,
|
||||
draggable: {handle: '.panel-heading', scroll: false, appendTo: 'body'},
|
||||
removable: false,
|
||||
animate: true,
|
||||
});
|
||||
// GridStack.init();
|
||||
|
||||
// a widget is ready to be loaded
|
||||
document.addEventListener('load-widget', function(event) {
|
||||
let containers = htmx.findAll('#widget');
|
||||
for (let x = 0, len = containers.length; x < len; x++) {
|
||||
container = containers[x];
|
||||
// get the scripts, they won't be run on the new element so we need to eval them
|
||||
let widgetelement = container.firstElementChild.cloneNode(true);
|
||||
console.log(widgetelement);
|
||||
var scripts = htmx.findAll(widgetelement, "script");
|
||||
var new_id = widgetelement.id;
|
||||
|
||||
// check if there's an existing element like the one we want to swap
|
||||
let grid_element = htmx.find('#grid-stack-main');
|
||||
let existing_widget = htmx.find(grid_element, "#"+new_id);
|
||||
|
||||
// get the size and position attributes
|
||||
if (existing_widget) {
|
||||
let attrs = existing_widget.getAttributeNames();
|
||||
for (let i = 0, len = attrs.length; i < len; i++) {
|
||||
if (attrs[i].startsWith('gs-')) { // only target gridstack attributes
|
||||
widgetelement.setAttribute(attrs[i], existing_widget.getAttribute(attrs[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
// clear the queue element
|
||||
container.outerHTML = "";
|
||||
// container.firstElementChild.outerHTML = "";
|
||||
grid.addWidget(widgetelement);
|
||||
|
||||
// re-create the HTMX JS listeners, otherwise HTMX won't work inside the grid
|
||||
htmx.process(widgetelement);
|
||||
if (typeof window.giaEnableWidgetSpawnButtons === "function") {
|
||||
window.giaEnableWidgetSpawnButtons(widgetelement);
|
||||
}
|
||||
|
||||
// update the size of the widget according to its content
|
||||
var added_widget = htmx.find(grid_element, "#"+new_id);
|
||||
var itemContent = htmx.find(added_widget, ".control");
|
||||
var scrollheight = itemContent.scrollHeight+80;
|
||||
var verticalmargin = 0;
|
||||
var cellheight = grid.opts.cellHeight;
|
||||
var height = Math.ceil((scrollheight + verticalmargin) / (cellheight + verticalmargin));
|
||||
var opts = {
|
||||
h: height,
|
||||
}
|
||||
grid.update(
|
||||
added_widget,
|
||||
opts
|
||||
);
|
||||
|
||||
// run the JS scripts inside the added element again
|
||||
for (var i = 0; i < scripts.length; i++) {
|
||||
eval(scripts[i].innerHTML);
|
||||
}
|
||||
}
|
||||
// clear the containers we just added
|
||||
// for (let x = 0, len = containers.length; x < len; x++) {
|
||||
// container = containers[x];
|
||||
// container.inner = "";
|
||||
// }
|
||||
grid.compact();
|
||||
if (typeof window.giaEnableWidgetSpawnButtons === "function") {
|
||||
window.giaEnableWidgetSpawnButtons(document);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<div>
|
||||
{% block load_widgets %}
|
||||
<!-- <div
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="#"
|
||||
hx-target="#widgets-here"
|
||||
hx-trigger="load"
|
||||
hx-swap="afterend"
|
||||
style="display: none;"></div> -->
|
||||
{% endblock %}
|
||||
</div>
|
||||
<section class="section gia-workspace-page">
|
||||
<div class="gia-workspace-shell">
|
||||
<div class="gia-workspace-main">
|
||||
<div class="gia-workspace-grid-column">
|
||||
<div class="grid-stack gia-workspace-grid" id="grid-stack-main">
|
||||
</div>
|
||||
</div>
|
||||
<aside
|
||||
id="gia-snap-assistant"
|
||||
class="panel gia-snap-assistant is-hidden"
|
||||
aria-label="Snap assistant">
|
||||
<p class="panel-heading gia-snap-assistant-heading">
|
||||
<span class="icon is-small"><i class="fa-solid fa-table-columns"></i></span>
|
||||
<span>Snap Right</span>
|
||||
<button
|
||||
type="button"
|
||||
class="delete is-small js-gia-snap-assistant-close"
|
||||
aria-label="Close snap assistant"></button>
|
||||
</p>
|
||||
<div class="panel-block">
|
||||
<div class="content is-small">
|
||||
<p>Choose a second window for the right side.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-block is-active gia-snap-assistant-body">
|
||||
<div
|
||||
id="gia-snap-assistant-options"
|
||||
class="buttons are-small gia-snap-assistant-options"></div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
<nav
|
||||
id="gia-taskbar"
|
||||
class="tabs is-boxed is-small gia-taskbar is-hidden"
|
||||
aria-label="Open windows">
|
||||
<ul id="gia-taskbar-items">
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div id="gia-workspace-stash" class="is-hidden" aria-hidden="true"></div>
|
||||
<div>
|
||||
{% block load_widgets %}
|
||||
<!-- <div
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="#"
|
||||
hx-target="#widgets-here"
|
||||
hx-trigger="load"
|
||||
hx-swap="beforeend"
|
||||
style="display: none;"></div> -->
|
||||
{% endblock %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,23 +1,62 @@
|
||||
<div id="widget">
|
||||
<div
|
||||
class="js-gia-widget-shell"
|
||||
data-gia-widget-shell="1"
|
||||
{% if widget_style_hrefs %}data-gia-style-hrefs="{{ widget_style_hrefs|join:'|' }}"{% endif %}
|
||||
{% if widget_script_srcs %}data-gia-script-srcs="{{ widget_script_srcs|join:'|' }}"{% endif %}>
|
||||
<div id="widget-{{ unique }}" class="grid-stack-item" {% block widget_options %}{% if widget_options is None %}gs-w="6" gs-h="1" gs-y="20" gs-x="0"{% else %}{% autoescape off %}{{ widget_options }}{% endautoescape %}{% endif %}{% endblock %}>
|
||||
<div class="grid-stack-item-content">
|
||||
<nav class="panel">
|
||||
<p class="panel-heading" style="padding: .2em; line-height: .5em;">
|
||||
{% block close_button %}
|
||||
{% include "mixins/partials/close-widget.html" %}
|
||||
{% endblock %}
|
||||
<span class="icon is-small mr-1">
|
||||
<i class="{{ widget_icon|default:'fa-solid fa-window-maximize' }}"></i>
|
||||
<nav class="panel gia-widget-panel">
|
||||
<p class="panel-heading gia-widget-heading">
|
||||
<span class="gia-widget-heading-main">
|
||||
<span class="icon is-small gia-widget-heading-icon">
|
||||
<i class="{{ widget_icon|default:'fa-solid fa-window-maximize' }}"></i>
|
||||
</span>
|
||||
<span class="gia-widget-title">
|
||||
{% block heading %}
|
||||
{{ title }}
|
||||
{% endblock %}
|
||||
</span>
|
||||
</span>
|
||||
<span class="buttons are-small has-addons gia-widget-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="button is-light js-gia-widget-action"
|
||||
data-gia-action="tile"
|
||||
data-gia-widget-id="widget-{{ unique }}"
|
||||
aria-label="Tile window">
|
||||
<span class="icon is-small"><i class="fa-solid fa-border-all"></i></span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button is-light js-gia-widget-action"
|
||||
data-gia-action="snap-left"
|
||||
data-gia-widget-id="widget-{{ unique }}"
|
||||
aria-label="Snap window left">
|
||||
<span class="icon is-small"><i class="fa-solid fa-arrow-left"></i></span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button is-light js-gia-widget-action"
|
||||
data-gia-action="snap-right"
|
||||
data-gia-widget-id="widget-{{ unique }}"
|
||||
aria-label="Snap window right">
|
||||
<span class="icon is-small"><i class="fa-solid fa-arrow-right"></i></span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button is-light js-gia-widget-action"
|
||||
data-gia-action="minimize"
|
||||
data-gia-widget-id="widget-{{ unique }}"
|
||||
aria-label="Minimize window">
|
||||
<span class="icon is-small"><i class="fa-solid fa-window-minimize"></i></span>
|
||||
</button>
|
||||
{% block close_button %}
|
||||
{% include "mixins/partials/close-widget.html" %}
|
||||
{% endblock %}
|
||||
</span>
|
||||
<i
|
||||
class="fa-solid fa-arrows-minimize has-text-grey-light float-right"
|
||||
onclick="grid.compact();"></i>
|
||||
{% block heading %}
|
||||
{{ title }}
|
||||
{% endblock %}
|
||||
</p>
|
||||
<article class="panel-block is-active">
|
||||
<div class="control">
|
||||
<article class="panel-block is-active gia-widget-body">
|
||||
<div class="control gia-widget-control{% if widget_control_class %} {{ widget_control_class }}{% endif %}">
|
||||
{% block panel_content %}
|
||||
{% include window_content %}
|
||||
{% endblock %}
|
||||
@@ -27,12 +66,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
{% block custom_script %}
|
||||
{% endblock %}
|
||||
var widget_event = new Event("load-widget");
|
||||
document.dispatchEvent(widget_event);
|
||||
</script>
|
||||
{% block custom_end %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,116 +1,228 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<div>
|
||||
<h1 class="title is-4">Traces</h1>
|
||||
<p class="subtitle is-6">Tracked model calls and usage metrics for this account.</p>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<div>
|
||||
<h1 class="title is-4">Traces</h1>
|
||||
<p class="subtitle is-6">Tracked model calls and usage metrics for this account.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
{% if stats.total_runs %}
|
||||
<span class="tag is-success is-light">Tracking Active</span>
|
||||
{% else %}
|
||||
<span class="tag is-warning is-light">No Runs Yet</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
{% if stats.total_runs %}
|
||||
<span class="tag is-success is-light">Tracking Active</span>
|
||||
{% else %}
|
||||
<span class="tag is-warning is-light">No Runs Yet</span>
|
||||
{% endif %}
|
||||
|
||||
<article class="notification is-light">
|
||||
<p class="is-size-7 has-text-grey-dark">Execution health at a glance</p>
|
||||
<div class="tags mt-2">
|
||||
<span class="tag is-light">Total {{ stats.total_runs }}</span>
|
||||
<span class="tag is-success is-light">OK {{ stats.total_ok }}</span>
|
||||
<span class="tag is-danger is-light">Failed {{ stats.total_failed }}</span>
|
||||
<span class="tag is-info is-light">24h {{ stats.last_24h_runs }}</span>
|
||||
<span class="tag is-warning is-light">24h Failed {{ stats.last_24h_failed }}</span>
|
||||
<span class="tag is-link is-light">7d {{ stats.last_7d_runs }}</span>
|
||||
</div>
|
||||
<p class="is-size-7 has-text-grey-dark mt-3">Success Rate</p>
|
||||
<progress class="progress is-link is-small" value="{{ stats.success_rate }}" max="100">{{ stats.success_rate }}%</progress>
|
||||
</article>
|
||||
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<article class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title is-size-6">Reliability</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<table class="table is-fullwidth is-narrow is-size-7">
|
||||
<tbody>
|
||||
<tr><th>Total Runs</th><td>{{ stats.total_runs }}</td></tr>
|
||||
<tr><th>OK</th><td class="has-text-success">{{ stats.total_ok }}</td></tr>
|
||||
<tr><th>Failed</th><td class="has-text-danger">{{ stats.total_failed }}</td></tr>
|
||||
<tr><th>Success Rate</th><td>{{ stats.success_rate }}%</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<article class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title is-size-6">Throughput</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<table class="table is-fullwidth is-narrow is-size-7">
|
||||
<tbody>
|
||||
<tr><th>Runs (24h)</th><td>{{ stats.last_24h_runs }}</td></tr>
|
||||
<tr><th>Failed (24h)</th><td>{{ stats.last_24h_failed }}</td></tr>
|
||||
<tr><th>Runs (7d)</th><td>{{ stats.last_7d_runs }}</td></tr>
|
||||
<tr><th>Avg Duration</th><td>{{ stats.avg_duration_ms }}ms</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<article class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title is-size-6">Token Proxy (Chars)</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<table class="table is-fullwidth is-narrow is-size-7">
|
||||
<tbody>
|
||||
<tr><th>Total Prompt</th><td>{{ stats.total_prompt_chars }}</td></tr>
|
||||
<tr><th>Total Response</th><td>{{ stats.total_response_chars }}</td></tr>
|
||||
<tr><th>Avg Prompt</th><td>{{ stats.avg_prompt_chars }}</td></tr>
|
||||
<tr><th>Avg Response</th><td>{{ stats.avg_response_chars }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<article class="notification is-light">
|
||||
<p class="is-size-7 has-text-grey-dark">Execution health at a glance</p>
|
||||
<div class="tags mt-2">
|
||||
<span class="tag is-light">Total {{ stats.total_runs }}</span>
|
||||
<span class="tag is-success is-light">OK {{ stats.total_ok }}</span>
|
||||
<span class="tag is-danger is-light">Failed {{ stats.total_failed }}</span>
|
||||
<span class="tag is-info is-light">24h {{ stats.last_24h_runs }}</span>
|
||||
<span class="tag is-warning is-light">24h Failed {{ stats.last_24h_failed }}</span>
|
||||
<span class="tag is-link is-light">7d {{ stats.last_7d_runs }}</span>
|
||||
</div>
|
||||
<p class="is-size-7 has-text-grey-dark mt-3">Success Rate</p>
|
||||
<progress class="progress is-link is-small" value="{{ stats.success_rate }}" max="100">{{ stats.success_rate }}%</progress>
|
||||
</article>
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<article class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title is-size-6">By Operation</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="table-container">
|
||||
<table class="table is-fullwidth is-size-7 is-striped is-hoverable">
|
||||
<thead>
|
||||
<tr><th>Operation</th><th>Total</th><th>OK</th><th>Failed</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in operation_breakdown %}
|
||||
<tr>
|
||||
<td>{{ row.operation|default:"(none)" }}</td>
|
||||
<td>{{ row.total }}</td>
|
||||
<td class="has-text-success">{{ row.ok }}</td>
|
||||
<td class="has-text-danger">{{ row.failed }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="4">No runs yet.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<article class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title is-size-6">By Model</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="table-container">
|
||||
<table class="table is-fullwidth is-size-7 is-striped is-hoverable">
|
||||
<thead>
|
||||
<tr><th>Model</th><th>Total</th><th>OK</th><th>Failed</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in model_breakdown %}
|
||||
<tr>
|
||||
<td>{{ row.model|default:"(none)" }}</td>
|
||||
<td>{{ row.total }}</td>
|
||||
<td class="has-text-success">{{ row.ok }}</td>
|
||||
<td class="has-text-danger">{{ row.failed }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="4">No runs yet.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<article class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title is-size-6">Reliability</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<table class="table is-fullwidth is-narrow is-size-7">
|
||||
<tbody>
|
||||
<tr><th>Total Runs</th><td>{{ stats.total_runs }}</td></tr>
|
||||
<tr><th>OK</th><td class="has-text-success">{{ stats.total_ok }}</td></tr>
|
||||
<tr><th>Failed</th><td class="has-text-danger">{{ stats.total_failed }}</td></tr>
|
||||
<tr><th>Success Rate</th><td>{{ stats.success_rate }}%</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<article class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title is-size-6">Throughput</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<table class="table is-fullwidth is-narrow is-size-7">
|
||||
<tbody>
|
||||
<tr><th>Runs (24h)</th><td>{{ stats.last_24h_runs }}</td></tr>
|
||||
<tr><th>Failed (24h)</th><td>{{ stats.last_24h_failed }}</td></tr>
|
||||
<tr><th>Runs (7d)</th><td>{{ stats.last_7d_runs }}</td></tr>
|
||||
<tr><th>Avg Duration</th><td>{{ stats.avg_duration_ms }}ms</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="column is-12-tablet is-4-desktop">
|
||||
<article class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title is-size-6">Token Proxy (Chars)</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<table class="table is-fullwidth is-narrow is-size-7">
|
||||
<tbody>
|
||||
<tr><th>Total Prompt</th><td>{{ stats.total_prompt_chars }}</td></tr>
|
||||
<tr><th>Total Response</th><td>{{ stats.total_response_chars }}</td></tr>
|
||||
<tr><th>Avg Prompt</th><td>{{ stats.avg_prompt_chars }}</td></tr>
|
||||
<tr><th>Avg Response</th><td>{{ stats.avg_response_chars }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<article class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title is-size-6">By Operation</p>
|
||||
<p class="card-header-title is-size-6">Recent Runs</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="table-container">
|
||||
<table class="table is-fullwidth is-size-7 is-striped is-hoverable">
|
||||
<thead>
|
||||
<tr><th>Operation</th><th>Total</th><th>OK</th><th>Failed</th></tr>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Started</th>
|
||||
<th>Status</th>
|
||||
<th>Operation</th>
|
||||
<th>Model</th>
|
||||
<th>Messages</th>
|
||||
<th>Prompt</th>
|
||||
<th>Response</th>
|
||||
<th>Duration</th>
|
||||
<th>Error</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in operation_breakdown %}
|
||||
{% for run in runs %}
|
||||
<tr>
|
||||
<td>{{ row.operation|default:"(none)" }}</td>
|
||||
<td>{{ row.total }}</td>
|
||||
<td class="has-text-success">{{ row.ok }}</td>
|
||||
<td class="has-text-danger">{{ row.failed }}</td>
|
||||
<td>
|
||||
<button
|
||||
class="button is-small is-light trace-run-expand"
|
||||
type="button"
|
||||
data-detail-row="trace-run-detail-{{ run.id }}"
|
||||
data-detail-content="trace-run-detail-content-{{ run.id }}"
|
||||
data-expanded-label="Hide"
|
||||
data-collapsed-label="Show"
|
||||
hx-get="{% url 'ai_execution_run_detail' run_id=run.id %}"
|
||||
hx-target="#trace-run-detail-content-{{ run.id }}"
|
||||
hx-swap="innerHTML"
|
||||
hx-trigger="click once"
|
||||
>
|
||||
Show
|
||||
</button>
|
||||
</td>
|
||||
<td>{{ run.started_at }}</td>
|
||||
<td>
|
||||
{% if run.status == "ok" %}
|
||||
<span class="tag is-success is-light">ok</span>
|
||||
{% elif run.status == "failed" %}
|
||||
<span class="tag is-danger is-light">failed</span>
|
||||
{% else %}
|
||||
<span class="tag is-light">{{ run.status }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ run.operation|default:"-" }}</td>
|
||||
<td>{{ run.model|default:"-" }}</td>
|
||||
<td>{{ run.message_count }}</td>
|
||||
<td>{{ run.prompt_chars }}</td>
|
||||
<td>{{ run.response_chars }}</td>
|
||||
<td>{% if run.duration_ms %}{{ run.duration_ms }}ms{% else %}-{% endif %}</td>
|
||||
<td>
|
||||
{% if run.error %}
|
||||
<span title="{{ run.error }}">{{ run.error|truncatechars:120 }}</span>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="trace-run-detail-{{ run.id }}" class="is-hidden">
|
||||
<td colspan="10">
|
||||
<div id="trace-run-detail-content-{{ run.id }}" class="trace-run-detail-shell is-size-7 has-text-grey">
|
||||
Click Show to load run details.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="4">No runs yet.</td></tr>
|
||||
<tr><td colspan="10">No runs yet.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -118,115 +230,7 @@
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<article class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title is-size-6">By Model</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="table-container">
|
||||
<table class="table is-fullwidth is-size-7 is-striped is-hoverable">
|
||||
<thead>
|
||||
<tr><th>Model</th><th>Total</th><th>OK</th><th>Failed</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in model_breakdown %}
|
||||
<tr>
|
||||
<td>{{ row.model|default:"(none)" }}</td>
|
||||
<td>{{ row.total }}</td>
|
||||
<td class="has-text-success">{{ row.ok }}</td>
|
||||
<td class="has-text-danger">{{ row.failed }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="4">No runs yet.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<article class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title is-size-6">Recent Runs</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="table-container">
|
||||
<table class="table is-fullwidth is-size-7 is-striped is-hoverable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Started</th>
|
||||
<th>Status</th>
|
||||
<th>Operation</th>
|
||||
<th>Model</th>
|
||||
<th>Messages</th>
|
||||
<th>Prompt</th>
|
||||
<th>Response</th>
|
||||
<th>Duration</th>
|
||||
<th>Error</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for run in runs %}
|
||||
<tr>
|
||||
<td>
|
||||
<button
|
||||
class="button is-small is-light trace-run-expand"
|
||||
type="button"
|
||||
data-detail-row="trace-run-detail-{{ run.id }}"
|
||||
data-detail-content="trace-run-detail-content-{{ run.id }}"
|
||||
data-expanded-label="Hide"
|
||||
data-collapsed-label="Show"
|
||||
hx-get="{% url 'ai_execution_run_detail' run_id=run.id %}"
|
||||
hx-target="#trace-run-detail-content-{{ run.id }}"
|
||||
hx-swap="innerHTML"
|
||||
hx-trigger="click once"
|
||||
>
|
||||
Show
|
||||
</button>
|
||||
</td>
|
||||
<td>{{ run.started_at }}</td>
|
||||
<td>
|
||||
{% if run.status == "ok" %}
|
||||
<span class="tag is-success is-light">ok</span>
|
||||
{% elif run.status == "failed" %}
|
||||
<span class="tag is-danger is-light">failed</span>
|
||||
{% else %}
|
||||
<span class="tag is-light">{{ run.status }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ run.operation|default:"-" }}</td>
|
||||
<td>{{ run.model|default:"-" }}</td>
|
||||
<td>{{ run.message_count }}</td>
|
||||
<td>{{ run.prompt_chars }}</td>
|
||||
<td>{{ run.response_chars }}</td>
|
||||
<td>{% if run.duration_ms %}{{ run.duration_ms }}ms{% else %}-{% endif %}</td>
|
||||
<td>
|
||||
{% if run.error %}
|
||||
<span title="{{ run.error }}">{{ run.error|truncatechars:120 }}</span>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="trace-run-detail-{{ run.id }}" class="is-hidden">
|
||||
<td colspan="10">
|
||||
<div id="trace-run-detail-content-{{ run.id }}" class="trace-run-detail-shell is-size-7 has-text-grey">
|
||||
Click Show to load run details.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="10">No runs yet.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
<script>
|
||||
(function () {
|
||||
document.querySelectorAll(".trace-run-expand").forEach(function (button) {
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
{% extends "index.html" %}
|
||||
|
||||
{% block load_widgets %}
|
||||
<div
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'ai_workspace_contacts' type='widget' %}"
|
||||
hx-target="#widgets-here"
|
||||
hx-trigger="load"
|
||||
hx-swap="afterend"
|
||||
style="display: none;"></div>
|
||||
{% url 'ai_workspace_contacts' type='widget' as contacts_widget_url %}
|
||||
{% include "partials/workspace-widget-loader.html" with widget_url=contacts_widget_url %}
|
||||
{% if selected_person_id %}
|
||||
<div
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'ai_workspace_person' type='widget' person_id=selected_person_id %}"
|
||||
hx-target="#widgets-here"
|
||||
hx-trigger="load delay:250ms"
|
||||
hx-swap="afterend"
|
||||
style="display: none;"></div>
|
||||
{% url 'ai_workspace_person' type='widget' person_id=selected_person_id as person_widget_url %}
|
||||
{% include "partials/workspace-widget-loader.html" with widget_url=person_widget_url trigger_delay="250ms" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
<div class="container gia-page-shell">
|
||||
<div class="gia-page-header">
|
||||
<div>
|
||||
<h1 class="title is-4">Codex Status</h1>
|
||||
<p class="subtitle is-6">Worker-backed task sync status, runs, and approvals for the canonical GIA task store.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<article class="box">
|
||||
<div class="codex-inline-stats">
|
||||
<span><strong>Provider</strong> codex_cli</span>
|
||||
<span><strong>Health</strong> <span class="{% if health and health.ok %}has-text-success{% else %}has-text-danger{% endif %}">{% if health and health.ok %}online{% else %}offline{% endif %}</span></span>
|
||||
<span><strong>Pending</strong> {{ queue_counts.pending }}</span>
|
||||
<span><strong>Waiting Approval</strong> {{ queue_counts.waiting_approval }}</span>
|
||||
</div>
|
||||
{% if health and health.error %}
|
||||
<p class="help">Healthcheck error: <code>{{ health.error }}</code></p>
|
||||
{% endif %}
|
||||
<p class="help">Config snapshot: command=<code>{{ provider_settings.command }}</code>, workspace=<code>{{ provider_settings.workspace_root|default:"-" }}</code>, profile=<code>{{ provider_settings.default_profile|default:"-" }}</code>, instance=<code>{{ provider_settings.instance_label }}</code>, approver=<code>{{ provider_settings.approver_service }} {{ provider_settings.approver_identifier }}</code>.</p>
|
||||
<p class="help"><a href="{% url 'tasks_settings' %}">Edit in Task Automation</a>.</p>
|
||||
</article>
|
||||
|
||||
<article class="box">
|
||||
<h2 class="title is-6">Run Filters</h2>
|
||||
<form method="get">
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-2">
|
||||
<label class="label is-size-7">Status</label>
|
||||
<input class="input is-small" name="status" value="{{ filters.status }}" placeholder="ok/failed/...">
|
||||
</div>
|
||||
<div class="column is-2">
|
||||
<label class="label is-size-7">Service</label>
|
||||
<input class="input is-small" name="service" value="{{ filters.service }}" placeholder="signal">
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<label class="label is-size-7">Channel</label>
|
||||
<input class="input is-small" name="channel" value="{{ filters.channel }}" placeholder="identifier">
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<label class="label is-size-7">Project</label>
|
||||
<div class="select is-small is-fullwidth">
|
||||
<select name="project">
|
||||
<option value="">All</option>
|
||||
{% for row in projects %}
|
||||
<option value="{{ row.id }}" {% if filters.project == row.id|stringformat:"s" %}selected{% endif %}>{{ row.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-2">
|
||||
<label class="label is-size-7">Date From</label>
|
||||
<input class="input is-small" type="date" name="date_from" value="{{ filters.date_from }}">
|
||||
</div>
|
||||
</div>
|
||||
<button class="button is-small is-link is-light" type="submit">Apply</button>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<article class="box">
|
||||
<h2 class="title is-6">Runs</h2>
|
||||
<table class="table is-fullwidth is-size-7 is-striped">
|
||||
<thead><tr><th>When</th><th>Status</th><th>Service/Channel</th><th>Project</th><th>Task</th><th>Summary</th><th>Files</th><th>Links</th></tr></thead>
|
||||
<tbody>
|
||||
{% for run in runs %}
|
||||
<tr>
|
||||
<td>{{ run.created_at }}</td>
|
||||
<td>{{ run.status }}</td>
|
||||
<td>{{ run.source_service }} · <code>{{ run.source_channel }}</code></td>
|
||||
<td>{{ run.project.name|default:"-" }}</td>
|
||||
<td>{% if run.task %}<a href="{% url 'tasks_task' task_id=run.task.id %}">#{{ run.task.reference_code }}</a>{% else %}-{% endif %}</td>
|
||||
<td>{{ run.result_payload.summary|default:"-" }}</td>
|
||||
<td>{{ run.result_payload.files_modified_count|default:"0" }}</td>
|
||||
<td>
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
<p><strong>Request</strong></p>
|
||||
<pre>{{ run.request_payload }}</pre>
|
||||
<p><strong>Result</strong></p>
|
||||
<pre>{{ run.result_payload }}</pre>
|
||||
<p><strong>Error</strong> {{ run.error|default:"-" }}</p>
|
||||
</details>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="8">No runs.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
|
||||
<article class="box">
|
||||
<h2 class="title is-6">Approvals Queue</h2>
|
||||
<table class="table is-fullwidth is-size-7 is-striped">
|
||||
<thead><tr><th>Requested</th><th>Approval Key</th><th>Status</th><th>Summary</th><th>Permissions</th><th>Run</th><th>Task</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
{% for row in permission_requests %}
|
||||
<tr>
|
||||
<td>{{ row.requested_at }}</td>
|
||||
<td><code>{{ row.approval_key }}</code></td>
|
||||
<td>{{ row.status }}</td>
|
||||
<td>{{ row.summary|default:"-" }}</td>
|
||||
<td><pre>{{ row.requested_permissions }}</pre></td>
|
||||
<td><code>{{ row.codex_run_id }}</code></td>
|
||||
<td>{% if row.codex_run.task %}<a href="{% url 'tasks_task' task_id=row.codex_run.task.id %}">#{{ row.codex_run.task.reference_code }}</a>{% else %}-{% endif %}</td>
|
||||
<td>
|
||||
{% if row.status == 'pending' %}
|
||||
<form method="post" action="{% url 'codex_approval' %}" style="display:inline;">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="request_id" value="{{ row.id }}">
|
||||
<input type="hidden" name="decision" value="approve">
|
||||
<button class="button is-small is-success is-light" type="submit">Approve</button>
|
||||
</form>
|
||||
<form method="post" action="{% url 'codex_approval' %}" style="display:inline;">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="request_id" value="{{ row.id }}">
|
||||
<input type="hidden" name="decision" value="deny">
|
||||
<button class="button is-small is-danger is-light" type="submit">Deny</button>
|
||||
</form>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="8">No permission requests.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
<style>
|
||||
.codex-inline-stats {
|
||||
display: flex;
|
||||
gap: 0.95rem;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0.92rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.codex-inline-stats span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -463,16 +463,8 @@
|
||||
return;
|
||||
}
|
||||
const applyDefaults = function () {
|
||||
const slug = String(commandSelect.value || "").trim().toLowerCase();
|
||||
if (slug === "codex") {
|
||||
triggerInput.value = ".codex";
|
||||
if (!nameInput.value || nameInput.value === "Business Plan") {
|
||||
nameInput.value = "Codex";
|
||||
}
|
||||
return;
|
||||
}
|
||||
triggerInput.value = ".bp";
|
||||
if (!nameInput.value || nameInput.value === "Codex") {
|
||||
if (!nameInput.value) {
|
||||
nameInput.value = "Business Plan";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
{% extends "index.html" %}
|
||||
|
||||
{% block load_widgets %}
|
||||
<div
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{{ contacts_widget_url }}"
|
||||
hx-target="#widgets-here"
|
||||
hx-trigger="load"
|
||||
hx-swap="afterend"
|
||||
style="display: none;"></div>
|
||||
{% include "partials/workspace-widget-loader.html" with widget_url=contacts_widget_url %}
|
||||
{% include "partials/workspace-widget-loader.html" with widget_url=history_widget_url trigger_delay="125ms" %}
|
||||
{% if initial_widget_url %}
|
||||
<div
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{{ initial_widget_url }}"
|
||||
hx-target="#widgets-here"
|
||||
hx-trigger="load delay:250ms"
|
||||
hx-swap="afterend"
|
||||
style="display: none;"></div>
|
||||
{% include "partials/workspace-widget-loader.html" with widget_url=initial_widget_url trigger_delay="250ms" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block extra_head_assets %}
|
||||
{% include "partials/compose-panel-assets.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="section pt-5 pb-0">
|
||||
<div class="container">
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
{% extends "index.html" %}
|
||||
|
||||
{% block load_widgets %}
|
||||
<div
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{{ tabs_widget_url }}"
|
||||
hx-target="#widgets-here"
|
||||
hx-trigger="load"
|
||||
hx-swap="afterend"
|
||||
style="display: none;"></div>
|
||||
{% include "partials/workspace-widget-loader.html" with widget_url=tabs_widget_url %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
{% extends "index.html" %}
|
||||
|
||||
{% block load_widgets %}
|
||||
<div
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url accounts_url_name type='widget' %}"
|
||||
hx-target="#widgets-here"
|
||||
hx-trigger="load"
|
||||
hx-swap="afterend"
|
||||
style="display: none;"></div>
|
||||
{% url accounts_url_name type='widget' as accounts_widget_url %}
|
||||
{% include "partials/workspace-widget-loader.html" with widget_url=accounts_widget_url %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -15,12 +15,6 @@
|
||||
</p>
|
||||
<div class="buttons">
|
||||
<a class="button is-small is-light" href="{% url 'tasks_hub' %}">Back</a>
|
||||
<form method="post" action="{% url 'tasks_codex_submit' %}" style="display:inline;">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="task_id" value="{{ task.id }}">
|
||||
<input type="hidden" name="next" value="{% url 'tasks_task' task_id=task.id %}">
|
||||
<button class="button is-small is-link is-light" type="submit">Send to Codex</button>
|
||||
</form>
|
||||
</div>
|
||||
<article class="box">
|
||||
<h2 class="title is-6">Events</h2>
|
||||
@@ -57,57 +51,6 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
<article class="box">
|
||||
<h2 class="title is-6">External Sync</h2>
|
||||
<table class="table is-fullwidth is-size-7">
|
||||
<thead><tr><th>When</th><th>Provider</th><th>Status</th><th>Error</th></tr></thead>
|
||||
<tbody>
|
||||
{% for row in sync_events %}
|
||||
<tr><td>{{ row.updated_at }}</td><td>{{ row.provider }}</td><td>{{ row.status }}</td><td>{{ row.error }}</td></tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="4">No sync events.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
<article class="box">
|
||||
<h2 class="title is-6">Codex Runs</h2>
|
||||
<table class="table is-fullwidth is-size-7">
|
||||
<thead><tr><th>When</th><th>Status</th><th>Summary</th><th>Files</th><th>Error</th></tr></thead>
|
||||
<tbody>
|
||||
{% for row in codex_runs %}
|
||||
<tr>
|
||||
<td>{{ row.updated_at }}</td>
|
||||
<td>{{ row.status }}</td>
|
||||
<td>{{ row.result_payload.summary|default:"-" }}</td>
|
||||
<td>{{ row.result_payload.files_modified_count|default:"0" }}</td>
|
||||
<td>{{ row.error|default:"" }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="5">No Codex runs.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
<article class="box">
|
||||
<h2 class="title is-6">Permission Requests</h2>
|
||||
<table class="table is-fullwidth is-size-7">
|
||||
<thead><tr><th>When</th><th>Approval Key</th><th>Status</th><th>Summary</th><th>Resolved</th></tr></thead>
|
||||
<tbody>
|
||||
{% for row in permission_requests %}
|
||||
<tr>
|
||||
<td>{{ row.requested_at }}</td>
|
||||
<td><code>{{ row.approval_key }}</code></td>
|
||||
<td>{{ row.status }}</td>
|
||||
<td>{{ row.summary|default:"-" }}</td>
|
||||
<td>{{ row.resolved_at|default:"-" }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="5">No permission requests.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
</div></section>
|
||||
<style>
|
||||
.task-event-payload {
|
||||
|
||||
@@ -216,37 +216,6 @@
|
||||
<td>{{ row.status_snapshot }}</td>
|
||||
<td>
|
||||
<a class="button is-small is-light" href="{% url 'tasks_task' task_id=row.id %}">Open</a>
|
||||
{% if enabled_providers|length == 1 %}
|
||||
<form method="post" action="{% url 'tasks_codex_submit' %}" style="display:inline;">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="task_id" value="{{ row.id }}">
|
||||
<input type="hidden" name="next" value="{% url 'tasks_hub' %}">
|
||||
<input type="hidden" name="provider" value="{{ enabled_providers.0 }}">
|
||||
<button class="button is-small is-link is-light" type="submit">
|
||||
Send to {% if enabled_providers.0 == "claude_cli" %}Claude{% else %}Codex{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
{% elif enabled_providers %}
|
||||
<form method="post" action="{% url 'tasks_codex_submit' %}" style="display:inline;">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="task_id" value="{{ row.id }}">
|
||||
<input type="hidden" name="next" value="{% url 'tasks_hub' %}">
|
||||
<div class="field has-addons" style="display:inline-flex;">
|
||||
<div class="control">
|
||||
<div class="select is-small">
|
||||
<select name="provider">
|
||||
{% for p in enabled_providers %}
|
||||
<option value="{{ p }}">{% if p == "claude_cli" %}Claude{% else %}Codex{% endif %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-small is-link is-light" type="submit">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
|
||||
@@ -322,7 +322,7 @@
|
||||
<div class="column is-6">
|
||||
<section class="tasks-panel">
|
||||
<h3 class="title is-7">Providers</h3>
|
||||
<p class="help">Controls outbound sync to external tracking systems. If disabled, tasks are still derived and visible inside GIA only.</p>
|
||||
<p class="help">Controls outbound sync from canonical GIA tasks. If disabled, tasks still work inside GIA but no sync event is emitted.</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="provider_update">
|
||||
@@ -333,250 +333,9 @@
|
||||
<button class="button is-small is-link is-light" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
<hr>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="provider_update">
|
||||
<input type="hidden" name="provider" value="codex_cli">
|
||||
<label class="checkbox"><input type="checkbox" name="enabled" value="1" {% if codex_provider_config and codex_provider_config.enabled %}checked{% endif %}> Enable Codex CLI provider</label>
|
||||
<p class="help">Codex task-sync runs in a dedicated worker (<code>python manage.py codex_worker</code>).</p>
|
||||
<p class="help">This provider config is global per-user and shared across all projects/chats. This phase is task-sync only (no full transcript mirroring by default).</p>
|
||||
<div class="field" style="margin-top:0.5rem;">
|
||||
<label class="label is-size-7">Command</label>
|
||||
<input class="input is-small" name="command" value="{{ codex_provider_settings.command }}" placeholder="codex">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label is-size-7">Workspace Root</label>
|
||||
<input class="input is-small" name="workspace_root" value="{{ codex_provider_settings.workspace_root }}" placeholder="/code/xf">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label is-size-7">Default Profile</label>
|
||||
<input class="input is-small" name="default_profile" value="{{ codex_provider_settings.default_profile }}" placeholder="default">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label is-size-7">Timeout Seconds</label>
|
||||
<input class="input is-small" type="number" min="1" name="timeout_seconds" value="{{ codex_provider_settings.timeout_seconds }}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label is-size-7">Instance Label</label>
|
||||
<input class="input is-small" name="instance_label" value="{{ codex_provider_settings.instance_label }}" placeholder="default">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label is-size-7">Approver Service</label>
|
||||
<input class="input is-small" name="approver_service" value="{{ codex_provider_settings.approver_service }}" placeholder="signal">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label is-size-7">Approver Identifier</label>
|
||||
<input class="input is-small" name="approver_identifier" value="{{ codex_provider_settings.approver_identifier }}" placeholder="+15550000001">
|
||||
</div>
|
||||
<div style="margin-top:0.5rem;">
|
||||
<button class="button is-small is-link is-light" type="submit">Save Codex Provider</button>
|
||||
<a class="button is-small is-light" href="{% url 'codex_settings' %}">Open Codex Status</a>
|
||||
</div>
|
||||
</form>
|
||||
<hr>
|
||||
<article class="box" style="margin-top:0.5rem;">
|
||||
<h4 class="title is-7">Codex Compact Summary</h4>
|
||||
<p class="help">
|
||||
Health:
|
||||
{% if codex_compact_summary.healthcheck_ok %}
|
||||
<span class="tag is-success is-light">online</span>
|
||||
{% else %}
|
||||
<span class="tag is-danger is-light">offline</span>
|
||||
{% endif %}
|
||||
{% if codex_compact_summary.healthcheck_error %}
|
||||
<code>{{ codex_compact_summary.healthcheck_error }}</code>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="help">
|
||||
Worker heartbeat:
|
||||
{% if codex_compact_summary.worker_heartbeat_at %}
|
||||
{{ codex_compact_summary.worker_heartbeat_at }} ({{ codex_compact_summary.worker_heartbeat_age }})
|
||||
{% else %}
|
||||
no worker activity yet
|
||||
{% endif %}
|
||||
</p>
|
||||
<div class="tags">
|
||||
<span class="tag is-light">pending {{ codex_compact_summary.queue_counts.pending }}</span>
|
||||
<span class="tag is-warning is-light">waiting_approval {{ codex_compact_summary.queue_counts.waiting_approval }}</span>
|
||||
<span class="tag is-danger is-light">failed {{ codex_compact_summary.queue_counts.failed }}</span>
|
||||
<span class="tag is-success is-light">ok {{ codex_compact_summary.queue_counts.ok }}</span>
|
||||
</div>
|
||||
<table class="table is-fullwidth is-size-7 is-striped" style="margin-top:0.5rem;">
|
||||
<thead><tr><th>When</th><th>Status</th><th>Task</th><th>Summary</th></tr></thead>
|
||||
<tbody>
|
||||
{% for run in codex_compact_summary.recent_runs %}
|
||||
<tr>
|
||||
<td>{{ run.created_at }}</td>
|
||||
<td>{{ run.status }}</td>
|
||||
<td>{% if run.task %}<a href="{% url 'tasks_task' task_id=run.task.id %}">#{{ run.task.reference_code }}</a>{% else %}-{% endif %}</td>
|
||||
<td>{{ run.result_payload.summary|default:"-" }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="4">No runs yet.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
<hr>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="provider_update">
|
||||
<input type="hidden" name="provider" value="claude_cli">
|
||||
<label class="checkbox"><input type="checkbox" name="enabled" value="1" {% if claude_provider_config and claude_provider_config.enabled %}checked{% endif %}> Enable Claude CLI provider</label>
|
||||
<p class="help">Claude task-sync runs in the same dedicated worker (<code>python manage.py codex_worker</code>).</p>
|
||||
<p class="help">This provider config is global per-user and shared across all projects/chats.</p>
|
||||
<div class="field" style="margin-top:0.5rem;">
|
||||
<label class="label is-size-7">Command</label>
|
||||
<input class="input is-small" name="command" value="{{ claude_provider_settings.command }}" placeholder="claude">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label is-size-7">Workspace Root</label>
|
||||
<input class="input is-small" name="workspace_root" value="{{ claude_provider_settings.workspace_root }}" placeholder="/code/xf">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label is-size-7">Default Profile</label>
|
||||
<input class="input is-small" name="default_profile" value="{{ claude_provider_settings.default_profile }}" placeholder="default">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label is-size-7">Timeout Seconds</label>
|
||||
<input class="input is-small" type="number" min="1" name="timeout_seconds" value="{{ claude_provider_settings.timeout_seconds }}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label is-size-7">Approver Service</label>
|
||||
<input class="input is-small" name="approver_service" value="{{ claude_provider_settings.approver_service }}" placeholder="signal">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label is-size-7">Approver Identifier</label>
|
||||
<input class="input is-small" name="approver_identifier" value="{{ claude_provider_settings.approver_identifier }}" placeholder="+15550000001">
|
||||
</div>
|
||||
<div style="margin-top:0.5rem;">
|
||||
<button class="button is-small is-link is-light" type="submit">Save Claude Provider</button>
|
||||
</div>
|
||||
</form>
|
||||
<hr>
|
||||
<article class="box" style="margin-top:0.5rem;">
|
||||
<h4 class="title is-7">Claude Compact Summary</h4>
|
||||
<p class="help">
|
||||
Health:
|
||||
{% if claude_compact_summary.healthcheck_ok %}
|
||||
<span class="tag is-success is-light">online</span>
|
||||
{% else %}
|
||||
<span class="tag is-danger is-light">offline</span>
|
||||
{% endif %}
|
||||
{% if claude_compact_summary.healthcheck_error %}
|
||||
<code>{{ claude_compact_summary.healthcheck_error }}</code>
|
||||
{% endif %}
|
||||
</p>
|
||||
<div class="tags">
|
||||
<span class="tag is-light">pending {{ claude_compact_summary.queue_counts.pending }}</span>
|
||||
<span class="tag is-warning is-light">waiting_approval {{ claude_compact_summary.queue_counts.waiting_approval }}</span>
|
||||
<span class="tag is-danger is-light">failed {{ claude_compact_summary.queue_counts.failed }}</span>
|
||||
<span class="tag is-success is-light">ok {{ claude_compact_summary.queue_counts.ok }}</span>
|
||||
</div>
|
||||
</article>
|
||||
<p class="help">Browse all derived tasks in <a href="{% url 'tasks_hub' %}">Task Inbox</a>.</p>
|
||||
</section>
|
||||
</div>
|
||||
<div class="column is-12">
|
||||
<section class="tasks-panel">
|
||||
<h3 class="title is-7">External Chat Links</h3>
|
||||
<p class="help">Map one GIA contact to one Codex thread for task-sync routing.</p>
|
||||
<details class="tasks-external-help">
|
||||
<summary class="is-size-7">More info</summary>
|
||||
<p class="help">
|
||||
This is task-sync only. It does not mirror full chat history. The link tells the Codex worker which Codex conversation/session should receive updates for tasks from that contact/group.
|
||||
</p>
|
||||
</details>
|
||||
{% if external_link_scoped %}
|
||||
<article class="message is-info is-light tasks-link-scope-note">
|
||||
<div class="message-body">
|
||||
Scoped to <strong>{{ external_link_scope_label }}</strong>. Only matching identifiers are available below.
|
||||
</div>
|
||||
</article>
|
||||
{% endif %}
|
||||
<form method="post" class="block">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="external_chat_link_upsert">
|
||||
<input type="hidden" name="prefill_service" value="{{ prefill_service }}">
|
||||
<input type="hidden" name="prefill_identifier" value="{{ prefill_identifier }}">
|
||||
<div class="columns is-multiline is-variable is-2 tasks-external-link-columns">
|
||||
<div class="column is-12-mobile is-4-tablet is-2-desktop">
|
||||
<div class="field">
|
||||
<label class="label is-size-7">Provider</label>
|
||||
<div class="control">
|
||||
<div class="select is-small is-fullwidth">
|
||||
<select name="provider">
|
||||
<option value="codex_cli" selected>codex_cli</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-mobile is-8-tablet is-5-desktop">
|
||||
<div class="field">
|
||||
<label class="label is-size-7">Contact</label>
|
||||
<div class="control">
|
||||
<div class="select is-small is-fullwidth">
|
||||
<select name="person_identifier_id">
|
||||
<option value="">Unlinked</option>
|
||||
{% for row in external_link_person_identifiers %}
|
||||
<option value="{{ row.id }}">{{ row.person.name }} · {{ row.service }} · {{ row.identifier }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help">Which GIA contact/group this link belongs to.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-mobile is-8-tablet is-3-desktop">
|
||||
<div class="field">
|
||||
<label class="label is-size-7">Codex Chat ID</label>
|
||||
<div class="control">
|
||||
<input class="input is-small" name="external_chat_id" placeholder="codex-chat-...">
|
||||
</div>
|
||||
<p class="help">Stable Codex conversation/session ID.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-6-mobile is-4-tablet is-2-desktop">
|
||||
<div class="field">
|
||||
<label class="label is-size-7">Enabled</label>
|
||||
<label class="checkbox"><input type="checkbox" name="enabled" value="1" checked> Active</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<button class="button is-small is-link is-light" type="submit">Save Link</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<table class="table is-fullwidth is-striped is-size-7">
|
||||
<thead><tr><th>Provider</th><th>Person</th><th>Identifier</th><th>External Chat</th><th>Enabled</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for row in external_chat_links %}
|
||||
<tr>
|
||||
<td>{{ row.provider }}</td>
|
||||
<td>{% if row.person %}{{ row.person.name }}{% else %}-{% endif %}</td>
|
||||
<td>{% if row.person_identifier %}{{ row.person_identifier.service }} · {{ row.person_identifier.identifier }}{% else %}-{% endif %}</td>
|
||||
<td>{{ row.external_chat_id }}</td>
|
||||
<td>{{ row.enabled }}</td>
|
||||
<td>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="external_chat_link_delete">
|
||||
<input type="hidden" name="external_link_id" value="{{ row.id }}">
|
||||
<button class="button is-danger is-light is-small" type="submit">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="6">No external chat links.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
@@ -607,20 +366,6 @@
|
||||
.tasks-settings-page .tasks-settings-list {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
.tasks-settings-page .tasks-link-scope-note {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.tasks-settings-page .tasks-external-help {
|
||||
margin-bottom: 0.55rem;
|
||||
}
|
||||
.tasks-settings-page .tasks-external-help > summary {
|
||||
cursor: pointer;
|
||||
color: #4a4a4a;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
.tasks-settings-page .tasks-external-link-columns .field {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.tasks-settings-page .prefix-chip {
|
||||
margin-right: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<div
|
||||
id="ai-result-links-{{ person.id }}-{{ operation }}-{{ ai_result_id|default:'new' }}"
|
||||
style="margin-bottom: 0.5rem;">
|
||||
<div class="tags has-addons" style="display: inline-flex; margin-bottom: 0.4rem;">
|
||||
<span class="tag is-dark">
|
||||
<div class="tags has-addons gia-tag-ribbon" style="margin-bottom: 0.4rem;">
|
||||
<span class="tag is-dark gia-badge">
|
||||
<i class="fa-solid fa-wand-magic-sparkles" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="tag is-white" style="border: 1px solid rgba(0, 0, 0, 0.2);">
|
||||
<span class="tag is-white gia-badge gia-tag-ribbon-main">
|
||||
AI {{ operation_label }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -85,13 +85,28 @@
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{{ compose_widget_url }}"
|
||||
hx-target="#widgets-here"
|
||||
hx-swap="afterend"
|
||||
hx-swap="beforeend"
|
||||
title="Open Manual Text widget here"
|
||||
aria-label="Open Manual Text widget here">
|
||||
<span class="icon is-small"><i class="fa-solid fa-table-cells-large"></i></span>
|
||||
<span>Widget</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if history_widget_url %}
|
||||
<button
|
||||
type="button"
|
||||
class="button is-light is-small js-widget-spawn-trigger is-hidden"
|
||||
data-widget-url="{{ history_widget_url }}"
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{{ history_widget_url }}"
|
||||
hx-target="#widgets-here"
|
||||
hx-swap="beforeend"
|
||||
title="Open message history widget here"
|
||||
aria-label="Open message history widget here">
|
||||
<span class="icon is-small"><i class="fa-solid fa-clock-rotate-left"></i></span>
|
||||
<span>History</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -28,12 +28,12 @@
|
||||
hx-get="{% url 'ai_workspace_person' type='widget' person_id=row.person.id %}"
|
||||
hx-include="#ai-window-form"
|
||||
hx-target="#widgets-here"
|
||||
hx-swap="afterend">
|
||||
<span class="tags has-addons" style="display: inline-flex; width: 100%; margin: 0; white-space: nowrap;">
|
||||
<span class="tag is-dark" style="min-width: 2.5rem; justify-content: center;">
|
||||
hx-swap="beforeend">
|
||||
<span class="tags has-addons gia-tag-ribbon">
|
||||
<span class="tag is-dark gia-badge" style="min-width: 2.5rem; justify-content: center;">
|
||||
<i class="fa-solid fa-comment-dots" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="tag is-white" style="flex: 1; display: inline-flex; align-items: center; justify-content: space-between; gap: 0.75rem; padding-left: 0.7rem; padding-right: 0.7rem; border-top: 1px solid rgba(0, 0, 0, 0.2); border-bottom: 1px solid rgba(0, 0, 0, 0.2);">
|
||||
<span class="tag is-white gia-badge gia-tag-ribbon-main">
|
||||
<span style="display: inline-flex; align-items: baseline; gap: 0.35rem; min-width: 0;">
|
||||
<strong>{{ row.person.name }}</strong>
|
||||
</span>
|
||||
@@ -41,7 +41,7 @@
|
||||
<small style="padding-left: 0.5rem;">{{ row.last_ts_label }}</small>
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="tag is-dark" style="min-width: 3.25rem; justify-content: center;">{{ row.message_count }}</span>
|
||||
<span class="tag is-dark gia-badge" style="min-width: 3.25rem; justify-content: center;">{{ row.message_count }}</span>
|
||||
</span>
|
||||
</button>
|
||||
{% else %}
|
||||
@@ -49,11 +49,11 @@
|
||||
class="button is-fullwidth"
|
||||
style="border-radius: 8px; border: 0; background: transparent; box-shadow: none; padding: 0;"
|
||||
disabled>
|
||||
<span class="tags has-addons" style="display: inline-flex; width: 100%; margin: 0; white-space: nowrap;">
|
||||
<span class="tag is-info is-light" style="min-width: 2.5rem; justify-content: center;">
|
||||
<span class="tags has-addons gia-tag-ribbon">
|
||||
<span class="tag is-info is-light gia-badge" style="min-width: 2.5rem; justify-content: center;">
|
||||
<i class="fa-solid fa-users" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="tag is-white" style="flex: 1; display: inline-flex; align-items: center; justify-content: space-between; gap: 0.75rem; padding-left: 0.7rem; padding-right: 0.7rem; border-top: 1px solid rgba(0, 0, 0, 0.2); border-bottom: 1px solid rgba(0, 0, 0, 0.2);">
|
||||
<span class="tag is-white gia-badge gia-tag-ribbon-main">
|
||||
<span style="display: inline-flex; align-items: baseline; gap: 0.35rem; min-width: 0;">
|
||||
<strong>{{ row.chat_name }}</strong>
|
||||
<small class="has-text-grey">{{ row.service }}</small>
|
||||
|
||||
24
core/templates/partials/bulma-send-composer.html
Normal file
24
core/templates/partials/bulma-send-composer.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<div class="box is-shadowless gia-send-composer{% if composer_class %} {{ composer_class }}{% endif %}">
|
||||
<div class="field has-addons gia-send-composer-row">
|
||||
<div class="control is-expanded gia-send-composer-input-wrap">
|
||||
<textarea
|
||||
id="{{ textarea_id }}"
|
||||
class="textarea gia-send-composer-input{% if textarea_class %} {{ textarea_class }}{% endif %}"
|
||||
name="{{ textarea_name|default:'text' }}"
|
||||
rows="{{ textarea_rows|default:'1' }}"
|
||||
{% if textarea_placeholder %}placeholder="{{ textarea_placeholder }}"{% endif %}></textarea>
|
||||
</div>
|
||||
<div class="control gia-send-composer-action">
|
||||
<button
|
||||
class="button gia-send-composer-button{% if button_class %} {{ button_class }}{% endif %}"
|
||||
type="{{ button_type|default:'submit' }}"
|
||||
{% if button_disabled %}disabled{% endif %}
|
||||
{% if button_title %}title="{{ button_title }}"{% endif %}>
|
||||
{% if button_icon_class %}
|
||||
<span class="icon is-small"><i class="{{ button_icon_class }}"></i></span>
|
||||
{% endif %}
|
||||
<span>{{ button_label|default:"Send" }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
88
core/templates/partials/compose-message-row.html
Normal file
88
core/templates/partials/compose-message-row.html
Normal file
@@ -0,0 +1,88 @@
|
||||
<div class="compose-row {% if msg.outgoing %}is-out{% else %}is-in{% endif %}{% if msg.is_deleted %} is-deleted{% endif %}" data-ts="{{ msg.ts }}" data-message-id="{{ msg.id }}"{% if msg.reply_to_id %} data-reply-to-id="{{ msg.reply_to_id }}"{% endif %} data-reply-snippet="{{ msg.display_text|default:msg.text|default:''|truncatechars:120|escape }}">
|
||||
<article class="compose-bubble {% if msg.outgoing %}is-out{% else %}is-in{% endif %}">
|
||||
{% if msg.reply_to_id %}
|
||||
<div class="compose-reply-ref" data-reply-target-id="{{ msg.reply_to_id }}">
|
||||
<button type="button" class="compose-reply-link" title="Jump to referenced message">
|
||||
Reply to: {{ msg.reply_preview|default:"Referenced message"|escape }}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="compose-source-badge-wrap">
|
||||
<span class="tag is-light gia-badge compose-source-badge source-{{ msg.source_service|default:'web'|lower }}">{{ msg.source_label }}</span>
|
||||
</div>
|
||||
{% if msg.image_urls %}
|
||||
{% for image_url in msg.image_urls %}
|
||||
<figure class="compose-media">
|
||||
<img
|
||||
class="compose-image"
|
||||
src="{{ image_url }}"
|
||||
alt="Attachment"
|
||||
referrerpolicy="no-referrer"
|
||||
loading="lazy"
|
||||
decoding="async">
|
||||
</figure>
|
||||
{% endfor %}
|
||||
{% elif msg.image_url %}
|
||||
<figure class="compose-media">
|
||||
<img
|
||||
class="compose-image"
|
||||
src="{{ msg.image_url }}"
|
||||
alt="Attachment"
|
||||
referrerpolicy="no-referrer"
|
||||
loading="lazy"
|
||||
decoding="async">
|
||||
</figure>
|
||||
{% endif %}
|
||||
{% if not msg.hide_text %}
|
||||
<p class="compose-body">{{ msg.display_text|default:"(no text)" }}</p>
|
||||
{% else %}
|
||||
<p class="compose-body compose-image-fallback is-hidden">(no text)</p>
|
||||
{% endif %}
|
||||
{% if msg.edit_count %}
|
||||
<details class="compose-edit-history">
|
||||
<summary>Edited {{ msg.edit_count }} time{% if msg.edit_count != 1 %}s{% endif %}</summary>
|
||||
<ul>
|
||||
{% for edit in msg.edit_history %}
|
||||
<li>
|
||||
{% if edit.edited_display %}{{ edit.edited_display }}{% else %}Unknown time{% endif %}
|
||||
{% if edit.actor %} · {{ edit.actor }}{% endif %}
|
||||
{% if edit.source_service %} · {{ edit.source_service|upper }}{% endif %}
|
||||
<div class="compose-edit-diff">
|
||||
<span class="compose-edit-old">{{ edit.previous_text|default:"(empty)" }}</span>
|
||||
<span class="compose-edit-arrow">→</span>
|
||||
<span class="compose-edit-new">{{ edit.new_text|default:"(empty)" }}</span>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
{% endif %}
|
||||
{% if msg.reactions %}
|
||||
<div class="compose-reactions" aria-label="Message reactions">
|
||||
{% for reaction in msg.reactions %}
|
||||
<span
|
||||
class="tag is-light gia-badge compose-reaction-chip"
|
||||
data-emoji="{{ reaction.emoji|escape }}"
|
||||
data-actor="{{ reaction.actor|default:''|escape }}"
|
||||
data-source-service="{{ reaction.source_service|default:''|escape }}"
|
||||
title="{{ reaction.actor|default:'Unknown' }} via {{ reaction.source_service|default:'unknown'|upper }}">
|
||||
{{ reaction.emoji }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p class="compose-msg-meta">
|
||||
{{ msg.display_ts }}{% if msg.author %} · {{ msg.author }}{% endif %}
|
||||
{% if msg.is_edited %}
|
||||
<span class="tag is-light gia-badge compose-msg-flag is-edited" title="Message edited{% if msg.last_edit_display %} at {{ msg.last_edit_display }}{% endif %}">edited</span>
|
||||
{% endif %}
|
||||
{% if msg.is_deleted %}
|
||||
<span class="tag is-light gia-badge compose-msg-flag is-deleted" title="Deleted{% if msg.deleted_display %} at {{ msg.deleted_display }}{% endif %}{% if msg.deleted_actor %} by {{ msg.deleted_actor }}{% endif %}">deleted</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
<button type="button" class="button is-white is-small compose-reply-btn" title="Reply to this message" aria-label="Reply to this message">
|
||||
<span class="icon is-small"><i class="fa-solid fa-reply"></i></span>
|
||||
<span class="compose-reply-btn-label">Reply</span>
|
||||
</button>
|
||||
</article>
|
||||
</div>
|
||||
7
core/templates/partials/compose-message-rows.html
Normal file
7
core/templates/partials/compose-message-rows.html
Normal file
@@ -0,0 +1,7 @@
|
||||
{% if message_rows %}
|
||||
{% for msg in message_rows %}
|
||||
{% include "partials/compose-message-row.html" with msg=msg only %}
|
||||
{% endfor %}
|
||||
{% elif show_empty_state %}
|
||||
<p class="compose-empty">{{ empty_message|default:"No stored messages for this contact yet." }}</p>
|
||||
{% endif %}
|
||||
6
core/templates/partials/compose-panel-assets.html
Normal file
6
core/templates/partials/compose-panel-assets.html
Normal file
@@ -0,0 +1,6 @@
|
||||
{% load static %}
|
||||
<link rel="stylesheet" href="{% static 'css/compose-panel.css' %}">
|
||||
<script defer src="{% static 'js/compose-panel-core.js' %}"></script>
|
||||
<script defer src="{% static 'js/compose-panel-thread.js' %}"></script>
|
||||
<script defer src="{% static 'js/compose-panel-send.js' %}"></script>
|
||||
<script defer src="{% static 'js/compose-panel.js' %}"></script>
|
||||
@@ -1,5 +1,3 @@
|
||||
{% load static %}
|
||||
<link rel="stylesheet" href="{% static 'css/compose-panel.css' %}">
|
||||
<div
|
||||
id="{{ panel_id }}"
|
||||
class="compose-shell box"
|
||||
@@ -10,57 +8,63 @@
|
||||
data-initial-typing="{{ typing_state_json|default:'{}'|escape }}"
|
||||
data-cancel-send-url="{% url 'compose_cancel_send' %}"
|
||||
data-command-result-url="{% url 'compose_command_result' %}">
|
||||
<div class="compose-shell-head is-flex is-justify-content-space-between is-align-items-flex-start is-flex-wrap-wrap is-gap-2 mb-3">
|
||||
<div class="compose-shell-head is-flex is-justify-content-space-between is-align-items-flex-start is-flex-wrap-wrap is-gap-2 mb-2">
|
||||
<div>
|
||||
<p class="compose-shell-eyebrow is-size-7 has-text-weight-semibold mb-1">Manual Text Mode</p>
|
||||
{% if recent_contacts %}
|
||||
<div class="compose-contact-switch">
|
||||
<div class="select is-small">
|
||||
<select id="{{ panel_id }}-contact-select" class="compose-contact-select">
|
||||
{% for option in recent_contacts %}
|
||||
<option
|
||||
value="{{ option.identifier }}"
|
||||
data-service="{{ option.service }}"
|
||||
{% if option.person_id %}data-person="{{ option.person_id }}"{% endif %}
|
||||
data-service-map='{{ option.service_identifiers_json|escape }}'
|
||||
{% if option.is_active %}selected{% endif %}>
|
||||
{{ option.person_name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="is-size-6 mb-0">
|
||||
{% if person %}
|
||||
{{ person.name }}
|
||||
{% elif group_name %}
|
||||
{{ group_name }}
|
||||
<div class="compose-context-row">
|
||||
<div class="compose-context-primary">
|
||||
{% if recent_contacts %}
|
||||
<div class="compose-contact-switch">
|
||||
<div class="select is-small is-fullwidth">
|
||||
<select id="{{ panel_id }}-contact-select" class="compose-contact-select">
|
||||
{% for option in recent_contacts %}
|
||||
<option
|
||||
value="{{ option.identifier }}"
|
||||
data-service="{{ option.service }}"
|
||||
{% if option.person_id %}data-person="{{ option.person_id }}"{% endif %}
|
||||
data-service-map='{{ option.service_identifiers_json|escape }}'
|
||||
{% if option.is_active %}selected{% endif %}>
|
||||
{{ option.person_name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ identifier }}
|
||||
<p class="is-size-6 mb-0">
|
||||
{% if person %}
|
||||
{{ person.name }}
|
||||
{% elif group_name %}
|
||||
{{ group_name }}
|
||||
{% else %}
|
||||
{{ identifier }}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if platform_options %}
|
||||
<div class="compose-context-secondary">
|
||||
<div class="compose-platform-switch">
|
||||
<div class="select is-small is-fullwidth">
|
||||
<select id="{{ panel_id }}-platform-select" class="compose-platform-select">
|
||||
{% for option in platform_options %}
|
||||
<option
|
||||
value="{{ option.service }}"
|
||||
data-identifier="{{ option.identifier }}"
|
||||
data-person="{{ option.person_id }}"
|
||||
{% if option.is_active %}selected{% endif %}>
|
||||
{{ option.service_label }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p id="{{ panel_id }}-meta-line" class="is-size-7 compose-meta-line mb-0">
|
||||
{{ service|title }} · {{ identifier }}
|
||||
</p>
|
||||
{% if platform_options %}
|
||||
<div class="compose-platform-switch">
|
||||
<div class="select is-small">
|
||||
<select id="{{ panel_id }}-platform-select" class="compose-platform-select">
|
||||
{% for option in platform_options %}
|
||||
<option
|
||||
value="{{ option.service }}"
|
||||
data-identifier="{{ option.identifier }}"
|
||||
data-person="{{ option.person_id }}"
|
||||
{% if option.is_active %}selected{% endif %}>
|
||||
{{ option.service_label }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -86,96 +90,7 @@
|
||||
data-limit="{{ limit }}"
|
||||
data-last-ts="{{ last_ts }}"
|
||||
data-ws-url="{{ compose_ws_url }}">
|
||||
{% for msg in serialized_messages %}
|
||||
<div class="compose-row {% if msg.outgoing %}is-out{% else %}is-in{% endif %}{% if msg.is_deleted %} is-deleted{% endif %}" data-ts="{{ msg.ts }}" data-message-id="{{ msg.id }}"{% if msg.reply_to_id %} data-reply-to-id="{{ msg.reply_to_id }}"{% endif %} data-reply-snippet="{{ msg.display_text|default:msg.text|default:''|truncatechars:120|escape }}">
|
||||
<article class="compose-bubble {% if msg.outgoing %}is-out{% else %}is-in{% endif %}">
|
||||
{% if msg.reply_to_id %}
|
||||
<div class="compose-reply-ref" data-reply-target-id="{{ msg.reply_to_id }}" data-reply-preview="{{ msg.reply_preview|default:''|escape }}">
|
||||
<button type="button" class="compose-reply-link" title="Jump to referenced message"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="compose-source-badge-wrap">
|
||||
<span class="tag is-light compose-source-badge source-{{ msg.source_service|default:'web'|lower }}">{{ msg.source_label }}</span>
|
||||
</div>
|
||||
{% if msg.image_urls %}
|
||||
{% for image_url in msg.image_urls %}
|
||||
<figure class="compose-media">
|
||||
<img
|
||||
class="compose-image"
|
||||
src="{{ image_url }}"
|
||||
alt="Attachment"
|
||||
referrerpolicy="no-referrer"
|
||||
loading="lazy"
|
||||
decoding="async">
|
||||
</figure>
|
||||
{% endfor %}
|
||||
{% elif msg.image_url %}
|
||||
<figure class="compose-media">
|
||||
<img
|
||||
class="compose-image"
|
||||
src="{{ msg.image_url }}"
|
||||
alt="Attachment"
|
||||
referrerpolicy="no-referrer"
|
||||
loading="lazy"
|
||||
decoding="async">
|
||||
</figure>
|
||||
{% endif %}
|
||||
{% if not msg.hide_text %}
|
||||
<p class="compose-body">{{ msg.display_text|default:"(no text)" }}</p>
|
||||
{% else %}
|
||||
<p class="compose-body compose-image-fallback is-hidden">(no text)</p>
|
||||
{% endif %}
|
||||
{% if msg.edit_count %}
|
||||
<details class="compose-edit-history">
|
||||
<summary>Edited {{ msg.edit_count }} time{% if msg.edit_count != 1 %}s{% endif %}</summary>
|
||||
<ul>
|
||||
{% for edit in msg.edit_history %}
|
||||
<li>
|
||||
{% if edit.edited_display %}{{ edit.edited_display }}{% else %}Unknown time{% endif %}
|
||||
{% if edit.actor %} · {{ edit.actor }}{% endif %}
|
||||
{% if edit.source_service %} · {{ edit.source_service|upper }}{% endif %}
|
||||
<div class="compose-edit-diff">
|
||||
<span class="compose-edit-old">{{ edit.previous_text|default:"(empty)" }}</span>
|
||||
<span class="compose-edit-arrow">→</span>
|
||||
<span class="compose-edit-new">{{ edit.new_text|default:"(empty)" }}</span>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
{% endif %}
|
||||
{% if msg.reactions %}
|
||||
<div class="compose-reactions" aria-label="Message reactions">
|
||||
{% for reaction in msg.reactions %}
|
||||
<span
|
||||
class="tag is-light compose-reaction-chip"
|
||||
data-emoji="{{ reaction.emoji|escape }}"
|
||||
data-actor="{{ reaction.actor|default:''|escape }}"
|
||||
data-source-service="{{ reaction.source_service|default:''|escape }}"
|
||||
title="{{ reaction.actor|default:'Unknown' }} via {{ reaction.source_service|default:'unknown'|upper }}">
|
||||
{{ reaction.emoji }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p class="compose-msg-meta">
|
||||
{{ msg.display_ts }}{% if msg.author %} · {{ msg.author }}{% endif %}
|
||||
{% if msg.is_edited %}
|
||||
<span class="tag is-light compose-msg-flag is-edited" title="Message edited{% if msg.last_edit_display %} at {{ msg.last_edit_display }}{% endif %}">edited</span>
|
||||
{% endif %}
|
||||
{% if msg.is_deleted %}
|
||||
<span class="tag is-light compose-msg-flag is-deleted" title="Deleted{% if msg.deleted_display %} at {{ msg.deleted_display }}{% endif %}{% if msg.deleted_actor %} by {{ msg.deleted_actor }}{% endif %}">deleted</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
<button type="button" class="button is-white is-small compose-reply-btn" title="Reply to this message" aria-label="Reply to this message">
|
||||
<span class="icon is-small"><i class="fa-solid fa-reply"></i></span>
|
||||
<span class="compose-reply-btn-label">Reply</span>
|
||||
</button>
|
||||
</article>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="compose-empty">No stored messages for this contact yet.</p>
|
||||
{% endfor %}
|
||||
{% include "partials/compose-message-rows.html" with message_rows=serialized_messages show_empty_state=True empty_message="No stored messages for this contact yet." %}
|
||||
</div>
|
||||
<p id="{{ panel_id }}-typing" class="compose-typing is-hidden">
|
||||
{% if person %}{{ person.name }}{% else %}Contact{% endif %} is typing...
|
||||
@@ -198,8 +113,15 @@
|
||||
<input type="hidden" name="failsafe_arm" value="0">
|
||||
<input type="hidden" name="failsafe_confirm" value="0">
|
||||
<div class="compose-send-safety">
|
||||
<label class="checkbox is-size-7">
|
||||
<input type="checkbox" class="manual-confirm"{% if not capability_send %} disabled{% endif %}> Confirm Send
|
||||
<label class="checkbox is-size-7" for="{{ panel_id }}-manual-confirm">
|
||||
<input
|
||||
id="{{ panel_id }}-manual-confirm"
|
||||
type="checkbox"
|
||||
class="manual-confirm"
|
||||
name="manual_confirm"
|
||||
value="1"
|
||||
{% if not capability_send %}disabled{% endif %}>
|
||||
Confirm Send
|
||||
</label>
|
||||
{% if not capability_send %}
|
||||
<p class="help is-size-7 has-text-grey">Send disabled: {{ capability_send_reason }}</p>
|
||||
@@ -210,19 +132,6 @@
|
||||
<span id="{{ panel_id }}-reply-text" class="compose-reply-banner-text"></span>
|
||||
<button type="button" id="{{ panel_id }}-reply-clear" class="button is-white is-small compose-reply-clear-btn">Clear</button>
|
||||
</div>
|
||||
<div class="compose-composer-capsule box is-shadowless">
|
||||
<textarea
|
||||
id="{{ panel_id }}-textarea"
|
||||
class="textarea compose-textarea"
|
||||
name="text"
|
||||
rows="1"
|
||||
placeholder="Type a message. Enter to send, Shift+Enter for newline."></textarea>
|
||||
<button class="button is-link is-light compose-send-btn" type="submit" disabled{% if not capability_send %} title="{{ capability_send_reason }}"{% endif %}>
|
||||
<span class="icon is-small"><i class="{{ manual_icon_class }}"></i></span>
|
||||
<span>Send</span>
|
||||
</button>
|
||||
</div>
|
||||
{% include "partials/bulma-send-composer.html" with composer_class="compose-composer-capsule" textarea_id=panel_id|add:"-textarea" textarea_class="compose-textarea" textarea_name="text" textarea_rows="1" textarea_placeholder="Type a message. Enter to send, Shift+Enter for newline." button_class="is-link is-light compose-send-btn" button_type="submit" button_disabled=True button_title=capability_send_reason|default_if_none:"" button_label="Send" button_icon_class=manual_icon_class %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script src="{% static 'js/compose-panel.js' %}"></script>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<div id="{{ results_id }}">
|
||||
<p class="is-size-7 has-text-grey mb-3">
|
||||
{% if total_matches %}
|
||||
{% if is_search_results %}
|
||||
Showing {{ visible_count }} of {{ total_matches }} contacts.
|
||||
{% elif result_mode == "active_chats" %}
|
||||
Recent chats. Search to see all {{ total_contacts }} contacts.
|
||||
{% else %}
|
||||
Showing {{ visible_count }} contacts.
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if is_search_results %}
|
||||
No contacts found.
|
||||
{% else %}
|
||||
No contacts yet.
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
{% if contact_rows %}
|
||||
<nav class="panel">
|
||||
{% for row in contact_rows %}
|
||||
<a
|
||||
class="panel-block"
|
||||
hx-get="{{ row.compose_widget_url }}"
|
||||
hx-target="#widgets-here"
|
||||
hx-swap="beforeend">
|
||||
<span class="is-flex is-justify-content-space-between is-align-items-center is-flex-grow-1" style="min-width: 0; gap: 0.75rem;">
|
||||
<span style="min-width: 0;">
|
||||
<span class="has-text-weight-semibold">{{ row.person_name }}</span>
|
||||
<span class="is-size-7 has-text-grey is-block" style="overflow-wrap: anywhere;">
|
||||
{{ row.identifier }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="tag is-light gia-badge">{{ row.service|title }}</span>
|
||||
</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
{% if has_more %}
|
||||
<div class="mt-3">
|
||||
<button
|
||||
class="button is-small is-fullwidth is-light"
|
||||
hx-get="{{ results_url }}"
|
||||
hx-include="#{{ launcher_form_id }}"
|
||||
hx-vals='{"page": "{{ next_page }}"}'
|
||||
hx-target="#{{ results_id }}"
|
||||
hx-swap="outerHTML">
|
||||
Show {{ next_count }} more
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -1,117 +1,31 @@
|
||||
<div class="compose-workspace-widget">
|
||||
<div class="columns is-mobile is-gapless">
|
||||
<div class="column is-12-mobile is-12-tablet">
|
||||
<div
|
||||
style="
|
||||
margin-bottom: 0.75rem;
|
||||
padding: 0.5rem 0.25rem;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
">
|
||||
<p class="is-size-7 has-text-weight-semibold">Manual Workspace</p>
|
||||
<h3 class="title is-6" style="margin-bottom: 0.5rem;">Choose A Contact</h3>
|
||||
<p class="is-size-7">
|
||||
Open one or more direct chat widgets and keep them live in this workspace.
|
||||
</p>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<h3 class="title is-6 mb-1">Contacts</h3>
|
||||
<p class="is-size-7 has-text-grey">
|
||||
See all your contacts. Search to narrow the list.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form
|
||||
id="compose-workspace-window-form"
|
||||
style="
|
||||
margin-bottom: 0.75rem;
|
||||
padding: 0.5rem 0.25rem;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
">
|
||||
<label class="label is-small" for="compose-workspace-limit">Window</label>
|
||||
<div class="select is-fullwidth is-small">
|
||||
<select id="compose-workspace-limit" name="limit">
|
||||
{% for option in limit_options %}
|
||||
<option value="{{ option }}" {% if option == limit %}selected{% endif %}>
|
||||
{{ option }} messages
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<p class="help">
|
||||
How many recent messages to load in each new message widget.
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<div>
|
||||
{% if contact_rows %}
|
||||
<div class="buttons are-small" style="display: grid; gap: 0.5rem;">
|
||||
{% for row in contact_rows %}
|
||||
<button
|
||||
class="button is-fullwidth"
|
||||
style="
|
||||
border-radius: 8px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
"
|
||||
hx-get="{{ row.compose_widget_url }}"
|
||||
hx-include="#compose-workspace-window-form"
|
||||
hx-target="#widgets-here"
|
||||
hx-swap="afterend">
|
||||
<span
|
||||
class="tags has-addons"
|
||||
style="
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
">
|
||||
<span
|
||||
class="tag is-white"
|
||||
style="
|
||||
flex: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding-left: 0.7rem;
|
||||
padding-right: 0.7rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
min-width: 0;
|
||||
">
|
||||
<span
|
||||
style="
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 0.45rem;
|
||||
min-width: 0;
|
||||
">
|
||||
<strong>{{ row.person_name }}</strong>
|
||||
<small class="has-text-grey">{{ row.service|title }}</small>
|
||||
</span>
|
||||
<small
|
||||
class="has-text-grey"
|
||||
style="
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-all;
|
||||
text-align: right;
|
||||
">
|
||||
{{ row.identifier }}
|
||||
</small>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{% if not row.linked_person %}
|
||||
<a
|
||||
class="button is-small is-light"
|
||||
href="{{ row.match_url }}"
|
||||
title="Link this identifier to a person">
|
||||
<span class="icon is-small"><i class="fa-solid fa-link"></i></span>
|
||||
<span>Match</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="has-text-grey">No contacts available yet.</p>
|
||||
{% endif %}
|
||||
<form id="{{ launcher_form_id }}">
|
||||
<div class="field">
|
||||
<label class="label is-small" for="{{ search_input_id }}">Search</label>
|
||||
<div class="control has-icons-left">
|
||||
<input
|
||||
id="{{ search_input_id }}"
|
||||
class="input is-small"
|
||||
type="search"
|
||||
name="q"
|
||||
value="{{ search_query }}"
|
||||
placeholder="Name, identifier, or service"
|
||||
hx-get="{{ results_url }}"
|
||||
hx-trigger="input changed delay:250ms, search"
|
||||
hx-target="#{{ results_id }}"
|
||||
hx-include="#{{ launcher_form_id }}"
|
||||
hx-swap="outerHTML">
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-magnifying-glass"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% include "partials/compose-workspace-contact-results.html" %}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<div id="{{ results_id }}">
|
||||
<p class="is-size-7 has-text-grey mb-3">
|
||||
{% if history_rows %}
|
||||
Showing {{ result_start }}-{{ result_end }} recent matches.
|
||||
{% else %}
|
||||
No persisted messages match this search.
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
{% if history_rows %}
|
||||
<div class="is-flex is-flex-direction-column" style="gap: 0.5rem;">
|
||||
{% for row in history_rows %}
|
||||
<div class="box is-shadowless p-3 m-0">
|
||||
<div class="is-flex is-justify-content-space-between is-align-items-flex-start" style="gap: 0.75rem;">
|
||||
<div style="min-width: 0;">
|
||||
<p class="has-text-weight-semibold mb-1">
|
||||
{{ row.person_name }}
|
||||
<span class="tag is-light gia-badge ml-2">{{ row.service_label }}</span>
|
||||
<span class="tag {% if row.outgoing %}is-warning{% else %}is-info{% endif %} is-light gia-badge ml-1">{{ row.direction_label }}</span>
|
||||
</p>
|
||||
<p class="is-size-7 has-text-grey mb-1" style="overflow-wrap: anywhere;">
|
||||
{{ row.identifier }} · {{ row.display_ts }}
|
||||
</p>
|
||||
<p class="mb-0" style="overflow-wrap: anywhere;">
|
||||
{{ row.text_preview }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="buttons are-small m-0">
|
||||
<button
|
||||
class="button is-small is-link is-light"
|
||||
hx-get="{{ row.compose_widget_url }}"
|
||||
hx-include="#{{ browser_form_id }}"
|
||||
hx-target="#widgets-here"
|
||||
hx-swap="beforeend">
|
||||
<span class="icon is-small"><i class="fa-solid fa-table-cells-large"></i></span>
|
||||
<span>Open</span>
|
||||
</button>
|
||||
<a class="button is-small is-light" href="{{ row.compose_page_url }}">
|
||||
<span class="icon is-small"><i class="fa-solid fa-arrow-up-right-from-square"></i></span>
|
||||
<span>Page</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if has_more %}
|
||||
<div class="mt-3">
|
||||
<button
|
||||
class="button is-small is-fullwidth is-light"
|
||||
hx-get="{{ results_url }}"
|
||||
hx-include="#{{ browser_form_id }}"
|
||||
hx-vals='{"page": "{{ next_page }}"}'
|
||||
hx-target="#{{ results_id }}"
|
||||
hx-swap="outerHTML">
|
||||
Show more history
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
108
core/templates/partials/compose-workspace-history-widget.html
Normal file
108
core/templates/partials/compose-workspace-history-widget.html
Normal file
@@ -0,0 +1,108 @@
|
||||
<div class="compose-workspace-widget">
|
||||
<div class="mb-4">
|
||||
<p class="is-size-7 has-text-weight-semibold">Manual Workspace</p>
|
||||
<h3 class="title is-6 mb-2">Browse Message History</h3>
|
||||
<p class="is-size-7">
|
||||
Filter persisted messages across contacts, then reopen the matching live thread widget.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form id="{{ browser_form_id }}">
|
||||
{% if person_scope_id %}
|
||||
<input type="hidden" name="person" value="{{ person_scope_id }}">
|
||||
{% endif %}
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-small" for="{{ search_input_id }}">Find Messages</label>
|
||||
<div class="control has-icons-left">
|
||||
<input
|
||||
id="{{ search_input_id }}"
|
||||
class="input is-small"
|
||||
type="search"
|
||||
name="q"
|
||||
value="{{ search_query }}"
|
||||
placeholder="Person, identifier, or message text"
|
||||
hx-get="{{ results_url }}"
|
||||
hx-trigger="input changed delay:250ms, search"
|
||||
hx-target="#{{ results_id }}"
|
||||
hx-include="#{{ browser_form_id }}"
|
||||
hx-swap="outerHTML">
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-magnifying-glass"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns is-mobile is-multiline mb-1">
|
||||
<div class="column is-half pt-0 pb-2">
|
||||
<label class="label is-small" for="{{ service_input_id }}">Service</label>
|
||||
<div class="select is-small is-fullwidth">
|
||||
<select
|
||||
id="{{ service_input_id }}"
|
||||
name="service"
|
||||
hx-get="{{ results_url }}"
|
||||
hx-trigger="change"
|
||||
hx-target="#{{ results_id }}"
|
||||
hx-include="#{{ browser_form_id }}"
|
||||
hx-swap="outerHTML">
|
||||
{% for value, label in service_options %}
|
||||
<option value="{{ value }}" {% if value == service %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-half pt-0 pb-2">
|
||||
<label class="label is-small" for="{{ direction_input_id }}">Direction</label>
|
||||
<div class="select is-small is-fullwidth">
|
||||
<select
|
||||
id="{{ direction_input_id }}"
|
||||
name="direction"
|
||||
hx-get="{{ results_url }}"
|
||||
hx-trigger="change"
|
||||
hx-target="#{{ results_id }}"
|
||||
hx-include="#{{ browser_form_id }}"
|
||||
hx-swap="outerHTML">
|
||||
{% for value, label in direction_options %}
|
||||
<option value="{{ value }}" {% if value == direction %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-half pt-0 pb-2">
|
||||
<label class="label is-small" for="{{ days_input_id }}">Range</label>
|
||||
<div class="select is-small is-fullwidth">
|
||||
<select
|
||||
id="{{ days_input_id }}"
|
||||
name="days"
|
||||
hx-get="{{ results_url }}"
|
||||
hx-trigger="change"
|
||||
hx-target="#{{ results_id }}"
|
||||
hx-include="#{{ browser_form_id }}"
|
||||
hx-swap="outerHTML">
|
||||
{% for value, label in days_options %}
|
||||
<option value="{{ value }}" {% if value == days %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-half pt-0 pb-2">
|
||||
<label class="label is-small" for="{{ thread_limit_input_id }}">Thread Window</label>
|
||||
<div class="select is-small is-fullwidth">
|
||||
<select id="{{ thread_limit_input_id }}" name="limit">
|
||||
{% for option in thread_limit_options %}
|
||||
<option value="{{ option }}" {% if option == limit %}selected{% endif %}>
|
||||
{{ option }} messages
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if person_scope %}
|
||||
<p class="help mb-3">
|
||||
Scoped to <strong>{{ person_scope.name }}</strong>.
|
||||
</p>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
{% include "partials/compose-workspace-history-results.html" %}
|
||||
</div>
|
||||
@@ -23,7 +23,7 @@
|
||||
<a
|
||||
class="has-text-grey button nowrap-child"
|
||||
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{ item.id }}');">
|
||||
<span class="icon" data-tooltip="Copy to clipboard">
|
||||
<span class="icon" title="Copy to clipboard" aria-label="Copy to clipboard">
|
||||
<i class="fa-solid fa-copy" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
@@ -66,4 +66,4 @@
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
{% endcache %}
|
||||
{% endcache %}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<a
|
||||
class="has-text-grey button nowrap-child"
|
||||
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{ item.id }}');">
|
||||
<span class="icon" data-tooltip="Copy to clipboard">
|
||||
<span class="icon" title="Copy to clipboard" aria-label="Copy to clipboard">
|
||||
<i class="fa-solid fa-copy" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
@@ -94,4 +94,4 @@
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
{% endcache %}
|
||||
{% endcache %}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<a
|
||||
class="has-text-grey button nowrap-child"
|
||||
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{ item.id }}');">
|
||||
<span class="icon" data-tooltip="Copy to clipboard">
|
||||
<span class="icon" title="Copy to clipboard" aria-label="Copy to clipboard">
|
||||
<i class="fa-solid fa-copy" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
@@ -40,7 +40,7 @@
|
||||
<a
|
||||
class="has-text-grey button nowrap-child"
|
||||
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{ item.sender_uuid }}');">
|
||||
<span class="icon" data-tooltip="Copy to clipboard">
|
||||
<span class="icon" title="Copy to clipboard" aria-label="Copy to clipboard">
|
||||
<i class="fa-solid fa-copy" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
<a class="navbar-item" href="{% url 'compose_workspace' %}">
|
||||
<span class="icon is-small"><i class="fa-solid fa-table-cells-large"></i></span>
|
||||
<span style="margin-left: 0.35rem;">Compose Workspace</span>
|
||||
</a>
|
||||
<hr class="navbar-divider" style="margin: 0.2rem 0;">
|
||||
{% if items %}
|
||||
{% for item in items %}
|
||||
<a class="navbar-item" href="{{ item.compose_url }}">
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
class="button osint-capsule-tab"
|
||||
hx-get="{{ tab.widget_url }}"
|
||||
hx-target="#widgets-here"
|
||||
hx-swap="afterend"
|
||||
onclick="document.getElementById('widgets-here').style.display='block';"
|
||||
hx-swap="beforeend"
|
||||
title="Open {{ tab.label }} setup widget">
|
||||
<span class="icon is-small"><i class="{{ tab.icon }}"></i></span>
|
||||
<span>{{ tab.label }}</span>
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
hx-get="{{ action.url }}"
|
||||
hx-target="{{ action.target }}"
|
||||
hx-swap="innerHTML"
|
||||
{% if action.target == "#windows-here" %}onclick="window.giaPrepareWindowAnchor(this);"{% endif %}
|
||||
{% if action.target == "#windows-here" %}onclick="if (window.giaPrepareWindowAnchor) { window.giaPrepareWindowAnchor(this); }"{% endif %}
|
||||
title="{{ action.title }}">
|
||||
<span class="icon"><i class="{{ action.icon }}"></i></span>
|
||||
</button>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<a
|
||||
class="has-text-grey button nowrap-child"
|
||||
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{ item.id }}');">
|
||||
<span class="icon" data-tooltip="Copy to clipboard">
|
||||
<span class="icon" title="Copy to clipboard" aria-label="Copy to clipboard">
|
||||
<i class="fa-solid fa-copy" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
@@ -77,4 +77,4 @@
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
{% endcache %}
|
||||
{% endcache %}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<a
|
||||
class="has-text-grey button nowrap-child"
|
||||
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{ item.id }}');">
|
||||
<span class="icon" data-tooltip="Copy to clipboard">
|
||||
<span class="icon" title="Copy to clipboard" aria-label="Copy to clipboard">
|
||||
<i class="fa-solid fa-copy" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
@@ -74,4 +74,4 @@
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
{% endcache %}
|
||||
{% endcache %}
|
||||
|
||||
@@ -37,7 +37,9 @@
|
||||
class="button is-success is-light"
|
||||
hx-get="{% url 'message_accept_api' message_id=item.id %}"
|
||||
hx-swap="none"
|
||||
_="on htmx:afterRequest if event.detail.successful remove #queue-card-{{ item.id }} then trigger {{ context_object_name_singular }}Event on body end">
|
||||
data-queue-after="remove-card"
|
||||
data-queue-card-id="queue-card-{{ item.id }}"
|
||||
data-queue-refresh-event="{{ context_object_name_singular }}Event">
|
||||
<span class="icon is-small"><i class="fa-solid fa-check"></i></span>
|
||||
<span>Approve</span>
|
||||
</button>
|
||||
@@ -45,7 +47,9 @@
|
||||
class="button is-danger is-light"
|
||||
hx-get="{% url 'message_reject_api' message_id=item.id %}"
|
||||
hx-swap="none"
|
||||
_="on htmx:afterRequest if event.detail.successful remove #queue-card-{{ item.id }} then trigger {{ context_object_name_singular }}Event on body end">
|
||||
data-queue-after="remove-card"
|
||||
data-queue-card-id="queue-card-{{ item.id }}"
|
||||
data-queue-refresh-event="{{ context_object_name_singular }}Event">
|
||||
<span class="icon is-small"><i class="fa-solid fa-xmark"></i></span>
|
||||
<span>Reject</span>
|
||||
</button>
|
||||
@@ -65,7 +69,8 @@
|
||||
hx-trigger="click"
|
||||
hx-target="#queue-inline-editor-{{ item.id }}"
|
||||
hx-swap="innerHTML"
|
||||
_="on htmx:afterRequest if event.detail.successful set #queue-inline-editor-{{ item.id }}.style.display to 'block' end"
|
||||
data-queue-after="show-inline-editor"
|
||||
data-queue-editor-id="queue-inline-editor-{{ item.id }}"
|
||||
class="button is-light">
|
||||
<span class="icon is-small"><i class="fa-solid fa-pen"></i></span>
|
||||
<span>Edit</span>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<a
|
||||
class="has-text-grey button nowrap-child"
|
||||
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{ item.id }}');">
|
||||
<span class="icon" data-tooltip="Copy to clipboard">
|
||||
<span class="icon" title="Copy to clipboard" aria-label="Copy to clipboard">
|
||||
<i class="fa-solid fa-copy" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
{% if settings_nav %}
|
||||
<h1 class="title is-4">{{ settings_nav.title }}</h1>
|
||||
<div class="tabs is-boxed is-small mb-4 security-page-tabs">
|
||||
<ul>
|
||||
{% for tab in settings_nav.tabs %}
|
||||
<li class="{% if tab.active %}is-active{% endif %}">
|
||||
<a href="{{ tab.href }}">{{ tab.label }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<nav class="gia-settings-nav" aria-label="Settings navigation">
|
||||
{% if settings_nav.groups %}
|
||||
<div class="tabs is-boxed is-small mb-2 security-page-tabs gia-settings-nav-groups">
|
||||
<ul>
|
||||
{% for tab in settings_nav.groups %}
|
||||
<li class="{% if tab.active %}is-active{% endif %}">
|
||||
<a href="{{ tab.href }}">{{ tab.label }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for row in settings_nav.rows %}
|
||||
{% if row.tabs|length > 1 %}
|
||||
<div class="tabs is-toggle is-toggle-rounded is-small security-page-tabs gia-settings-nav-row {% if forloop.last %}mb-4{% else %}mb-2{% endif %}">
|
||||
<ul>
|
||||
{% for tab in row.tabs %}
|
||||
<li class="{% if tab.active %}is-active{% endif %}">
|
||||
<a href="{{ tab.href }}">{{ tab.label }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<a
|
||||
class="has-text-grey button nowrap-child"
|
||||
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{ item.chat.source_uuid }}');">
|
||||
<span class="icon" data-tooltip="Copy to clipboard">
|
||||
<span class="icon" title="Copy to clipboard" aria-label="Copy to clipboard">
|
||||
<i class="fa-solid fa-copy" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
@@ -112,7 +112,7 @@
|
||||
hx-get="{{ item.compose_widget_url }}"
|
||||
hx-trigger="click"
|
||||
hx-target="#widgets-here"
|
||||
hx-swap="afterend"
|
||||
hx-swap="beforeend"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<a
|
||||
class="has-text-grey button nowrap-child"
|
||||
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{ item.uuid }}');">
|
||||
<span class="icon" data-tooltip="Copy to clipboard">
|
||||
<span class="icon" title="Copy to clipboard" aria-label="Copy to clipboard">
|
||||
<i class="fa-solid fa-copy" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
@@ -55,4 +55,4 @@
|
||||
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endcache %}
|
||||
{% endcache %}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<a
|
||||
class="has-text-grey button nowrap-child"
|
||||
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{ item.identifier }}');">
|
||||
<span class="icon" data-tooltip="Copy identifier">
|
||||
<span class="icon" title="Copy identifier" aria-label="Copy identifier">
|
||||
<i class="fa-solid fa-copy" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
@@ -46,7 +46,7 @@
|
||||
hx-get="{{ item.compose_widget_url }}"
|
||||
hx-trigger="click"
|
||||
hx-target="#widgets-here"
|
||||
hx-swap="afterend"
|
||||
hx-swap="beforeend"
|
||||
class="button"
|
||||
title="Manual text mode widget">
|
||||
<span class="icon"><i class="fa-solid fa-paper-plane"></i></span>
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
hx-get="{{ item.compose_widget_url }}"
|
||||
hx-trigger="click"
|
||||
hx-target="#widgets-here"
|
||||
hx-swap="afterend"
|
||||
hx-swap="beforeend"
|
||||
class="button"
|
||||
title="Open manual chat widget">
|
||||
<span class="icon"><i class="fa-solid fa-paper-plane"></i></span>
|
||||
|
||||
7
core/templates/partials/workspace-widget-loader.html
Normal file
7
core/templates/partials/workspace-widget-loader.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<div
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{{ widget_url }}"
|
||||
hx-target="{% firstof target_selector '#widgets-here' %}"
|
||||
hx-trigger="{% firstof trigger_name 'load' %}{% if trigger_delay %} delay:{{ trigger_delay }}{% endif %}"
|
||||
hx-swap="{% firstof swap_strategy 'beforeend' %}"
|
||||
style="display: none;"></div>
|
||||
@@ -1 +1,9 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content_wrapper %}
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user