- Added EveAI Client to project

- Improvements to EntitlementsDomain & Services
- Prechecks in Document domain
- Add audit information to LicenseUsage
This commit is contained in:
Josako
2025-05-17 15:56:14 +02:00
parent b4f7b210e0
commit 5c982fcc2c
260 changed files with 48683 additions and 43 deletions

View File

@@ -0,0 +1,56 @@
<html lang="en" itemscope itemtype="http://schema.org/WebPage">
{% include 'head.html' %}
<body>
<!-- Overall Layout -------------------------------------------------------------------------->
<div class="wrapper">
<!-- Top Navigation Bar ----------------------------->
<header class="navbar navbar-expand-lg navbar-light bg-white">
<div class="container-fluid">
<h1 class="navbar-brand mb-0">EveAI Client</h1>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('main.index') }}">
<i class="material-icons">home</i> Home
</a>
</li>
</ul>
</div>
</div>
</header>
<!-- Main container Layout -------------------------->
<main class="container-fluid mt-2 mb-4 px-3 console-expanded">
{% block content %}{% endblock %}
</main>
</div>
<!-- Status Bar ----------------------------------------->
<div class="status-bar fixed-bottom w-100 bg-light border-top">
<div class="container-fluid p-0">
<!-- Toggle button for console -->
<div class="d-flex justify-content-between align-items-center p-1">
<button id="toggleConsole" class="btn btn-sm" type="button" data-bs-toggle="collapse" data-bs-target="#consoleOutput" aria-expanded="true" aria-controls="consoleOutput">
<i class="material-icons" style="font-size: 16px;">expand_more</i>
</button>
<span id="currentStatus">Ready</span>
<span id="processingStatus" class="badge bg-ready">Ready</span>
</div>
<!-- Collapsible console log area - show by default -->
<div id="consoleOutput" class="console-output collapse show">
<div id="statusMessages" class="status-messages p-2">
<div class="log-entries">
<!-- Initial message will be added here -->
</div>
</div>
</div>
</div>
</div>
</body>
{% include 'scripts.html' %}
</html>

View File

@@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block title %}EveAI - Error{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header p-3 bg-danger">
<h2 class="card-title text-white mb-0">Error</h2>
</div>
<div class="card-body">
<div class="alert alert-danger mb-4">
{{ error }}
</div>
<div class="d-flex justify-content-between">
<button onclick="window.history.back()" class="btn btn-secondary">Back</button>
<a href="{{ url_for('main.index') }}" class="btn btn-primary">Home</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,34 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="apple-touch-icon" sizes="76x76" href="{{url_for('static', filename='assets/img/apple-icon.png')}}">
<link rel="icon" type="image/png" href="{{url_for('static', filename='assets/img/favicon.png')}}">
<title>
{% block title %}EveAI Client{% endblock %}
</title>
<!-- Fonts and icons -->
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700,900|Roboto+Slab:400,700" />
<!-- Nucleo Icons -->
<link href="{{url_for('static', filename='assets/css/nucleo-icons.css" rel="stylesheet')}}" />
<link href="{{url_for('static', filename='assets/css/nucleo-svg.css" rel="stylesheet')}}" />
<!-- Font Awesome Icons -->
<script src="https://kit.fontawesome.com/42d5adcbca.js" crossorigin="anonymous"></script>
<!-- Material Icons -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons+Round" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet" />
<!-- CSS Files -->
<link id="pagestyle" href="{{url_for('static', filename='assets/css/material-kit-pro.css')}}" rel="stylesheet" />
<link id="pagestyle" href="{{url_for('static', filename='assets/css/eveai.css')}}" rel="stylesheet" />
<!-- HTMX Scripts -->
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
<script>
// Initialize a basic logViewer placeholder that will collect messages until the real one is ready
window.logViewerQueue = [];
window.logViewer = {
info: function(msg) { window.logViewerQueue.push({type: 'info', message: msg}); },
error: function(msg) { window.logViewerQueue.push({type: 'error', message: msg}); },
warning: function(msg) { window.logViewerQueue.push({type: 'warning', message: msg}); },
success: function(msg) { window.logViewerQueue.push({type: 'success', message: msg}); }
};
</script>
</head>

View File

@@ -0,0 +1,433 @@
{% macro render_field_content(field, disabled=False, class='') %}
{% if field.type == 'BooleanField' %}
<div class="form-group">
<div class="form-check form-switch">
{{ field(class="form-check-input " + class, disabled=disabled) }}
{% if field.description %}
{{ field.label(class="form-check-label",
**{'data-bs-toggle': 'tooltip',
'data-bs-placement': 'right',
'title': field.description}) }}
{% if field.flags.required %}
<span class="required-field-indicator" aria-hidden="true">
<i class="material-symbols-outlined required-icon">check_circle</i>
</span>
<span class="visually-hidden">Required field</span>
{% endif %}
{% else %}
{{ field.label(class="form-check-label") }}
{% if field.flags.required %}
<span class="required-field-indicator" aria-hidden="true">
<i class="material-symbols-outlined required-icon">check_circle</i>
</span>
<span class="visually-hidden">Required field</span>
{% endif %}
{% endif %}
</div>
{% if field.errors %}
<div class="invalid-feedback d-block">
{% for error in field.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
{% else %}
<div class="form-group">
{% if field.description %}
{{ field.label(class="form-label",
**{'data-bs-toggle': 'tooltip',
'data-bs-placement': 'right',
'title': field.description}) }}
{% if field.flags.required %}
<span class="required-field-indicator" aria-hidden="true">
<i class="material-symbols-outlined required-icon">check_circle</i>
</span>
<span class="visually-hidden">Required field</span>
{% endif %}
{% else %}
{{ field.label(class="form-label") }}
{% if field.flags.required %}
<span class="required-field-indicator" aria-hidden="true">
<i class="material-symbols-outlined required-icon">check_circle</i>
</span>
<span class="visually-hidden">Required field</span>
{% endif %}
{% endif %}
{% if field.type == 'TextAreaField' and 'json-editor' in class %}
<div id="{{ field.id }}-editor" class="json-editor-container"></div>
{{ field(class="form-control d-none " + class, disabled=disabled) }}
{% elif field.type == 'SelectField' %}
{{ field(class="form-control form-select " + class, disabled=disabled) }}
{% else %}
{{ field(class="form-control " + class, disabled=disabled) }}
{% endif %}
{% if field.errors %}
<div class="invalid-feedback d-block">
{% for error in field.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
{% endif %}
{% endmacro %}
{% macro render_field(field, disabled_fields=[], exclude_fields=[], class='') %}
<!-- Debug info -->
<!-- Field name: {{ field.name }}, Field type: {{ field.__class__.__name__ }} -->
{% set disabled = field.name in disabled_fields %}
{% set exclude_fields = exclude_fields + ['csrf_token', 'submit'] %}
{% if field.name not in exclude_fields %}
{{ render_field_content(field, disabled, class) }}
{% endif %}
{% endmacro %}
{% macro render_included_field(field, disabled_fields=[], include_fields=[], class='') %}
{% set disabled = field.name in disabled_fields %}
{% if field.name in include_fields %}
{{ render_field_content(field, disabled, class) }}
{% endif %}
{% endmacro %}
{% macro render_table(headers, rows) %}
<div class="card">
<div class="table-responsive">
<table class="table align-items-center mb-0">
<thead>
<tr>
{% for header in headers %}
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">{{ header }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr>
{% for cell in row %}
<td class="{{ cell.class }}">
{% if cell.type == 'image' %}
<div class="d-flex px-2 py-1">
<div>
<img src="{{ cell.value }}" class="avatar avatar-sm me-3">
</div>
</div>
{% elif cell.type == 'text' %}
<p class="text-xs {{ cell.text_class }}">{{ cell.value }}</p>
{% elif cell.type == 'badge' %}
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
{% elif cell.type == 'link' %}
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
{% else %}
{{ cell.value }}
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endmacro %}
{% macro render_selectable_table(headers, rows, selectable, id, is_component_selector=False) %}
<div class="card">
<div class="table-responsive">
<table class="table align-items-center mb-0" id="{{ id }}">
<thead>
<tr>
{% if selectable %}
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">Select</th>
{% endif %}
{% for header in headers %}
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">{{ header }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr>
{% if selectable %}
<td>
<input type="radio"
name="selected_row"
value="{{ row[0] }}"
{% if is_component_selector %}
data-component-selector="true"
{% else %}
required
{% endif %}>
</td>
{% endif %}
{% for cell in row %}
<td class="{{ cell.class }}">
{% if cell.type == 'image' %}
<div class="d-flex px-2 py-1">
<div>
<img src="{{ cell.value }}" class="avatar avatar-sm me-3">
</div>
</div>
{% elif cell.type == 'text' %}
<p class="text-xs {{ cell.text_class }}">{{ cell.value }}</p>
{% elif cell.type == 'badge' %}
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
{% elif cell.type == 'link' %}
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
{% else %}
{{ cell.value }}
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endmacro %}
{% macro render_selectable_sortable_table(headers, rows, selectable, id, sort_by, sort_order) %}
<div class="card">
<div class="table-responsive">
<table class="table align-items-center mb-0" id="{{ id }}">
<thead>
<tr>
{% if selectable %}
<th>Select</th>
{% endif %}
{% for header in headers %}
<th class="sortable" data-sort="{{ header|lower|replace(' ', '_') }}">
{{ header }}
{% if sort_by == header|lower|replace(' ', '_') %}
{% if sort_order == 'asc' %}
<i class="fas fa-sort-up"></i>
{% elif sort_order == 'desc' %}
<i class="fas fa-sort-down"></i>
{% endif %}
{% else %}
<i class="fas fa-sort"></i>
{% endif %}
</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr>
{% if selectable %}
<td><input type="radio" name="selected_row" value="{{ row[0].value }}"></td>
{% endif %}
{% for cell in row %}
<td>{{ cell.value }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endmacro %}
{% macro render_selectable_sortable_table_with_dict_headers(headers, rows, selectable, id, sort_by, sort_order) %}
<div class="card">
<div class="table-responsive">
<table class="table align-items-center mb-0" id="{{ id }}">
<thead>
<tr>
{% if selectable %}
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">Select</th>
{% endif %}
{% for header in headers %}
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 sortable" data-sort="{{ header['sort'] }}">
{{ header['text'] }}
{% if sort_by == header['sort'] %}
{% if sort_order == 'asc' %}
<i class="fas fa-sort-up"></i>
{% elif sort_order == 'desc' %}
<i class="fas fa-sort-down"></i>
{% endif %}
{% else %}
<i class="fas fa-sort"></i>
{% endif %}
</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr>
{% if selectable %}
<td><input type="radio" name="selected_row" value="{{ row[0].value }}"></td>
{% endif %}
{% for cell in row %}
<td>{{ cell.value }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endmacro %}
{% macro render_accordion(accordion_id, accordion_items, header_title, header_description) %}
<div class="accordion-1">
<div class="container">
<div class="row my-5">
<div class="col-md-6 mx-auto text-center">
<h2>{{ header_title }}</h2>
<p>{{ header_description }}</p>
</div>
</div>
<div class="row">
<div class="col-md-10 mx-auto">
<div class="accordion" id="{{ accordion_id }}">
{% for item in accordion_items %}
<div class="accordion-item mb-3">
<h5 class="accordion-header" id="heading{{ loop.index }}">
<button class="accordion-button border-bottom font-weight-bold collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse{{ loop.index }}" aria-expanded="false" aria-controls="collapse{{ loop.index }}">
{{ item.title }}
<i class="collapse-close fa fa-plus text-xs pt-1 position-absolute end-0 me-3" aria-hidden="true"></i>
<i class="collapse-open fa fa-minus text-xs pt-1 position-absolute end-0 me-3" aria-hidden="true"></i>
</button>
</h5>
<div id="collapse{{ loop.index }}" class="accordion-collapse collapse" aria-labelledby="heading{{ loop.index }}" data-bs-parent="#{{ accordion_id }}">
<div class="accordion-body text-sm opacity-8">
{{ item.content }}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endmacro %}
{% macro render_nested_table(headers, rows) %}
<div class="">
<div class="table-responsive">
<table class="table align-items-center mb-0">
<thead>
<tr>
{% for header in headers %}
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">{{ header }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr>
{% for cell in row %}
{% if cell.is_group %}
<td colspan="{{ cell.colspan }}" class="{{ cell.class }}">
{{ render_nested_table(cell.headers, cell.sub_rows) }}
</td>
{% else %}
<td class="{{ cell.class }}">
{% if cell.type == 'image' %}
<div class="d-flex px-2 py-1">
<div>
<img src="{{ cell.value }}" class="avatar avatar-sm me-3">
</div>
</div>
{% elif cell.type == 'text' %}
<p class="text-xs {{ cell.text_class }}">{{ cell.value }}</p>
{% elif cell.type == 'badge' %}
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
{% elif cell.type == 'link' %}
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
{% else %}
{{ cell.value }}
{% endif %}
</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endmacro %}
{% macro render_pagination(pagination, endpoint) %}
<nav aria-label="Page navigation">
<ul class="pagination">
<!-- Previous Button -->
<li class="page-item {{ 'disabled' if not pagination.has_prev }}">
<a class="page-link" href="{{ url_for(endpoint, page=pagination.prev_num) if pagination.has_prev else 'javascript:;' }}" tabindex="-1">
<span class="material-symbols-outlined">keyboard_double_arrow_left</span>
{# <span class="sr-only">Previous</span>#}
</a>
</li>
<!-- Page Number Buttons -->
{% for page in pagination.iter_pages(left_edge=1, left_current=2, right_current=3, right_edge=1) %}
<li class="page-item {{ 'active' if page == pagination.page }}">
<a class="page-link" href="{{ url_for(endpoint, page=page) }}">
{% if page == pagination.page %}
<span class="material-symbols-outlined">target</span>
{# <span class="sr-only">(current)</span>#}
{% else %}
{{ page }}
{% endif %}
</a>
</li>
{% endfor %}
<!-- Next Button -->
<li class="page-item {{ 'disabled' if not pagination.has_next }}">
<a class="page-link" href="{{ url_for(endpoint, page=pagination.next_num) if pagination.has_next else 'javascript:;' }}">
<span class="material-symbols-outlined">keyboard_double_arrow_right</span>
{# <span class="sr-only">Next</span>#}
</a>
</li>
</ul>
</nav>
{% endmacro %}
{% macro render_filter_field(field_name, label, options, current_value) %}
<div class="form-group">
<label for="{{ field_name }}">{{ label }}</label>
<select class="form-control" id="{{ field_name }}" name="{{ field_name }}">
<option value="">All</option>
{% for value, text in options %}
<option value="{{ value }}" {% if value == current_value %}selected{% endif %}>{{ text }}</option>
{% endfor %}
</select>
</div>
{% endmacro %}
{% macro render_date_filter_field(field_name, label, current_value) %}
<div class="form-group">
<label for="{{ field_name }}">{{ label }}</label>
<input type="date" class="form-control" id="{{ field_name }}" name="{{ field_name }}" value="{{ current_value }}">
</div>
{% endmacro %}
{% macro render_collapsible_section(id, title, content) %}
<div class="accordion" id="accordion{{ id }}">
<div class="accordion-item mb-3">
<h5 class="accordion-header" id="heading{{ id }}">
<button class="accordion-button border-bottom font-weight-bold collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse{{ id }}" aria-expanded="false" aria-controls="collapse{{ id }}">
{{ title }}
<i class="collapse-close fa fa-plus text-xs pt-1 position-absolute end-0 me-3" aria-hidden="true"></i>
<i class="collapse-open fa fa-minus text-xs pt-1 position-absolute end-0 me-3" aria-hidden="true"></i>
</button>
</h5>
<div id="collapse{{ id }}" class="accordion-collapse collapse" aria-labelledby="heading{{ id }}" data-bs-parent="#accordion{{ id }}">
<div class="accordion-body text-sm opacity-8">
{{ content }}
</div>
</div>
</div>
</div>
{% endmacro %}

View File

@@ -0,0 +1,14 @@
<!-- partials/message_exchange.html -->
<!-- User Message -->
<div class="message-container user-container">
<div class="message-bubble user-message">{{ query }}</div>
</div>
<!-- AI Response -->
<div class="message-container ai-container" id="response-{{ task_id }}">
<div class="message-bubble ai-message">
<div class="typing-indicator">
<span></span><span></span><span></span>
</div>
</div>
</div>

View File

@@ -0,0 +1,57 @@
<!-- Optional JavaScript -->
<!-- Public scripts ------------------------------------------------------------------------------>
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script src="https://cdn.datatables.net/1.10.21/js/jquery.dataTables.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/perfect-scrollbar.min.js"></script>
<script src="{{url_for('static', filename='assets/js/plugins/typedjs.js')}}"></script>
<script src="{{url_for('static', filename='assets/js/plugins/prism.min.js')}}"></script>
<script src="{{url_for('static', filename='assets/js/plugins/highlight.min.js')}}"></script>
<script src="{{url_for('static', filename='assets/js/plugins/parallax.min.js')}}"></script>
<script src="{{url_for('static', filename='assets/js/plugins/nouislider.min.js')}}"></script>
<script src="{{url_for('static', filename='assets/js/plugins/anime.min.js')}}"></script>
<script src="{{url_for('static', filename='assets/js/material-kit-pro.min.js')}}?v=3.0.4 type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/10.1.0/jsoneditor.min.css" rel="stylesheet" type="text/css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/10.1.0/jsoneditor.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.6/purify.min.js"></script>
<!-- Custom scripts ------------------------------------------------------------------------------>
<script src="{{url_for('static', filename='assets/js/console.js')}}"></script>
<script src="{{url_for('static', filename='assets/js/processing-status.js')}}"></script>
<!-- Marked Component ---------------------------------------------------------------------------->
<script>
// Configure Marked.js options
marked.setOptions({
gfm: true, // GitHub flavored markdown
breaks: true, // Interpret line breaks as <br>
headerIds: true, // Add ids to headings
mangle: false, // Don't escape HTML
pedantic: false, // Don't be overly conformant to original markdown
sanitize: false, // Don't sanitize HTML (use DOMPurify instead for security)
smartLists: true, // Use smarter list behavior
smartypants: true, // Use smart typography (quotes, dashes)
xhtml: false // Don't close single tags with />
});
// Function to safely convert markdown to HTML
function renderMarkdown(markdownText) {
if (typeof markdownText === 'string') {
try {
// Convert markdown to HTML, then sanitize it
const rawHtml = marked.parse(markdownText);
return DOMPurify.sanitize(rawHtml);
} catch (error) {
console.error('Error parsing markdown:', error);
return markdownText;
}
}
return markdownText;
}
</script>

View File

@@ -0,0 +1,392 @@
<div class="card border-0">
<div class="d-flex flex-column h-100 border-0 overflow-hidden specialist-container">
<!-- Configuration area - auto-sized based on content -->
<div class="specialist-arguments p-3 border-bottom config-section">
<h5 class="mb-3">Configuration</h5>
<form id="specialistConfigForm">
<input type="hidden" name="specialist_id" value="{{ specialist_id }}">
<div class="row">
{% for arg_name, arg_config in config.items() %}
{% if arg_name|lower != 'query' %}
<div class="col-md-4 mb-3">
<label for="{{ arg_name }}" class="form-label">
{{ arg_name }}
{% if arg_config.required %}
<span class="text-danger">*</span>
{% endif %}
</label>
{% if arg_config.type == 'bool' or arg_config.type == 'checkbox' %}
<div class="form-check form-switch">
<input type="checkbox" id="{{ arg_name }}" name="{{ arg_name }}"
class="form-check-input" value="true">
</div>
{% elif arg_config.type == 'select' and arg_config.options %}
<select id="{{ arg_name }}" name="{{ arg_name }}" class="form-select form-control">
{% for option in arg_config.options %}
<option value="{{ option.value }}">{{ option.label }}</option>
{% endfor %}
</select>
{% else %}
<input type="text" id="{{ arg_name }}" name="{{ arg_name }}"
class="form-control"
{% if arg_config.required %}required{% endif %}
placeholder="{{ arg_config.description }}">
{% endif %}
</div>
{% endif %}
{% endfor %}
</div>
</form>
</div>
<!-- Chat Interface - takes remaining height -->
<div class="chat-interface d-flex flex-column flex-grow-1 overflow-hidden">
<!-- Chat History - grows to fill available space -->
<div class="message-history p-3 flex-grow-1 overflow-auto" id="messageHistory">
<!-- Welcome message -->
<div class="message-container ai-container">
<div class="message-bubble ai-message markdown-content">
Hello! I'm {{ specialist.name }}. How can I help you today?
</div>
</div>
</div>
<!-- Message Input - fixed height at bottom -->
<div class="message-input p-3 border-top message-input-section">
<div class="input-group">
<textarea class="form-control" id="queryInput"
placeholder="Type your message here... (Shift+Enter for new line)"
rows="2"></textarea>
<button class="btn btn-primary" id="sendButton" type="button">
<i class="material-icons">send</i>
</button>
</div>
<!-- Hidden fields for data -->
<div id="hiddenData" style="display:none;">
<input type="hidden" id="specialistIdField" value="{{ specialist_id }}">
<input type="hidden" id="sessionIdField" value="">
</div>
</div>
</div>
</div>
</div>
<!-- Inline script directly after the relevant HTML -->
<script type="text/javascript">
// Use an immediately invoked function to avoid global scope issues
(function() {
console.log('Script starting execution');
function setupSSEConnection(taskId) {
console.log(`Setting up SSE connection for task ${taskId}`);
// Close any previous connections
if (window.activeEventSource) {
console.log('Closing previous SSE connection');
window.activeEventSource.close();
}
// Create new EventSource
const evtSource = new EventSource(`/specialist/process/${taskId}`);
window.activeEventSource = evtSource;
evtSource.onopen = function() {
console.log(`SSE connection opened for task ${taskId}`);
};
evtSource.onmessage = function(event) {
console.log('SSE message received:', event.data);
try {
const data = JSON.parse(event.data);
// Debug log viewer state
console.log('LogViewer state:', window.logViewer ? 'exists' : 'undefined');
// Update log viewer
if (window.logViewer) {
try {
if (data.type && typeof window.logViewer[data.type] === 'function') {
console.log(`Calling logViewer.${data.type} with message:`, data.message || JSON.stringify(data));
window.logViewer[data.type](data.message || JSON.stringify(data));
window.processingStatus.updateStatus("Processing", data.type)
} else {
console.log('Calling logViewer.info with message:', data.message || JSON.stringify(data));
window.logViewer.info(data.message || JSON.stringify(data));
window.processingStatus.processing('Processing')
}
} catch (logError) {
console.error('Error updating logViewer:', logError);
}
} else {
console.warn('LogViewer not available for message:', data);
}
// Update the AI response bubble with the content when complete
if (data.complete && data.content) {
const responseElement = document.getElementById(`response-${taskId}`);
if (responseElement) {
const messageBubble = responseElement.querySelector('.ai-message');
if (messageBubble) {
// Use Markdown rendering to format the content
messageBubble.innerHTML = renderMarkdown(data.content);
messageBubble.classList.add('markdown-content');
}
}
// Scroll to the bottom
messageHistory.scrollTop = messageHistory.scrollHeight;
}
} catch (error) {
console.error('Error processing SSE message:', error);
}
};
evtSource.onerror = function(error) {
console.error('SSE connection error:', error);
// Update log viewer
if (window.logViewer && typeof window.logViewer.error === 'function') {
window.logViewer.error('Connection error with specialist stream');
window.processingStatus.error('Disconnected')
}
// Clean up
if (window.activeEventSource === evtSource) {
evtSource.close();
window.activeEventSource = null;
}
};
// Clean up when page changes
window.addEventListener('beforeunload', function() {
if (window.activeEventSource === evtSource) {
evtSource.close();
window.activeEventSource = null;
}
});
}
// Basic function to make sure the script is running
function initializeUI() {
console.log('Initializing UI');
// Get references to DOM elements
var queryInput = document.getElementById('queryInput');
var sendButton = document.getElementById('sendButton');
var messageHistory = document.getElementById('messageHistory');
var specialistId = document.getElementById('specialistIdField').value;
var sessionIdField = document.getElementById('sessionIdField');
// Debug element existence
console.log('Query input found:', !!queryInput);
console.log('Send button found:', !!sendButton);
console.log('Message history found:', !!messageHistory);
console.log('Specialist ID:', specialistId);
console.log('LogViewer type check:', {
exists: typeof window.logViewer !== 'undefined',
isObject: typeof window.logViewer === 'object',
constructor: window.logViewer ? window.logViewer.constructor.name : 'unknown',
hasInfoMethod: window.logViewer && typeof window.logViewer.info === 'function',
methods: window.logViewer ? Object.getOwnPropertyNames(window.logViewer).filter(name =>
typeof window.logViewer[name] === 'function') : []
});
// Initialize session
initializeSession();
// Add event listeners
if (sendButton) {
sendButton.onclick = function() {
console.log('Send button clicked');
sendMessage();
};
}
if (queryInput) {
queryInput.onkeydown = function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
console.log('Enter key pressed');
e.preventDefault();
sendMessage();
}
};
}
// Start a session
function initializeSession() {
var xhr = new XMLHttpRequest();
xhr.open('GET', '{{ url_for("specialist.start_session") }}', true);
xhr.onload = function() {
if (xhr.status === 200) {
var response = JSON.parse(xhr.responseText);
console.log('Session initialized:', response.session_id);
sessionIdField.value = response.session_id;
} else {
console.error('Failed to initialize session');
}
};
xhr.onerror = function() {
console.error('Error initializing session');
};
xhr.send();
}
// Send message function
function sendMessage() {
console.log('Sending message');
// Prevent double execution
if (window.isSendingMessage) {
console.log('Already sending a message, ignoring');
return;
}
window.isSendingMessage = true;
// Get message text
var messageText = queryInput.value.trim();
if (!messageText) {
console.log('No message to send');
window.isSendingMessage = false;
return;
}
// Get form values
var configValues = getConfigValues();
// Remember original message
var originalMessage = messageText;
// Clear input
queryInput.value = '';
// Prepare form data
var formData = new FormData();
formData.append('specialist_id', specialistId);
formData.append('session_id', sessionIdField.value);
formData.append('query', messageText);
// Add config values
for (var key in configValues) {
formData.append(key, configValues[key]);
}
// Send request
var xhr = new XMLHttpRequest();
xhr.open('POST', '{{ url_for("specialist.execute_specialist") }}', true);
xhr.setRequestHeader('HX-Request', 'true');
xhr.onload = function() {
window.isSendingMessage = false;
if (xhr.status === 200) {
console.log('Response received');
// Add AI response HTML to the message history
messageHistory.innerHTML += xhr.responseText;
console.log('Added response HTML to message history');
// Extract the task_id from the response
const taskIdMatch = xhr.responseText.match(/id="response-([^"]+)"/);
if (taskIdMatch && taskIdMatch[1]) {
const taskId = taskIdMatch[1];
console.log(`Extracted task ID: ${taskId}`);
// Set up the SSE connection for this task
setupSSEConnection(taskId);
} else {
console.error('Could not extract task_id from response');
}
// Scroll to bottom
messageHistory.scrollTop = messageHistory.scrollHeight;
} else {
console.error('Request failed:', xhr.status);
addErrorMessage('Failed to process your request');
queryInput.value = originalMessage;
}
};
xhr.onerror = function() {
window.isSendingMessage = false;
console.error('Request error');
addErrorMessage('Request failed. Please try again.');
queryInput.value = originalMessage;
};
xhr.send(formData);
}
// Helper to add user message
function addUserMessage(text) {
var userMessageHtml =
'<div class="message-container user-container">' +
'<div class="message-bubble user-message">' + text + '</div>' +
'</div>';
messageHistory.innerHTML += userMessageHtml;
messageHistory.scrollTop = messageHistory.scrollHeight;
}
// Helper to add error message
function addErrorMessage(text) {
var errorMessageHtml =
'<div class="message-container ai-container">' +
'<div class="message-bubble ai-message" style="background-color: var(--bs-danger);">' +
'Error: ' + text + '</div>' +
'</div>';
messageHistory.innerHTML += errorMessageHtml;
messageHistory.scrollTop = messageHistory.scrollHeight;
}
// Get values from config form
function getConfigValues() {
var configForm = document.getElementById('specialistConfigForm');
var values = {};
if (configForm) {
var elements = configForm.elements;
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
if (element.name && element.name !== 'specialist_id') {
if (element.type === 'checkbox') {
values[element.name] = element.checked ? 'true' : 'false';
} else {
values[element.name] = element.value;
}
}
}
}
return values;
}
}
// Initialize the UI when DOM is fully loaded
if (document.readyState === 'complete' || document.readyState === 'interactive') {
// Document already ready, call immediately
console.log('Document already ready');
initializeUI();
} else {
// Set up event listener
document.addEventListener('DOMContentLoaded', function() {
console.log('DOMContentLoaded event fired');
initializeUI();
});
}
// Backup initialization in case DOMContentLoaded never fires
setTimeout(function() {
console.log('Backup initialization');
if (!window.uiInitialized) {
window.uiInitialized = true;
initializeUI();
}
}, 1000);
console.log('Script execution completed');
})();
</script>

View File

@@ -0,0 +1,57 @@
{% extends "base.html" %}
{% block title %}EveAI - Specialists{% endblock %}
{% block content %}
<div class="d-flex flex-row h-100" style="height: calc(100vh - 120px) !important;">
<!-- Fixed-width specialist selector -->
<div class="specialist-selector" style="width: 320px; overflow-y: auto; flex-shrink: 0;">
<div class="p-3">
{% for specialist in specialists %}
<div class="card mb-3 specialist-card"
hx-get="{{ url_for('specialist.get_specialist_form', specialist_id=specialist.id) }}"
hx-target="#specialist-form-container">
<div class="card-body p-3">
<h3 class="card-title mb-2">{{ specialist.name }}</h3>
<p class="card-text">{{ specialist.description }}</p>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Flexible specialist form area -->
<div class="flex-grow-1 d-flex flex-column overflow-hidden">
<div id="specialist-form-container" class="flex-grow-1 d-flex flex-column overflow-hidden">
<div class="card border-0 h-100 d-flex flex-column">
<div class="card-body d-flex align-items-center justify-content-center">
<p class="text-muted mb-0">Select a specialist to start</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Add click styling to specialist cards
const specialistCards = document.querySelectorAll('.specialist-card');
specialistCards.forEach(card => {
card.addEventListener('click', function() {
// Remove active class from all cards
specialistCards.forEach(c => {
c.classList.remove('bg-light');
c.classList.remove('active-specialist');
});
// Add active class to clicked card
this.classList.add('bg-light');
this.classList.add('active-specialist');
});
});
});
</script>
{% endblock %}