Files
lollms_hub/app/templates/admin/base.html
Saifeddine ALOUI eb5a10676d Refactor: Update API routes, data stores, and admin templates
This commit updates several API routes, data store definitions, and related templates for the admin section.

Changes include:
- Refactoring in `admin.py`, `datastores.py`, `evaluations.py`, and `proxy.py`.
- Updates to schema settings and static assets.
- Modifications to various admin templates (`base.html`, `dashboard.html`, etc.).
- Updates to agent management logic (`agentManager.ts`, `extensionState.ts`) and UI components.
2026-04-23 03:45:25 +02:00

1850 lines
118 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}LoLLMs Hub Admin{% endblock %}</title>
<link rel="icon" href="{{ url_for('static', path='favicon.svg') }}" type="image/svg+xml">
<link rel="shortcut icon" href="{{ url_for('static', path='favicon.svg') }}" type="image/svg+xml">
<!-- Redirect standard favicon request to our SVG -->
<link rel="icon" type="image/x-icon" href="{{ url_for('static', path='favicon.svg') }}">
<script src="{{ url_for('static', path='vendor/tailwindcss.js') }}"></script>
<script src="{{ url_for('static', path='vendor/chart.js') }}"></script>
<script src="{{ url_for('static', path='vendor/marked.min.js') }}"></script>
<script src="{{ url_for('static', path='vendor/js-yaml.min.js') }}"></script>
<script src="{{ url_for('static', path='vendor/mermaid.min.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', path='css/styles.css') }}">
<link rel="stylesheet" href="{{ url_for('static', path='vendor/highlight-dark.min.css') }}">
<script src="{{ url_for('static', path='vendor/highlight.min.js') }}"></script>
<style>
{% set theme_colors = settings.available_themes[settings.selected_theme] %}
:root {
--color-primary-500: {{ theme_colors['500'] }};
--color-primary-600: {{ theme_colors['600'] }};
--color-primary-700: {{ theme_colors['700'] }};
--color-primary-800: {{ theme_colors['800'] }};
}
</style>
</head>
<body class="font-sans leading-normal tracking-normal ui-{{ settings.ui_style }}">
<button id="sidebar-open-btn" class="fixed top-4 left-4 z-50 p-2 text-white rounded-md hover:opacity-80 shadow-lg">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
</button>
<div class="flex h-screen">
<div id="sidebar" class="w-64 min-w-[16rem] flex-shrink-0 flex flex-col justify-between transition-all duration-300 ease-in-out">
<div>
<div class="flex items-center justify-between p-4 border-b border-white/10 h-16">
<a href="{{ url_for('admin_dashboard') }}" class="flex items-center text-2xl font-bold whitespace-nowrap overflow-hidden">
{% if settings.branding_logo_url %}
<img src="{{ settings.branding_logo_url }}" alt="Logo" class="h-8 w-8 mr-2 object-contain flex-shrink-0">
{% endif %}
<span class="truncate">{{ settings.branding_title }}</span>
</a>
<button id="sidebar-close-btn" class="ml-2 flex-shrink-0">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
{% if request.state.user %}
<nav class="mt-4 pb-12 overflow-y-auto max-h-[calc(100vh-16rem)]">
<!-- GROUP: CORE -->
<div class="sidebar-group-label">Core System</div>
<a id="nav-dashboard" href="{{ url_for('admin_dashboard') }}" class="flex items-center px-4 py-3 whitespace-nowrap {% if 'dashboard' in request.url.path %}active{% endif %}">
<svg class="w-6 h-6 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 8v8m-4-5v5m-4-2v2m-2 4h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
Dashboard
</a>
<a id="nav-users" href="{{ url_for('admin_users') }}" class="flex items-center px-4 py-3 whitespace-nowrap {% if 'users' in request.url.path %}active{% endif %}">
<svg class="w-6 h-6 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M15 21a6 6 0 00-9-5.197M15 21a6 6 0 00-9-5.197"></path></svg>
User Management
</a>
<a id="nav-settings" href="{{ url_for('admin_settings') }}" class="flex items-center px-4 py-3 whitespace-nowrap {% if 'settings' in request.url.path %}active{% endif %}">
<svg class="w-6 h-6 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
Settings
</a>
{% if settings.enable_bot_mode %}
<a href="{{ url_for('admin_bots') }}" class="flex items-center px-4 py-3 whitespace-nowrap {% if 'bots' in request.url.path %}active{% endif %}">
<svg class="w-6 h-6 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path></svg>
Bot Connectors
</a>
{% endif %}
<!-- GROUP: INFRASTRUCTURE -->
<div id="label-infra" class="sidebar-group-label">Infrastructure</div>
<a href="{{ url_for('admin_servers') }}" class="flex items-center px-4 py-3 whitespace-nowrap {% if 'servers' in request.url.path %}active{% endif %}">
<svg class="w-6 h-6 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2-2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"></path></svg>
Server Management
</a>
<a href="{{ url_for('admin_instances') }}" class="flex items-center px-4 py-3 whitespace-nowrap {% if 'instances' in request.url.path %}active{% endif %}">
<svg class="w-6 h-6 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"></path></svg>
Instance Manager
</a>
<!-- GROUP: ORCHESTRATION -->
<div id="label-intel" class="sidebar-group-label">Intelligence Layer</div>
<a href="{{ url_for('admin_models_manager') }}" class="flex items-center px-4 py-3 whitespace-nowrap {% if 'models-manager' in request.url.path %}active{% endif %}">
<svg class="w-6 h-6 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 18.657A8 8 0 016.343 7.343S7 9 9 10c0-2 .5-5 2.986-7.014A8.003 8.003 0 0122 12c0 3.771-2.5 7-6.343 6.657zM2 6a8 8 0 1111.314 11.314L2 6z"></path></svg>
Models Manager
</a>
<a href="{{ url_for('admin_conception') }}" class="flex items-center px-4 py-3 whitespace-nowrap {% if 'conception' in request.url.path %}active{% endif %}">
<svg class="w-6 h-6 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z"></path></svg>
Workflow Architect
</a>
<a href="{{ url_for('admin_datastores') }}" class="flex items-center px-4 py-3 whitespace-nowrap {% if 'datastores' in request.url.path %}active{% endif %}">
<svg class="w-6 h-6 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"></path></svg>
Data Stores (RAG)
</a>
<a href="{{ url_for('admin_memory_systems') }}" class="flex items-center px-4 py-3 whitespace-nowrap {% if 'memory-systems' in request.url.path %}active{% endif %}">
<svg class="w-6 h-6 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg>
Memory Cores
</a>
<a href="{{ url_for('admin_skills') }}" class="flex items-center px-4 py-3 whitespace-nowrap {% if 'skills' in request.url.path %}active{% endif %}">
<svg class="w-6 h-6 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"></path></svg>
Skills Library
</a>
<a href="{{ url_for('admin_personalities') }}" class="flex items-center px-4 py-3 whitespace-nowrap {% if 'personalities' in request.url.path %}active{% endif %}">
<svg class="w-6 h-6 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
Personalities Studio
</a>
<a href="{{ url_for('admin_tools') }}" class="flex items-center px-4 py-3 whitespace-nowrap {% if 'tools' in request.url.path %}active{% endif %}">
<svg class="w-6 h-6 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
Tools Library
</a>
<!-- GROUP: LABS -->
<div class="sidebar-group-label">AI Playground</div>
<a id="nav-playground" href="{{ url_for('admin_playground') }}" class="flex items-center px-4 py-3 whitespace-nowrap {% if 'playground' in request.url.path and 'embedding' not in request.url.path %}active{% endif %}">
<svg class="w-6 h-6 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path></svg>
Chat Playground
</a>
<a id="nav-embedding" href="{{ url_for('admin_embedding_playground') }}" class="flex items-center px-4 py-3 whitespace-nowrap {% if 'embedding-playground' in request.url.path %}active{% endif %}">
<svg class="w-6 h-6 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 14v6m-3-3h6M3 10h2a2 2 0 012 2v2a2 2 0 01-2 2H3v-6zm0 6h2a2 2 0 002-2v-2a2 2 0 00-2-2H3v6zm6 0h2a2 2 0 002-2v-2a2 2 0 00-2-2H9v6z"></path></svg>
Embedding Benchmark
</a>
<a id="nav-evaluations" href="{{ url_for('admin_evaluations') }}" class="flex items-center px-4 py-3 whitespace-nowrap {% if 'evaluations' in request.url.path %}active{% endif %}">
<svg class="w-6 h-6 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012-2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path></svg>
Capabilities Evaluation
</a>
<!-- GROUP: MONITORING -->
<div class="sidebar-group-label">Monitoring</div>
<a href="{{ url_for('admin_live_status') }}" class="flex items-center px-4 py-3 whitespace-nowrap {% if 'live-status' in request.url.path %}active{% endif %}">
<svg class="w-6 h-6 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
Live System Flow
</a>
<a href="{{ url_for('admin_stats') }}" class="flex items-center px-4 py-3 whitespace-nowrap {% if 'stats' in request.url.path %}active{% endif %}">
<svg class="w-6 h-6 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012-2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path></svg>
Usage Statistics
</a>
<a href="{{ url_for('admin_logs') }}" class="flex items-center px-4 py-3 whitespace-nowrap {% if 'logs' in request.url.path %}active{% endif %}">
<svg class="w-6 h-6 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
System Logs
</a>
<!-- GROUP: INFO -->
<div class="sidebar-group-label">Support</div>
<a id="nav-help" href="{{ url_for('admin_help') }}" class="flex items-center px-4 py-3 whitespace-nowrap {% if 'help' in request.url.path %}active{% endif %}">
<svg class="w-6 h-6 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.79 4 4 0 2.21-1.79 4-4 4-1.742 0-3.223-.835-3.772-2M12 18.5v.01M12 5.5v.01"></path></svg>
Help & Credits
</a>
</nav>
{% endif %}
</div>
<div class="w-full flex-shrink-0">
{% if request.state.user %}
<div class="p-4 border-t border-white/10">
<div class="pb-3 mb-3 border-b border-white/10 text-xs text-gray-400">
<p>{{ settings.branding_title }} v{{ request.app.version }}</p>
<p class="truncate">Powered by ParisNeo's LoLLMs Hub Fortress</p>
</div>
<p class="text-sm whitespace-nowrap">Welcome, <span class="font-semibold">{{ request.state.user.username }}</span></p>
{% if is_redis_connected %}
<div class="flex items-center mt-2 text-xs text-green-400">
<span class="h-2 w-2 mr-2 bg-green-500 rounded-full"></span>
Redis: Connected
</div>
{% else %}
<a href="{{ url_for('admin_help') }}#redis-setup" class="flex items-center mt-2 text-xs text-red-400 hover:text-red-300 hover:underline">
<span class="h-2 w-2 mr-2 bg-red-500 rounded-full"></span>
Redis: Disconnected (Help)
</a>
{% endif %}
</div>
<a href="{{ url_for('admin_logout') }}" class="flex items-center w-full px-4 py-3 bg-red-600/50 hover:bg-red-600 whitespace-nowrap">
<svg class="w-6 h-6 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path></svg>
Logout
</a>
{% endif %}
</div>
</div>
<div class="flex-1 flex flex-col bg-transparent">
<main class="flex-1 p-6 md:p-10 overflow-y-auto">
{% include 'partials/_flash_messages.html' %}
<div class="max-w-7xl mx-auto">
<h1 class="text-2xl font-bold text-current mb-8">{% block header_title %}{% endblock %}</h1>
{% block content %}{% endblock %}
</div>
</main>
</div>
</div>
<!-- Help Tooltip Component -->
<style>
.help-tooltip { position: relative; display: inline-block; cursor: help; }
.help-tooltip:hover .help-text { opacity: 1; visibility: visible; transform: translateY(0); }
.help-text {
position: absolute; bottom: 125%; left: 50%; transform: translateX(-50%) translateY(10px);
width: 280px; padding: 12px; background: rgba(0,0,0,0.95); border: 1px solid var(--color-primary-600);
border-radius: 8px; font-size: 12px; line-height: 1.4; color: #e5e5e5;
opacity: 0; visibility: hidden; transition: all 0.2s ease; z-index: 100;
box-shadow: 0 10px 25px rgba(0,0,0,0.5);
}
.help-text::after {
content: ''; position: absolute; top: 100%; left: 50%; margin-left: -5px;
border-width: 5px; border-style: solid; border-color: var(--color-primary-600) transparent transparent transparent;
}
.help-icon { width: 16px; height: 16px; border-radius: 50%; background: var(--color-primary-600);
color: white; font-size: 11px; display: inline-flex; align-items: center; justify-content: center;
font-weight: bold; margin-left: 6px; }
</style>
<!-- Wizard Structure -->
<div id="wizard-overlay" class="fixed inset-0 bg-black/60 backdrop-blur-md z-[200] hidden transition-opacity duration-500"></div>
<div id="wizard-bubble" class="fixed z-[220] hidden w-80 bg-[#0a0a0f]/95 backdrop-blur-xl border-2 border-indigo-500 rounded-2xl shadow-[0_0_50px_rgba(0,0,0,0.9)] p-5 transition-all duration-300">
<div class="flex items-center gap-3 mb-3">
<div class="h-8 w-8 rounded-full bg-indigo-600 flex items-center justify-center text-white text-xs font-black">L</div>
<span class="text-xs font-black uppercase tracking-widest text-indigo-400">Hub Architect</span>
</div>
<div id="wizard-text" class="text-sm text-gray-300 leading-relaxed mb-4"></div>
<div class="flex justify-between items-center pt-2 border-t border-white/10">
<button id="wizard-skip" class="text-[10px] font-bold text-gray-500 hover:text-white uppercase">Skip Tour</button>
<div class="flex gap-2">
<button id="wizard-prev" class="px-3 py-1 bg-white/5 hover:bg-white/10 rounded text-[10px] font-bold text-gray-400 hidden">Back</button>
<button id="wizard-next" class="px-4 py-1 bg-indigo-600 hover:bg-indigo-500 rounded text-[10px] font-black uppercase text-white shadow-lg">Next Step</button>
</div>
</div>
<label class="flex items-center gap-2 cursor-pointer mt-3 pt-2 border-t border-white/5">
<input type="checkbox" id="wizard-never" class="h-3 w-3 rounded text-indigo-600 bg-black/40 border-white/10">
<span class="text-[9px] font-bold text-gray-500 uppercase tracking-tighter">Don't show this again</span>
</label>
</div>
<!-- Lollms Agent Summoning Zone -->
<div id="agent-zone-toggle" class="fixed bottom-6 right-6 z-[150] cursor-pointer group">
<div class="relative">
<div class="absolute inset-0 bg-indigo-600 rounded-full animate-ping opacity-20 group-hover:opacity-40"></div>
<div class="relative h-14 w-14 bg-indigo-600 rounded-full shadow-2xl flex items-center justify-center text-white border-2 border-white/20 transform transition-transform group-hover:scale-110 active:scale-95">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
</div>
</div>
</div>
<!-- Drawing Board UI (Canvas) -->
<div id="core-paint-panel" class="fixed inset-0 z-[250] bg-black/80 backdrop-blur-md flex items-center justify-center hidden">
<div class="bg-[#0f111a] border border-indigo-500/30 rounded-3xl p-6 shadow-2xl w-full max-w-2xl flex flex-col gap-4">
<div class="flex justify-between items-center border-b border-white/10 pb-4">
<h3 class="text-xs font-black text-indigo-400 uppercase tracking-widest" id="paint-title">Neural Sketchpad</h3>
<div class="flex items-center gap-1 bg-black/20 p-1 rounded-xl border border-white/5">
<button onclick="undoCorePaint()" class="p-2 hover:bg-white/5 rounded-lg text-gray-400" title="Undo (Ctrl+Z)"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"></path></svg></button>
<button onclick="redoCorePaint()" class="p-2 hover:bg-white/5 rounded-lg text-gray-400" title="Redo (Ctrl+Y)"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 10h-10a8 8 0 00-8 8v2M21 10l-6 6m6-6l-6-6"></path></svg></button>
<div class="w-px h-4 bg-white/10 mx-1"></div>
<button id="btn-paint-select" onclick="setPaintMode('select')" class="p-2 hover:bg-white/5 rounded-lg text-gray-400" title="Select Area"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"></path></svg></button>
<button onclick="copyPaintSelection()" class="p-2 hover:bg-white/5 rounded-lg text-gray-400" title="Copy (Ctrl+C)"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2"></path></svg></button>
<button onclick="pastePaintSelection()" class="p-2 hover:bg-white/5 rounded-lg text-gray-400" title="Paste (Ctrl+V)"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"></path></svg></button>
<div class="w-px h-4 bg-white/10 mx-1"></div>
<button onclick="document.getElementById('paint-insert-img').click()" class="p-2 hover:bg-white/5 rounded-lg text-indigo-400" title="Insert Image"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg></button>
<input type="file" id="paint-insert-img" class="hidden" accept="image/*" onchange="insertImageToPaint(event)">
</div>
<div class="flex gap-2">
<button onclick="clearCoreCanvas()" class="p-2 hover:bg-white/5 rounded-lg text-gray-500" title="Clear Canvas"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg></button>
<button onclick="closeCorePaint()" class="p-2 hover:bg-white/5 rounded-lg text-gray-400">&times;</button>
</div>
</div>
<div class="relative bg-white rounded-xl overflow-hidden cursor-crosshair h-[400px]">
<canvas id="core-drawing-canvas"></canvas>
</div>
<div class="flex justify-between items-center">
<div class="flex gap-4 items-center">
<input type="color" id="paint-color" value="#4f46e5" class="h-8 w-8 bg-transparent border-none cursor-pointer">
<input type="range" id="paint-size" min="1" max="50" value="5" class="w-32 h-1 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-indigo-500">
</div>
<button onclick="saveCorePaint()" class="bg-indigo-600 hover:bg-indigo-500 text-white px-8 py-2 rounded-xl font-black text-xs uppercase tracking-widest shadow-lg shadow-indigo-900/40 transition-all">Submit to Lollms</button>
</div>
</div>
</div>
<div id="agent-chat-panel" class="fixed bottom-24 right-6 w-96 h-[600px] bg-[#0a0a0f]/95 backdrop-blur-xl border border-indigo-500/30 rounded-2xl shadow-[0_20px_50px_rgba(0,0,0,0.5)] z-[160] flex flex-col hidden overflow-hidden transition-all duration-300 transform translate-y-4 opacity-0">
<div class="p-4 border-b border-white/10 bg-indigo-600/10 flex justify-between items-center">
<div class="flex items-center gap-3">
<div class="h-8 w-8 rounded-lg bg-indigo-600 flex items-center justify-center text-white font-black text-xs">L</div>
<div>
<h3 class="text-xs font-black text-white uppercase tracking-widest">Lollms Core</h3>
<div class="flex items-center gap-1.5">
<span class="h-1.5 w-1.5 rounded-full bg-green-500 animate-pulse"></span>
<span class="text-[9px] text-gray-400 font-bold uppercase">Sentient ROM Active</span>
</div>
</div>
</div>
<div class="flex items-center gap-2">
<button id="agent-reset" class="text-gray-500 hover:text-red-400 transition-colors" title="Reset Core Memory">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
</button>
<button id="agent-close" class="text-gray-500 hover:text-white transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
</button>
</div>
</div>
<div id="agent-messages" class="flex-grow overflow-y-auto p-4 space-y-4 custom-scrollbar text-xs">
<div class="bg-indigo-600/10 border border-indigo-500/20 p-3 rounded-xl text-indigo-200 leading-relaxed">
Greetings, Architect. I am <b>Lollms</b>. I am grounded in the Hub ROM. How can I assist with your cluster today?
</div>
</div>
<!-- Attachment Previews -->
<div id="agent-previews" class="px-3 py-1 flex flex-wrap gap-2 bg-black/20"></div>
<div class="p-3 border-t border-white/10 bg-black/40">
<div class="flex flex-col gap-2">
<div class="flex gap-2">
<button id="agent-attach-img" class="p-2 rounded hover:bg-white/5 text-gray-400" title="Attach Image"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg></button>
<button onclick="openCorePaint()" class="p-2 rounded hover:bg-white/5 text-indigo-400" title="Sketch something"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.5L15.232 5.232z"></path></svg></button>
<button id="agent-attach-doc" class="p-2 rounded hover:bg-white/5 text-gray-400" title="Attach Document"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg></button>
<input type="text" id="agent-input" placeholder="Ask Lollms..." class="flex-grow bg-black/40 border border-white/10 rounded-lg px-3 py-2 text-xs text-white outline-none focus:border-indigo-500">
<button id="agent-stop" class="hidden bg-red-600 hover:bg-red-500 px-3 py-2 rounded-lg text-white transition-all" title="Stop Generation">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
</button>
<button id="agent-send" class="bg-indigo-600 hover:bg-indigo-500 px-3 py-2 rounded-lg text-white transition-all">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path></svg>
</button>
</div>
<input type="file" id="agent-file-img" class="hidden" multiple accept="image/*">
<input type="file" id="agent-file-doc" class="hidden" multiple accept=".txt,.md,.pdf,.csv,.py,.json">
</div>
</div>
</div>
<!-- Global Task Notification Toast -->
<div id="global-notification" class="fixed top-6 right-6 z-[300] w-80 transform translate-x-full transition-transform duration-500 pointer-events-none">
<div class="bg-[#0f111a] border-l-4 border-indigo-500 shadow-2xl rounded-r-xl p-4 flex items-start gap-4 pointer-events-auto">
<div class="p-2 bg-indigo-600/20 rounded-lg text-indigo-400">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
</div>
<div class="flex-grow">
<div class="text-[10px] font-black text-gray-500 uppercase tracking-widest mb-1">System Task Update</div>
<div id="notif-text" class="text-xs text-white font-bold leading-tight">Task completed successfully.</div>
</div>
<button onclick="window.hideNotification()" class="text-gray-600 hover:text-white">&times;</button>
</div>
</div>
<!-- Modal Structure -->
<div id="modal-backdrop" class="fixed inset-0 bg-black bg-opacity-70 z-50 hidden"></div>
<div id="modal" class="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-50 card-style w-11/12 max-w-2xl hidden">
<div class="p-6">
<h3 id="modal-title" class="text-xl font-bold mb-4 truncate pr-8">Modal Title</h3>
<div id="modal-body" class="max-h-[70vh] overflow-y-auto custom-scrollbar pr-2">
<p>Modal content goes here.</p>
</div>
</div>
<div class="flex justify-end p-4 border-t border-white/10">
<button id="modal-close-btn" class="py-2 px-4 rounded-md shadow-sm text-sm font-medium text-white bg-[var(--color-primary-600)] hover:bg-[var(--color-primary-700)]">Close</button>
</div>
</div>
<script>
// --- GLOBAL NOTIFICATION ENGINE ---
window.showToast = (message, type = 'info') => {
const toast = document.getElementById('global-notification');
const textEl = document.getElementById('notif-text');
const iconEl = toast.querySelector('.bg-indigo-600\\/20');
// Set Color Scheme
const colors = {
success: { border: 'border-emerald-500', text: 'text-emerald-400', bg: 'bg-emerald-600/20' },
error: { border: 'border-red-500', text: 'text-red-400', bg: 'bg-red-600/20' },
info: { border: 'border-indigo-500', text: 'text-indigo-400', bg: 'bg-indigo-600/20' }
};
const theme = colors[type] || colors.info;
toast.firstElementChild.className = `bg-[#0f111a] border-l-4 ${theme.border} shadow-2xl rounded-r-xl p-4 flex items-start gap-4 pointer-events-auto`;
iconEl.className = `p-2 ${theme.bg} rounded-lg ${theme.text}`;
textEl.innerText = message;
toast.classList.remove('translate-x-full');
setTimeout(window.hideNotification, 5000);
};
window.hideNotification = () => {
document.getElementById('global-notification').classList.add('translate-x-full');
};
window.showErrorModal = (title, data) => {
const hasTrace = data.traceback && {{ settings.enable_debug_mode | tojson }};
const modalBody = `
<div class="space-y-4">
<div class="p-4 bg-red-900/20 border border-red-500/50 rounded-xl flex items-start gap-4">
<div class="p-2 bg-red-600 rounded-lg text-white">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>
</div>
<div>
<div class="text-xs font-black text-red-400 uppercase tracking-widest mb-1">Execution Error</div>
<p class="text-sm text-white font-bold">${data.error || data.detail || "An unknown error occurred."}</p>
${data.suggestion ? `<p class="text-xs text-red-300/70 mt-2 italic">${data.suggestion}</p>` : ''}
</div>
</div>
${hasTrace ? `
<div class="border-t border-white/5 pt-4">
<details class="group">
<summary class="flex items-center gap-2 text-[10px] font-black text-gray-500 uppercase cursor-pointer hover:text-white transition-colors">
<svg class="w-3 h-3 transition-transform group-open:rotate-90" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
Technical Debug Trace
</summary>
<div class="mt-3 relative">
<pre id="debug-trace-box" class="bg-black/60 p-4 rounded-lg font-mono text-[10px] text-red-300/80 overflow-x-auto custom-scrollbar border border-white/5 max-h-64">${data.traceback}</pre>
<button onclick="window.copyTraceToClipboard()" class="absolute top-2 right-2 bg-white/5 hover:bg-white/10 border border-white/10 px-2 py-1 rounded text-[9px] font-bold text-gray-400 uppercase">Copy Trace</button>
</div>
</details>
</div>
` : ''}
<div class="flex justify-end pt-4">
<button onclick="document.getElementById('modal-close-btn').click()" class="bg-white/5 hover:bg-white/10 px-8 py-2 rounded-xl text-xs font-black uppercase text-gray-400 transition-all">Dismiss</button>
</div>
</div>
`;
window.showModal(title, modalBody);
};
window.copyTraceToClipboard = () => {
const text = document.getElementById('debug-trace-box').innerText;
navigator.clipboard.writeText(text).then(() => {
const btn = event.target;
const original = btn.innerText;
btn.innerText = "COPIED ✓";
setTimeout(() => btn.innerText = original, 2000);
});
};
window.openKnowledgeImporter = (targetCallback) => {
const modalBody = `
<div class="space-y-6 text-left" id="k-importer-root">
<!-- Tab Navigation -->
<div class="flex flex-wrap gap-4 border-b border-white/10 pb-2" id="importer-tabs">
<button onclick="setImpTab('url')" class="imp-tab active flex items-center gap-1.5 text-xs font-bold text-indigo-400 border-b-2 border-indigo-500 pb-2">🌐 URL</button>
<button onclick="setImpTab('arxiv')" class="imp-tab flex items-center gap-1.5 text-xs font-bold text-gray-500 pb-2">📚 ArXiv</button>
<button onclick="setImpTab('youtube')" class="imp-tab flex items-center gap-1.5 text-xs font-bold text-gray-500 pb-2">🎬 YouTube</button>
<button onclick="setImpTab('wiki')" class="imp-tab flex items-center gap-1.5 text-xs font-bold text-gray-500 pb-2">📖 Wiki</button>
<button onclick="setImpTab('google')" class="imp-tab flex items-center gap-1.5 text-xs font-bold text-gray-500 pb-2">🔍 Google</button>
</div>
<!-- Panel: URL -->
<div id="p-imp-url" class="imp-panel space-y-4">
<div class="text-[11px] text-gray-500">Extract content from a website and save it as an artifact.</div>
<div>
<label class="block text-xs font-bold text-gray-400 uppercase mb-1">Target URL</label>
<input type="text" id="imp-url-val" placeholder="https://..." class="w-full p-2.5 bg-black/40 border border-white/10 rounded-lg text-sm">
</div>
<div class="flex items-center gap-4">
<div class="flex-grow">
<label class="block text-xs font-bold text-gray-400 uppercase mb-1">Scraping Depth</label>
<input type="number" id="imp-url-depth" value="0" class="w-24 p-2 bg-black/40 border border-white/10 rounded-lg text-sm">
</div>
<label class="flex items-center gap-2 mt-4 cursor-pointer">
<input type="checkbox" class="rounded bg-black border-white/20">
<span class="text-xs font-bold text-gray-400">AI Processing</span>
</label>
</div>
</div>
<!-- Panel: ArXiv -->
<div id="p-imp-arxiv" class="imp-panel hidden space-y-4">
<div class="grid grid-cols-1 gap-4">
<div>
<label class="block text-xs font-bold text-gray-400 uppercase mb-1">Search Query</label>
<input type="text" id="imp-arxiv-query" placeholder="Topic keywords..." class="w-full p-2.5 bg-black/40 border border-white/10 rounded-lg text-sm">
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-xs font-bold text-gray-400 uppercase mb-1">Author</label>
<input type="text" id="imp-arxiv-author" placeholder="Einstein" class="w-full p-2.5 bg-black/40 border border-white/10 rounded-lg text-sm">
</div>
<div>
<label class="block text-xs font-bold text-gray-400 uppercase mb-1">Year</label>
<input type="text" id="imp-arxiv-year" placeholder="2024" class="w-full p-2.5 bg-black/40 border border-white/10 rounded-lg text-sm">
</div>
</div>
<div class="flex justify-between items-center">
<div class="flex items-center gap-2">
<label class="text-xs text-gray-400">Count:</label>
<input type="number" id="imp-arxiv-count" value="5" class="w-16 p-1.5 bg-black/40 border border-white/10 rounded-lg text-sm">
</div>
<button onclick="runSearch('arxiv')" class="bg-indigo-600 px-6 py-2 rounded-lg text-xs font-black uppercase text-white shadow-lg">Search ArXiv</button>
</div>
</div>
</div>
<!-- Panel: YouTube -->
<div id="p-imp-youtube" class="imp-panel hidden space-y-4">
<div class="text-[11px] text-gray-500">Extract transcript from a YouTube video.</div>
<div>
<label class="block text-xs font-bold text-gray-400 uppercase mb-1">Video URL</label>
<input type="text" id="imp-yt-url" placeholder="https://www.youtube.com/watch?v=..." class="w-full p-2.5 bg-black/40 border border-white/10 rounded-lg text-sm">
</div>
<div>
<label class="block text-xs font-bold text-gray-400 uppercase mb-1">Transcript Language</label>
<select id="imp-yt-lang" class="w-full p-2.5 bg-black/40 border border-white/10 rounded-lg text-sm text-white">
<option value="en">English (en)</option>
<option value="fr">French (fr)</option>
<option value="es">Spanish (es)</option>
<option value="de">German (de)</option>
</select>
<div class="text-[9px] text-gray-500 mt-1 italic">The system will try to find this language or translate the original captions to it.</div>
</div>
</div>
<!-- Panel: Wiki -->
<div id="p-imp-wiki" class="imp-panel hidden space-y-4">
<div class="text-[11px] text-gray-500">Search Wikipedia for relevant knowledge.</div>
<div>
<label class="block text-xs font-bold text-gray-400 uppercase mb-1">Topic</label>
<input type="text" id="imp-wiki-query" placeholder="e.g. Quantum Computing" class="w-full p-2.5 bg-black/40 border border-white/10 rounded-lg text-sm">
</div>
<div class="flex justify-end">
<button onclick="runSearch('wiki')" class="bg-indigo-600 px-6 py-2 rounded-lg text-xs font-black uppercase text-white shadow-lg">Search Wiki</button>
</div>
</div>
<!-- Panel: Google -->
<div id="p-imp-google" class="imp-panel hidden space-y-4">
<div class="text-[11px] text-gray-500">Search the web using Google.</div>
<div>
<label class="block text-xs font-bold text-gray-400 uppercase mb-1">Search Terms</label>
<input type="text" id="imp-google-query" placeholder="e.g. latest advancements in fusion energy" class="w-full p-2.5 bg-black/40 border border-white/10 rounded-lg text-sm">
</div>
<div class="flex justify-end">
<button onclick="runSearch('google')" class="bg-indigo-600 px-6 py-2 rounded-lg text-xs font-black uppercase text-white shadow-lg">Search Google</button>
</div>
</div>
<!-- Results Area -->
<div id="imp-results-container" class="hidden border-t border-white/10 pt-4 mt-4">
<div id="imp-results-list" class="space-y-2 max-h-60 overflow-y-auto custom-scrollbar pr-2"></div>
</div>
<!-- Footer -->
<div class="flex justify-end gap-3 border-t border-white/10 pt-4">
<button onclick="document.getElementById('modal-close-btn').click()" class="px-6 py-2 bg-white/5 hover:bg-white/10 rounded-lg text-xs font-bold text-gray-400">Cancel</button>
<button id="imp-final-btn" onclick="executeFinalImport()" class="px-6 py-2 bg-indigo-500/40 text-indigo-200 rounded-lg text-xs font-bold transition-all">Import</button>
</div>
</div>
`;
window.showModal("Knowledge Importer", modalBody);
window.activeImporterCallback = targetCallback;
window.importSelection = new Set();
};
window.setImpTab = (tabId) => {
const target = document.getElementById(`p-imp-${tabId}`);
if (!target) return;
document.querySelectorAll('.imp-panel').forEach(p => p.classList.add('hidden'));
target.classList.remove('hidden');
document.querySelectorAll('.imp-tab').forEach(t => {
t.classList.remove('active', 'text-indigo-400', 'border-b-2', 'border-indigo-500');
t.classList.add('text-gray-500');
});
event.target.classList.add('active', 'text-indigo-400', 'border-b-2', 'border-indigo-500');
event.target.classList.remove('text-gray-500');
// ArXiv and URL can show import directly or search
const finalBtn = document.getElementById('imp-final-btn');
finalBtn.classList.replace('bg-indigo-600', 'bg-indigo-500/40');
document.getElementById('imp-results-container').classList.add('hidden');
};
window.runSearch = async (provider) => {
const resultsContainer = document.getElementById('imp-results-container');
const list = document.getElementById('imp-results-list');
const finalBtn = document.getElementById('imp-final-btn');
list.innerHTML = '<div class="text-center py-10 animate-pulse text-indigo-400 font-bold">Querying Nodes...</div>';
resultsContainer.classList.remove('hidden');
let body = { provider };
if (provider === 'arxiv') {
body.query = document.getElementById('imp-arxiv-query').value;
body.author = document.getElementById('imp-arxiv-author').value;
body.year = document.getElementById('imp-arxiv-year').value;
body.count = parseInt(document.getElementById('imp-arxiv-count').value);
} else if (provider === 'wiki') {
body.query = document.getElementById('imp-wiki-query').value;
body.provider = 'wikipedia';
} else if (provider === 'google') {
body.query = document.getElementById('imp-google-query').value;
}
try {
const resp = await fetch("/admin/api/importer/search", {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const data = await resp.json();
window.lastImporterResults = data;
if (!data || data.length === 0) {
list.innerHTML = '<div class="text-center py-10 text-gray-500 italic">No results found for this query.</div>';
finalBtn.classList.replace('bg-indigo-600', 'bg-indigo-500/40');
return;
}
list.innerHTML = data.map((item, i) => `
<div class="p-3 bg-white/5 border border-white/10 rounded-xl hover:border-indigo-500/30 transition-all group">
<div class="flex items-start gap-3">
<input type="checkbox" onchange="toggleImpSelection(${i})" class="mt-1 rounded border-white/20 bg-black">
<div class="flex-grow min-w-0">
<div class="text-[11px] font-black text-white uppercase truncate">${item.title}</div>
<div class="text-[10px] text-gray-500 line-clamp-2 mt-0.5">${item.snippet}</div>
<div class="flex gap-2 mt-2">
<button onclick="setArxivMode(${i}, false)" id="mode-abs-${i}" class="text-[9px] px-2 py-0.5 rounded bg-indigo-600 text-white font-bold">Abstract</button>
<button onclick="setArxivMode(${i}, true)" id="mode-full-${i}" class="text-[9px] px-2 py-0.5 rounded bg-white/5 text-gray-400 font-bold hover:bg-white/10">Full Text</button>
</div>
</div>
</div>
</div>
`).join('');
finalBtn.classList.replace('bg-indigo-500/40', 'bg-indigo-600');
finalBtn.classList.add('text-white');
} catch (e) {
list.innerHTML = `<div class="text-red-500 text-xs p-4">Error: ${e.message}</div>`;
}
};
window.toggleImpSelection = (idx) => {
if (window.importSelection.has(idx)) window.importSelection.delete(idx);
else window.importSelection.add(idx);
};
window.setArxivMode = (idx, isFull) => {
// This is a UI hint - we'll handle the actual fetch on 'Import'
document.getElementById(`mode-abs-${idx}`).classList.toggle('bg-indigo-600', !isFull);
document.getElementById(`mode-abs-${idx}`).classList.toggle('text-white', !isFull);
document.getElementById(`mode-full-${idx}`).classList.toggle('bg-indigo-600', isFull);
document.getElementById(`mode-full-${idx}`).classList.toggle('text-white', isFull);
// Mark the item data so executeFinalImport knows what to do
window.lastImporterResults[idx].wantsFull = isFull;
};
window.executeFinalImport = async () => {
const activeTab = document.querySelector('.imp-tab.active').getAttribute('onclick').match(/'([^']+)'/)[1];
const finalBtn = document.getElementById('imp-final-btn');
// --- Case A: Direct YouTube Import ---
if (activeTab === 'youtube') {
const url = document.getElementById('imp-yt-url').value;
const lang = document.getElementById('imp-yt-lang').value;
if (!url) return alert("Please enter a YouTube URL");
finalBtn.disabled = true;
finalBtn.innerText = "EXTRACTING SCRIPT...";
try {
const resp = await fetch("/admin/api/importer/search", {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: url, provider: 'youtube', language: lang })
});
const data = await resp.json();
if (resp.ok && Array.isArray(data) && data.length > 0) {
document.getElementById('modal-close-btn').click();
// Delay callback slightly to allow DOM cleanup
setTimeout(() => window.activeImporterCallback(data[0].title, data[0].content), 50);
} else {
const errorMsg = data.error || data.detail || "No transcript found for this video/language.";
alert("YouTube Error: " + errorMsg);
}
} catch (e) {
alert("Connection Error: " + e.message);
} finally {
finalBtn.disabled = false;
finalBtn.innerText = "Import";
}
return;
}
// --- Case B: Direct URL Scrape ---
if (activeTab === 'url') {
const url = document.getElementById('imp-url-val').value;
const depth = document.getElementById('imp-url-depth').value;
if(!url) return alert("Enter URL");
finalBtn.disabled = true; finalBtn.innerText = "SCRAPING...";
try {
const formData = new FormData();
formData.append("url", url);
formData.append("depth", depth);
const resp = await fetch("/admin/api/importer/scrape", { method: 'POST', body: formData });
const data = await resp.json();
document.getElementById('modal-close-btn').click();
setTimeout(() => window.activeImporterCallback(data.title || url, data.content), 50);
} catch(e) { alert(e.message); }
finally { finalBtn.disabled = false; finalBtn.innerText = "Import Web Page"; }
return;
}
// --- Case C: Search Selection (ArXiv, Wiki, Google) ---
const selectedIndices = Array.from(window.importSelection);
if (selectedIndices.length === 0) return alert("Please search and select items first");
document.getElementById('modal-close-btn').click();
for (const idx of selectedIndices) {
const item = window.lastImporterResults[idx];
if (item.is_url) {
// Automatically scrape selected URLs before importing
try {
const formData = new FormData();
formData.append("url", item.url);
formData.append("depth", 0);
const resp = await fetch("/admin/api/importer/scrape", { method: 'POST', body: formData });
const data = await resp.json();
window.activeImporterCallback(data.title || item.title, data.content);
} catch(e) {
console.error("Scrape failed for " + item.url, e);
window.activeImporterCallback(item.title, item.content);
}
} else {
window.activeImporterCallback(item.title, item.content);
}
}
};
window.switchImporterTab = (tab) => {
document.getElementById('imp-panel-url').classList.toggle('hidden', tab !== 'url');
document.getElementById('imp-panel-search').classList.toggle('hidden', tab !== 'search');
document.querySelectorAll('.imp-tab').forEach(t => t.classList.replace('bg-indigo-600', 'bg-white/5'));
event.target.classList.replace('bg-white/5', 'bg-indigo-600');
};
window.toggleArxivOptions = () => {
const provider = document.getElementById('imp-provider').value;
document.getElementById('imp-arxiv-options').classList.toggle('hidden', provider !== 'arxiv');
};
window.executeExternalSearch = async () => {
const query = document.getElementById('imp-search-query').value;
const provider = document.getElementById('imp-provider').value;
const fullContent = document.getElementById('imp-arxiv-full').checked;
const resultsEl = document.getElementById('imp-results');
resultsEl.innerHTML = '<div class="text-center p-4 text-[10px] animate-pulse">Consulting global nodes...</div>';
try {
const resp = await fetch("/admin/api/importer/search", {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, provider, full_content: fullContent })
});
const data = await resp.json();
window.lastImporterResults = data;
resultsEl.innerHTML = data.map((r, i) => `
<div class="p-3 bg-white/5 border border-white/10 rounded-lg flex justify-between items-start gap-4 hover:border-indigo-500/30 transition-colors">
<div class="min-w-0">
<div class="text-[10px] font-black text-indigo-400 uppercase tracking-tighter truncate">${r.title}</div>
<div class="text-[9px] text-gray-500 line-clamp-2 mt-1">${r.snippet}</div>
</div>
<button onclick="confirmImportSelection(${i})" class="flex-shrink-0 bg-indigo-600 text-white px-3 py-1.5 rounded text-[10px] font-black uppercase">Import</button>
</div>
`).join('');
} catch(e) {
resultsEl.innerHTML = `
<div class="p-4 bg-red-900/20 border border-red-500/50 rounded-lg text-red-400 text-[10px]">
<strong>Provider Error:</strong> ${e.message}
</div>`;
}
};
window.confirmImportSelection = (index) => {
const item = window.lastImporterResults[index];
window.activeImporterCallback(item.title, item.content);
// Logic moved to callback to allow UI switching
};
window.executeUrlImport = async () => {
const url = document.getElementById('imp-url-input').value;
const depth = document.getElementById('imp-url-depth').value;
if(!url) return alert("Enter URL");
const btn = event.target;
btn.disabled = true; btn.innerText = "SCRAPING...";
try {
const formData = new FormData();
formData.append("url", url);
formData.append("depth", depth);
const resp = await fetch("/admin/api/importer/scrape", { method: 'POST', body: formData });
const data = await resp.json();
window.activeImporterCallback(data.title || url, data.content);
document.getElementById('modal-close-btn').click();
} catch(e) { alert(e.message); }
finally { btn.disabled = false; btn.innerText = "Import Web Page"; }
};
document.addEventListener('DOMContentLoaded', () => {
const body = document.body;
const sidebar = document.getElementById('sidebar');
const openBtn = document.getElementById('sidebar-open-btn');
const closeBtn = document.getElementById('sidebar-close-btn');
const openSidebar = () => {
sidebar.classList.remove('collapsed');
body.classList.remove('sidebar-collapsed');
localStorage.setItem('sidebarState', 'open');
};
const closeSidebar = () => {
sidebar.classList.add('collapsed');
body.classList.add('sidebar-collapsed');
localStorage.setItem('sidebarState', 'closed');
};
openBtn.addEventListener('click', openSidebar);
closeBtn.addEventListener('click', closeSidebar);
if (localStorage.getItem('sidebarState') === 'closed') {
closeSidebar();
}
// --- Modal Logic ---
const modal = document.getElementById('modal');
const modalBackdrop = document.getElementById('modal-backdrop');
const modalCloseBtn = document.getElementById('modal-close-btn');
const openModal = () => {
modal.classList.remove('hidden');
modalBackdrop.classList.remove('hidden');
};
const closeModal = () => {
modal.classList.add('hidden');
modalBackdrop.classList.add('hidden');
};
modalCloseBtn.addEventListener('click', closeModal);
modalBackdrop.addEventListener('click', closeModal);
window.showModal = (title, body) => {
document.getElementById('modal-title').textContent = title;
document.getElementById('modal-body').innerHTML = body;
openModal();
};
// --- SHARED ARCHITECT MODULE (Skills, Personalities, Tools, Graphs) ---
window.Architect = {
context: [],
prompt: "",
activeController: null,
reset: function() {
this.context = [];
this.prompt = "";
this.activeController = null;
},
handleFiles: function(files, renderCallback) {
for (let f of files) this.context.push({ type: 'file', name: f.name, data: f });
renderCallback();
},
triggerWebImport: function(promptId, callback) {
const el = document.getElementById(promptId);
if (el) this.prompt = el.value;
window.openKnowledgeImporter((title, content) => {
this.context.push({ type: 'web', name: title, content: content });
callback(); // Re-open parent modal
});
},
renderPreviews: function(containerId) {
const container = document.getElementById(containerId);
if (!container) return;
container.innerHTML = this.context.length ? '' : '<span class="text-[9px] text-gray-700 italic py-1">No sources attached...</span>';
this.context.forEach((item, index) => {
const div = document.createElement('div');
div.className = "flex items-center gap-2 text-[9px] bg-white/5 p-1 px-2 rounded-lg border border-white/10 text-white";
div.innerHTML = `<span>${item.type === 'web' ? '🌍' : '📄'}</span><span class="font-bold truncate max-w-[100px]">${item.name}</span><button onclick="window.Architect.removeSource(${index}, '${containerId}')" class="text-red-400 font-bold ml-1">&times;</button>`;
container.appendChild(div);
});
},
removeSource: function(idx, containerId) {
this.context.splice(idx, 1);
this.renderPreviews(containerId);
},
prepareFormData: function(promptId) {
const formData = new FormData();
const el = document.getElementById(promptId);
let fullPrompt = el ? el.value : "";
// Capture Self-Healing Toggle (if exists in current modal)
const shlToggle = document.querySelector('input[id^="shl-"]');
if (shlToggle) {
formData.append("activate_self_healing", shlToggle.checked);
}
this.context.forEach(it => {
if (it.type === 'file') formData.append("files", it.data);
else fullPrompt += `\n\nSOURCE ${it.name}:\n${it.content}`;
});
formData.append("prompt", fullPrompt || "");
return formData;
},
executeBuild: async function(promptId, endpoint, statusBoxId, btnId, successCallback) {
const btn = document.getElementById(btnId);
const statusBox = document.getElementById(statusBoxId);
const promptEl = document.getElementById(promptId);
// --- STOP LOGIC ---
if (this.activeController) {
this.activeController.abort();
return;
}
if (!btn || !statusBox || !promptEl) {
console.error("Architect Error: DOM elements missing.");
return;
}
if (!promptEl.value.trim()) {
alert("Please enter a description for the Architect.");
return;
}
// --- START LOGIC ---
this.activeController = new AbortController();
const originalText = btn.innerText;
const originalBg = btn.className;
// Transition to Stop State
btn.innerHTML = `
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
STOP GENERATION
`;
btn.className = "w-full py-3 bg-red-600 hover:bg-red-500 rounded-xl font-black text-xs uppercase tracking-widest text-white transition-all";
statusBox.classList.remove('hidden');
statusBox.innerHTML = '<div>[System] Handshaking with AI Host...</div>';
const formData = this.prepareFormData(promptId);
// Retrieve CSRF token
let csrfToken = "";
const csrfEl = document.querySelector('input[name="csrf_token"]');
if (csrfEl) csrfToken = csrfEl.value;
if (!csrfToken) {
alert("Security error: Session token missing. Please reload.");
this.activeController = null;
btn.innerText = originalText;
btn.className = originalBg;
return;
}
formData.append("csrf_token", csrfToken);
try {
const resp = await fetch(endpoint, {
method: 'POST',
body: formData,
signal: this.activeController.signal
});
const data = await resp.json();
if (data.success) {
successCallback(data);
document.getElementById('modal-close-btn').click();
} else {
const errMsg = data.error || "Operation failed.";
statusBox.innerHTML += `<div class="text-red-400 font-bold">[Error] ${errMsg}</div>`;
}
} catch(e) {
if (e.name === 'AbortError') {
statusBox.innerHTML += `<div class="text-amber-500 italic">[System] Generation cancelled by user.</div>`;
} else {
console.error(e);
statusBox.innerHTML += `<div class="text-red-400 font-bold">[Network Error] ${e.message}</div>`;
}
} finally {
this.activeController = null;
btn.innerText = originalText;
btn.className = originalBg;
}
}
};
// --- FIRST TIME WIZARD LOGIC ---
const WIZARD_STEPS = [
{
target: null,
text: "Welcome to the <b>Fortress</b>, Architect. I am Lollms. I'll show you how to orchestrate your AI cluster."
},
{% if bootstrap_settings.ADMIN_PASSWORD == "changeme" %}
{
target: '#nav-settings',
is_security: true,
text: "<span class='text-red-400 font-black'>⚠️ SECURITY ALERT</span><br>Your admin password is set to the default! <b>Change it immediately</b> in Settings to secure your Fortress."
},
{% endif %}
{
target: '#nav-dashboard',
text: "The <b>Dashboard</b> is your mission control. Monitor system health, GPU VRAM usage, and our live carbon footprint."
},
{
target: '#label-infra',
text: "<b>Infrastructure</b> is where you connect the limbs. Add Ollama or vLLM servers, and manage local GPU processes."
},
{
target: '#label-intel',
text: "This is the <b>Intelligence Layer</b>. Here you define 'auto' routing logic, build cognitive graphs, and manage per-user memories."
},
{
target: '#nav-playground',
text: "Ready to test? Use the <b>Chat Playground</b> to interact with your cluster using images, docs, and real-time telemetry."
},
{
target: '#nav-help',
text: "Finally, check the <b>Multi-User Manuals</b> in Help to learn about absolute data isolation and tool persistence. Good luck."
}
];
let currentStep = 0;
let currentTourSteps = [];
let currentStorageKey = "";
window.showStep = function(idx) {
const overlay = document.getElementById('wizard-overlay');
const bubble = document.getElementById('wizard-bubble');
const text = document.getElementById('wizard-text');
const nextBtn = document.getElementById('wizard-next');
const prevBtn = document.getElementById('wizard-prev');
const step = currentTourSteps[idx];
if (!step) return window.endWizard();
overlay.classList.remove('hidden');
bubble.classList.remove('hidden');
text.innerHTML = step.text;
prevBtn.classList.toggle('hidden', idx === 0);
nextBtn.innerText = idx === currentTourSteps.length - 1 ? "FINISH" : "NEXT STEP";
document.querySelectorAll('.wizard-highlight-zone').forEach(el => {
el.classList.remove('wizard-highlight-zone', 'z-[210]', 'relative', 'bg-indigo-500/10');
el.style.zIndex = "";
});
if (step.target) {
const targetEl = document.querySelector(step.target);
if (targetEl) {
targetEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => {
targetEl.classList.add('wizard-highlight-zone', 'bg-indigo-500/10');
targetEl.style.zIndex = "210";
targetEl.style.position = "relative";
const rect = targetEl.getBoundingClientRect();
bubble.style.transform = "none";
bubble.style.top = `${rect.top + window.scrollY}px`;
bubble.style.left = `${rect.right + 20}px`;
// Prevent overflowing right
if (rect.right + 340 > window.innerWidth) {
bubble.style.left = `${rect.left - 340}px`;
}
}, 300);
}
} else {
bubble.style.top = "40%";
bubble.style.left = "50%";
bubble.style.transform = "translate(-50%, -50%)";
}
};
window.endWizard = function() {
document.getElementById('wizard-overlay').classList.add('hidden');
document.getElementById('wizard-bubble').classList.add('hidden');
document.querySelectorAll('.wizard-highlight-zone').forEach(el => {
el.classList.remove('wizard-highlight-zone');
el.style.zIndex = "";
});
if (currentStorageKey) localStorage.setItem(currentStorageKey, 'true');
if (document.getElementById('wizard-never').checked) {
localStorage.setItem('hub_wizard_complete', 'true');
}
};
window.startCustomTour = (steps, storageKey, tourSettingEnabled = true, force = false) => {
const globalToursEnabled = {{ settings.enable_tours | tojson }};
if (!globalToursEnabled && !force) return;
if (!tourSettingEnabled && !force) return;
if (localStorage.getItem(storageKey) && !force) return;
currentTourSteps = steps;
currentStorageKey = storageKey;
currentStep = 0;
// Re-bind controls to the current context
document.getElementById('wizard-next').onclick = () => { currentStep++; window.showStep(currentStep); };
document.getElementById('wizard-prev').onclick = () => { currentStep--; window.showStep(currentStep); };
document.getElementById('wizard-skip').onclick = window.endWizard;
window.showStep(0);
};
window.startWizard = () => {
localStorage.removeItem('hub_wizard_complete');
window.startCustomTour(ONBOARDING_TOUR, 'hub_wizard_complete', true, true);
};
// Global Onboarding steps for first-time use
const ONBOARDING_TOUR = [...WIZARD_STEPS];
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('start_tour') === 'true') {
window.startCustomTour(ONBOARDING_TOUR, 'hub_wizard_complete', true, true);
}
// --- LOLLMS AGENT SUMMONING LOGIC ---
const agentToggle = document.getElementById('agent-zone-toggle');
const agentPanel = document.getElementById('agent-chat-panel');
const agentClose = document.getElementById('agent-close');
const agentInput = document.getElementById('agent-input');
const agentSend = document.getElementById('agent-send');
const agentMessages = document.getElementById('agent-messages');
const agentReset = document.getElementById('agent-reset');
const CORE_HISTORY_KEY = 'lollms_core_history';
let agentHistory = JSON.parse(localStorage.getItem(CORE_HISTORY_KEY) || '[]');
const toggleAgent = () => {
const isHidden = agentPanel.classList.contains('hidden');
if (isHidden) {
agentPanel.classList.remove('hidden');
setTimeout(() => {
agentPanel.classList.remove('translate-y-4', 'opacity-0');
}, 10);
} else {
agentPanel.classList.add('translate-y-4', 'opacity-0');
setTimeout(() => agentPanel.classList.add('hidden'), 300);
}
};
agentToggle.addEventListener('click', () => {
toggleAgent();
renderCoreChat();
});
agentClose.addEventListener('click', toggleAgent);
// --- Lollms Universal LCP & Artifact Engine ---
window.ArtifactRegistry = {
items: new Map(), // key: filename -> {versions: [], type: '', currentIdx: 0}
register: function(tag) {
const match = tag.match(/name=["'](.*?)["']/) || tag.match(/title=["'](.*?)["']/);
const name = match ? match[1] : `artifact_${Date.now()}`;
const type = tag.match(/type=["'](.*?)["']/)?.at(1) || 'file';
const path = tag.match(/path=["'](.*?)["']/)?.at(1);
const content = tag.match(/>([\s\S]*?)<\/artifact>/)?.at(1);
if (!this.items.has(name)) {
this.items.set(name, { name, type, versions: [], currentIdx: 0 });
}
const item = this.items.get(name);
const versionData = {
ts: new Date().toLocaleTimeString(),
path: path || null,
content: content || null
};
// Only add if content/path is different from last version (Dedup)
const last = item.versions[item.versions.length - 1];
if (!last || last.content !== versionData.content || last.path !== versionData.path) {
item.versions.push(versionData);
item.currentIdx = item.versions.length - 1;
if (window.showToast) window.showToast(`Artifact Managed: ${name} (v${item.versions.length})`, 'success');
}
return name;
},
getHtml: function(name) {
const item = this.items.get(name);
if (!item) return `<div class="text-red-400">[Artifact ${name} missing]</div>`;
const ver = item.versions[item.currentIdx];
const vCount = item.versions.length;
let contentHtml = '';
if (item.type.startsWith('image') && ver.path) {
contentHtml = `<img src="${ver.path}" class="max-w-full rounded-lg border border-white/10 shadow-lg">`;
} else if (ver.path) {
contentHtml = `<div class="flex items-center gap-3 bg-black/40 p-3 rounded-xl border border-white/5">
<svg class="w-8 h-8 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
<div class="flex-grow min-w-0">
<div class="text-[10px] font-black text-gray-500 uppercase tracking-widest">Linked Asset</div>
<div class="text-xs font-bold text-white truncate">${name}</div>
</div>
<a href="${ver.path}" download class="p-2 bg-indigo-600 rounded-lg text-white hover:bg-indigo-500 transition-all"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg></a>
</div>`;
} else {
contentHtml = `<div class="bg-black/40 p-3 rounded-xl border border-white/5 max-h-40 overflow-y-auto custom-scrollbar font-mono text-[10px] text-emerald-400 whitespace-pre-wrap">${ver.content || '[Empty]'}</div>`;
}
return `
<div class="artifact-card my-4 bg-indigo-600/5 border border-indigo-500/20 rounded-2xl overflow-hidden shadow-2xl">
<div class="bg-indigo-600/10 px-4 py-2 flex justify-between items-center border-b border-indigo-500/10">
<div class="flex items-center gap-2">
<span class="text-[10px] font-black text-indigo-400 uppercase tracking-widest">Artifact: ${item.type}</span>
${vCount > 1 ? `<span class="bg-indigo-500 text-white text-[8px] font-black px-1.5 py-0.5 rounded-full">v${item.currentIdx + 1}/${vCount}</span>` : ''}
</div>
<div class="flex gap-2">
${vCount > 1 ? `<button onclick="window.ArtifactRegistry.cycle('${name}', -1)" class="p-1 hover:text-white text-gray-500"><svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg></button>` : ''}
${vCount > 1 ? `<button onclick="window.ArtifactRegistry.cycle('${name}', 1)" class="p-1 hover:text-white text-gray-500"><svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg></button>` : ''}
<button onclick="window.ArtifactRegistry.delete('${name}')" class="p-1 hover:text-red-400 text-gray-500"><svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg></button>
</div>
</div>
<div class="p-4">${contentHtml}</div>
</div>`;
},
cycle: function(name, dir) {
const item = this.items.get(name);
item.currentIdx = (item.currentIdx + dir + item.versions.length) % item.versions.length;
if (window.renderCoreChat) window.renderCoreChat();
if (window.renderChat) window.renderChat();
},
delete: function(name) {
if (confirm(`Purge artifact '${name}' from cluster memory?`)) {
this.items.delete(name);
if (window.renderCoreChat) window.renderCoreChat();
if (window.renderChat) window.renderChat();
}
}
};
window.LCPProcessor = {
langExtMap: {
python: 'py', py: 'py', javascript: 'js', js: 'js', typescript: 'ts',
ts: 'ts', html: 'html', css: 'css', json: 'json', markdown: 'md', md: 'md',
bash: 'sh', sh: 'sh', shell: 'sh', sql: 'sql', yaml: 'yaml', yml: 'yml',
dockerfile: 'Dockerfile', java: 'java', c: 'c', cpp: 'cpp', csharp: 'cs',
go: 'go', rust: 'rs', ruby: 'rb', php: 'php', perl: 'pl', xml: 'xml',
ini: 'ini', toml: 'toml', plaintext: 'txt', text: 'txt', txt: 'txt'
},
render: function(text, contextIdx = null) {
if (!text) return "";
const protectedBlocks = [];
const placeholderPrefix = `__LCP_PROTECT_${Date.now()}_`;
const protectedText = text.replace(/```[\s\S]*?```/g, (match) => {
protectedBlocks.push(match);
return `${placeholderPrefix}${protectedBlocks.length - 1}__`;
});
const lcpRegex = /(<(?:think|thought)>[\s\S]*?<\/(?:think|thought)>|<processing\b[^>]*>[\s\S]*?(?:<\/processing>|$)|<memory[^>]*>[\s\S]*?<\/memory>|<image_artifact[^>]*\/>|<artifact\b[^>]*>[\s\S]*?(?:<\/artifact>|\/>))/g;
const parts = protectedText.split(lcpRegex).filter(Boolean);
let resultHtml = '';
for (const part of parts) {
if (part.startsWith('<think') || part.startsWith('<thought')) {
const isClosed = part.endsWith('>') && (part.includes('</think>') || part.includes('</thought>'));
const inner = part.replace(/<\/?thought?>/g, '').trim();
resultHtml += `<details class="think-block my-2" ${isClosed ? '' : 'open'}><summary class="text-[10px] font-black uppercase text-indigo-400 cursor-pointer">Internal Reasoning</summary><div class="p-3 bg-black/20 rounded-lg mt-1 border-l-2 border-indigo-500/30 text-[11px] italic text-gray-400">${marked.parse(inner)}</div></details>`;
}
else if (part.startsWith('<processing')) {
const isClosed = part.includes('</processing>');
const title = part.match(/title=["'](.*?)["']/)?.at(1) || "System Processing";
const inner = part.replace(/<[^>]*>?/g, '').split('\n').filter(l => l.trim());
const status = inner.length > 0 ? inner[inner.length-1].replace(/^\*\s*/, '') : (isClosed ? "Finished" : "Working...");
html += `<div class="my-3 bg-indigo-600/5 border border-indigo-500/20 rounded-xl overflow-hidden max-w-sm shadow-xl">
<div class="bg-indigo-600/10 px-3 py-2 flex justify-between items-center border-b border-indigo-500/10">
<div class="flex flex-col min-w-0"><span class="text-[8px] font-black text-indigo-400 uppercase tracking-widest opacity-60">${title}</span><span class="text-[10px] font-bold text-white truncate">${status}</span></div>
${isClosed ? '<span class="text-[8px] text-emerald-500 font-bold">DONE</span>' : '<span class="text-[8px] text-amber-500 animate-pulse font-bold">ACTIVE</span>'}
</div>
${inner.length > 0 ? `<div class="p-2 px-3 font-mono text-[9px] text-gray-400 space-y-1 bg-black/20">${inner.map(l => `<div>• ${l.replace(/^\*\s*/, '')}</div>`).join('')}</div>` : ''}
</div>`;
}
else if (part.startsWith('<memory')) {
const op = part.match(/operation=["'](.*?)["']/)?.at(1) || "update";
const title = part.match(/title=["'](.*?)["']/)?.at(1) || "Fact";
html += `<div class="inline-flex items-center gap-2 px-2 py-1 bg-fuchsia-600/10 border border-fuchsia-500/30 rounded-lg text-[9px] font-black text-fuchsia-400 uppercase tracking-tighter my-1">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
Neural Link: ${title}
</div>`;
}
else if (part.startsWith('<artifact')) {
const name = window.ArtifactRegistry.register(part);
html += window.ArtifactRegistry.getHtml(name);
}
else if (part.startsWith('<image_artifact')) {
const src = part.match(/src=["'](.*?)["']/)?.at(1);
html += `<img src="${src}" class="max-w-full h-auto rounded-lg my-2 border border-white/10 shadow-lg cursor-pointer hover:ring-2 ring-indigo-500 transition-all" onclick="openCorePaint('${src}', ${contextIdx})">`;
}
else {
let restoredPart = part;
protectedBlocks.forEach((content, i) => {
restoredPart = restoredPart.replace(`${placeholderPrefix}${i}__`, content);
});
// Parse Markdown
let parsedMd = marked.parse(restoredPart);
// Post-process code blocks in the parsed string to add buttons
const tempDiv = document.createElement('div');
tempDiv.innerHTML = parsedMd;
tempDiv.querySelectorAll('.code-block-wrapper').forEach(wrapper => {
const codeEl = wrapper.querySelector('code');
if (!codeEl) return;
const lang = [...codeEl.classList].find(c => c.startsWith('language-'))?.replace('language-', '') || '';
const actions = document.createElement('div');
actions.className = 'playground-code-actions';
actions.innerHTML = `
<button class="playground-code-btn copy-code-btn" title="Copy code">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
</button>
<button class="playground-code-btn save-code-btn" title="Save as file">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
</button>
`;
wrapper.appendChild(actions);
});
resultHtml += tempDiv.innerHTML;
}
}
return resultHtml;
}
};
window.renderCoreChat = () => {
agentMessages.innerHTML = '';
if (agentHistory.length === 0) {
agentMessages.innerHTML = `<div class="bg-indigo-600/10 border border-indigo-500/20 p-3 rounded-xl text-indigo-200 leading-relaxed">Greetings, Architect. I am <b>Lollms</b>. I am grounded in the Hub ROM. How can I assist with your cluster today?</div>`;
}
agentHistory.forEach((msg, idx) => {
const div = document.createElement('div');
div.className = `relative group/msg mb-4 ${msg.role === 'user' ? 'bg-white/5 border border-white/10 p-3 rounded-xl ml-12 text-right text-gray-300' : 'bg-indigo-600/10 border border-indigo-500/20 p-3 rounded-xl mr-12 text-indigo-200'}`;
let rawContent = "";
let hasImage = false;
let lastImageUrl = "";
if (Array.isArray(msg.content)) {
msg.content.forEach(part => {
if (part.type === 'text') rawContent += part.text;
else if (part.type === 'image_url') {
hasImage = true;
lastImageUrl = part.image_url.url;
rawContent += `\n<image_artifact src="${lastImageUrl}" index="${idx}"/>\n`;
}
});
} else {
rawContent = msg.content || "";
// Basic string check for artifacts that contain image URLs
const imgMatch = rawContent.match(/<image_artifact src=["'](.*?)["']/);
if (imgMatch) {
hasImage = true;
lastImageUrl = imgMatch[1];
}
}
const finalHtml = window.LCPProcessor.render(rawContent, idx);
div.innerHTML = `
<div class="message-body text-left whitespace-pre-wrap">${finalHtml}</div>
<div class="absolute -top-3 ${msg.role === 'user' ? 'right-2' : 'left-2'} opacity-0 group-hover/msg:opacity-100 flex gap-1 bg-[#0a0a0f] border border-white/10 rounded-lg p-1 transition-opacity z-20 shadow-xl">
<button onclick="copyCoreMsg(${idx})" class="p-1 hover:text-white text-gray-500" title="Copy Text"><svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg></button>
<button onclick="editCoreMsg(${idx}, this)" class="p-1 hover:text-indigo-400 text-gray-500" title="Edit"><svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.5L15.232 5.232z"></path></svg></button>
${hasImage ? `<button onclick="openCorePaint('${lastImageUrl}')" class="p-1 hover:text-teal-400 text-gray-500" title="Edit Image"><svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg></button>` : ''}
<button onclick="regenCoreMsg(${idx})" class="p-1 hover:text-amber-400 text-gray-500" title="Regenerate from here"><svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg></button>
<button onclick="deleteCoreMsg(${idx})" class="p-1 hover:text-red-400 text-gray-500" title="Delete"><svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg></button>
</div>
`;
agentMessages.appendChild(div);
// Trigger highlighting for newly injected code blocks
div.querySelectorAll('pre code').forEach((block) => {
if (window.hljs) hljs.highlightElement(block);
});
});
agentMessages.scrollTop = agentMessages.scrollHeight;
};
window.copyCoreMsg = (idx) => {
const msg = agentHistory[idx];
let text = Array.isArray(msg.content) ? msg.content.find(p => p.type === 'text')?.text : msg.content;
navigator.clipboard.writeText(text || "");
window.showToast("Copied to clipboard", "info");
};
window.editCoreMsg = (idx, btn) => {
const msg = agentHistory[idx];
const bubble = btn.closest('.group\\/msg');
const body = bubble.querySelector('.message-body');
let originalText = Array.isArray(msg.content) ? msg.content.find(p => p.type === 'text')?.text : msg.content;
body.innerHTML = `
<textarea class="w-full bg-black/40 border border-indigo-500/30 rounded-lg p-2 text-xs text-white outline-none focus:ring-1 ring-indigo-500 custom-scrollbar" rows="5">${originalText}</textarea>
<div class="flex justify-end gap-2 mt-2">
<button onclick="renderCoreChat()" class="text-[9px] font-bold text-gray-500 uppercase">Cancel</button>
<button onclick="saveCoreEdit(${idx}, this)" class="text-[9px] font-black text-indigo-400 uppercase">Save Changes</button>
</div>
`;
body.querySelector('textarea').focus();
};
window.saveCoreEdit = (idx, btn) => {
const newText = btn.parentElement.parentElement.querySelector('textarea').value;
if (Array.isArray(agentHistory[idx].content)) {
const textPart = agentHistory[idx].content.find(p => p.type === 'text');
if (textPart) textPart.text = newText;
} else {
agentHistory[idx].content = newText;
}
localStorage.setItem(CORE_HISTORY_KEY, JSON.stringify(agentHistory));
renderCoreChat();
};
window.deleteCoreMsg = (idx) => {
agentHistory.splice(idx);
localStorage.setItem(CORE_HISTORY_KEY, JSON.stringify(agentHistory));
renderCoreChat();
};
window.regenCoreMsg = (idx) => {
const lastUserPrompt = agentHistory[idx-1]?.content;
if (lastUserPrompt) {
agentHistory.splice(idx);
runAgentSummon(lastUserPrompt);
}
};
agentReset.onclick = () => {
if(confirm("Wipe Core session history?")) {
agentHistory = [];
localStorage.removeItem(CORE_HISTORY_KEY);
renderCoreChat();
}
};
// --- MULTIMODAL AGENT LOGIC ---
let agentFiles = []; // Array of {type: 'img'|'doc', name, data/content}
const renderAgentPreviews = () => {
const container = document.getElementById('agent-previews');
container.innerHTML = '';
agentFiles.forEach((f, i) => {
const div = document.createElement('div');
div.className = "flex items-center gap-2 bg-indigo-600/20 border border-indigo-500/30 px-2 py-1 rounded text-[9px] text-indigo-300 max-w-[120px]";
div.innerHTML = `
<span class="truncate">${f.type === 'img' ? '📷' : '📄'} ${f.name}</span>
<button onclick="removeAgentFile(${i})" class="text-red-400 font-black">&times;</button>
`;
container.appendChild(div);
});
};
window.removeAgentFile = (idx) => {
agentFiles.splice(idx, 1);
renderAgentPreviews();
};
document.getElementById('agent-attach-img').onclick = () => document.getElementById('agent-file-img').click();
document.getElementById('agent-attach-doc').onclick = () => document.getElementById('agent-file-doc').click();
document.getElementById('agent-file-img').onchange = (e) => {
for (let f of e.target.files) {
const reader = new FileReader();
reader.onload = (ev) => {
agentFiles.push({ type: 'img', name: f.name, data: ev.target.result });
renderAgentPreviews();
};
reader.readAsDataURL(f);
}
};
document.getElementById('agent-file-doc').onchange = async (e) => {
for (let f of e.target.files) {
if (f.name.toLowerCase().endsWith('.pdf') || f.name.toLowerCase().endsWith('.docx')) {
// DEEP EXTRACTION: Use backend for binary documents
window.showToast(`Extracting ${f.name}...`, 'info');
const formData = new FormData();
formData.append('file', f);
formData.append('csrf_token', '{{ csrf_token }}');
try {
const resp = await fetch("{{ url_for('api_importer_extract_file') }}", { method: 'POST', body: formData });
const data = await resp.json();
if (data.content) {
agentFiles.push({ type: 'doc', name: f.name, content: data.content });
renderAgentPreviews();
}
} catch (err) { window.showToast("Extraction failed", "error"); }
} else {
// Standard text-based files handled locally
const reader = new FileReader();
reader.onload = (ev) => {
agentFiles.push({ type: 'doc', name: f.name, content: ev.target.result });
renderAgentPreviews();
};
reader.readAsText(f);
}
}
e.target.value = ""; // Reset for re-selection
};
let agentAbortController = null;
document.getElementById('agent-stop').onclick = () => {
if (agentAbortController) agentAbortController.abort();
};
// --- PAINT CORE LOGIC ---
let isDrawing = false;
let currentEditingImageIndex = null;
let paintMode = 'draw'; // 'draw' or 'select'
let paintHistory = [];
let paintHistoryIndex = -1;
let selectionRect = null; // {x, y, w, h}
let paintClipboard = null; // ImageData
const coreCanvas = document.getElementById('core-drawing-canvas');
const coreCtx = coreCanvas.getContext('2d');
const corePaintPanel = document.getElementById('core-paint-panel');
function pushPaintHistory() {
// Truncate redo stack
paintHistory = paintHistory.slice(0, paintHistoryIndex + 1);
paintHistory.push(coreCtx.getImageData(0, 0, coreCanvas.width, coreCanvas.height));
if (paintHistory.length > 20) paintHistory.shift();
else paintHistoryIndex++;
}
window.undoCorePaint = () => {
if (paintHistoryIndex > 0) {
paintHistoryIndex--;
coreCtx.putImageData(paintHistory[paintHistoryIndex], 0, 0);
}
};
window.redoCorePaint = () => {
if (paintHistoryIndex < paintHistory.length - 1) {
paintHistoryIndex++;
coreCtx.putImageData(paintHistory[paintHistoryIndex], 0, 0);
}
};
window.setPaintMode = (mode) => {
paintMode = mode;
document.getElementById('btn-paint-select').classList.toggle('bg-indigo-600/20', mode === 'select');
document.getElementById('btn-paint-select').classList.toggle('text-indigo-400', mode === 'select');
if (mode === 'draw') selectionRect = null;
};
window.openCorePaint = (bgUrl = null, msgIndex = null) => {
paintHistory = [];
paintHistoryIndex = -1;
currentEditingImageIndex = msgIndex;
corePaintPanel.classList.remove('hidden');
// Set size based on container
coreCanvas.width = coreCanvas.parentElement.clientWidth;
coreCanvas.height = coreCanvas.parentElement.clientHeight;
coreCtx.fillStyle = "white";
coreCtx.fillRect(0, 0, coreCanvas.width, coreCanvas.height);
if (bgUrl) {
document.getElementById('paint-title').innerText = "Editing Image Artifact";
const img = new Image();
img.onload = () => {
const scale = Math.min(coreCanvas.width / img.width, coreCanvas.height / img.height);
const x = (coreCanvas.width / 2) - (img.width / 2) * scale;
const y = (coreCanvas.height / 2) - (img.height / 2) * scale;
coreCtx.drawImage(img, x, y, img.width * scale, img.height * scale);
pushPaintHistory();
};
img.src = bgUrl;
} else {
document.getElementById('paint-title').innerText = "Neural Sketchpad";
pushPaintHistory();
}
};
let startX, startY;
coreCanvas.addEventListener('mousedown', (e) => {
const rect = coreCanvas.getBoundingClientRect();
startX = e.clientX - rect.left;
startY = e.clientY - rect.top;
isDrawing = true;
if (paintMode === 'draw') draw(e);
});
coreCanvas.addEventListener('mousemove', (e) => {
if (!isDrawing) return;
if (paintMode === 'draw') draw(e);
else {
// Draw selection marquee
const rect = coreCanvas.getBoundingClientRect();
const curX = e.clientX - rect.left;
const curY = e.clientY - rect.top;
// Refresh from last state before drawing box
coreCtx.putImageData(paintHistory[paintHistoryIndex], 0, 0);
coreCtx.setLineDash([5, 5]);
coreCtx.strokeStyle = "#4f46e5";
coreCtx.lineWidth = 1;
coreCtx.strokeRect(startX, startY, curX - startX, curY - startY);
coreCtx.setLineDash([]);
}
});
coreCanvas.addEventListener('mouseup', (e) => {
if (paintMode === 'select') {
const rect = coreCanvas.getBoundingClientRect();
selectionRect = { x: startX, y: startY, w: (e.clientX - rect.left) - startX, h: (e.clientY - rect.top) - startY };
} else {
pushPaintHistory();
}
isDrawing = false;
coreCtx.beginPath();
});
function draw(e) {
const rect = coreCanvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
coreCtx.lineWidth = document.getElementById('paint-size').value;
coreCtx.lineCap = "round";
coreCtx.strokeStyle = document.getElementById('paint-color').value;
coreCtx.lineTo(x, y);
coreCtx.stroke();
coreCtx.beginPath();
coreCtx.moveTo(x, y);
}
window.copyPaintSelection = () => {
if (!selectionRect) return;
paintClipboard = coreCtx.getImageData(selectionRect.x, selectionRect.y, selectionRect.w, selectionRect.h);
window.showToast("Area copied to local clipboard", "info");
};
window.pastePaintSelection = () => {
if (!paintClipboard) return;
coreCtx.putImageData(paintClipboard, 10, 10);
pushPaintHistory();
window.showToast("Stamping clipboard at 10,10", "info");
};
window.insertImageToPaint = (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
const img = new Image();
img.onload = () => {
coreCtx.drawImage(img, 0, 0, 150, 150); // Small stamp
pushPaintHistory();
// Clear the input so the same image can be inserted again
e.target.value = "";
};
img.src = ev.target.result;
};
reader.readAsDataURL(file);
};
// Keyboard Shortcuts for Paint
window.addEventListener('keydown', (e) => {
if (corePaintPanel.classList.contains('hidden')) return;
const ctrl = e.ctrlKey || e.metaKey;
if (ctrl) {
if (e.key === 'z') { e.preventDefault(); undoCorePaint(); }
if (e.key === 'y') { e.preventDefault(); redoCorePaint(); }
if (e.key === 'c') { e.preventDefault(); copyPaintSelection(); }
if (e.key === 'v') { e.preventDefault(); pastePaintSelection(); }
}
});
window.clearCoreCanvas = () => coreCtx.fillRect(0, 0, coreCanvas.width, coreCanvas.height);
window.closeCorePaint = () => corePaintPanel.classList.add('hidden');
// Default Save Handler (Lollms Core Side-Panel)
window.handleCorePaintSave = (dataUrl, msgIndex) => {
if (msgIndex !== null) {
const msg = agentHistory[msgIndex];
if (Array.isArray(msg.content)) {
const imgPart = msg.content.find(p => p.type === 'image_url');
if (imgPart) imgPart.image_url.url = dataUrl;
}
localStorage.setItem(CORE_HISTORY_KEY, JSON.stringify(agentHistory));
renderCoreChat();
} else {
agentFiles.push({ type: 'img', name: 'sketch.jpg', data: dataUrl });
renderAgentPreviews();
}
};
window.saveCorePaint = () => {
const dataUrl = coreCanvas.toDataURL('image/jpeg', 0.8);
// Allow pages (like Playground) to intercept the save event
if (window.customPaintSaveHandler) {
window.customPaintSaveHandler(dataUrl, currentEditingImageIndex);
} else {
window.handleCorePaintSave(dataUrl, currentEditingImageIndex);
}
window.showToast("Sketch processed", "success");
closeCorePaint();
};
const runAgentSummon = async (forcedPrompt = null) => {
const text = forcedPrompt || agentInput.value.trim();
if (!text && agentFiles.length === 0) return;
if (!forcedPrompt) {
agentInput.value = '';
// --- UX FIX: Reset physical file inputs ---
document.getElementById('agent-file-img').value = "";
document.getElementById('agent-file-doc').value = "";
let content = [];
if (text) content.push({ type: 'text', text: text });
// Attach Document Contents
const docContents = agentFiles.filter(f => f.type === 'doc').map(f => `FILE: ${f.name}\n\`\`\`\n${f.content}\n\`\`\``).join('\n\n');
if (docContents) {
if (content.length > 0) content[0].text = docContents + "\n\n" + content[0].text;
else content.push({ type: 'text', text: docContents });
}
// Attach Images
agentFiles.filter(f => f.type === 'img').forEach(f => {
content.push({ type: 'image_url', image_url: { url: f.data } });
});
agentHistory.push({role: 'user', content: content});
}
agentFiles = [];
renderAgentPreviews();
renderCoreChat();
const loadingDiv = document.createElement('div');
loadingDiv.className = 'text-[9px] font-black text-indigo-400 uppercase animate-pulse px-2';
loadingDiv.innerText = 'Consulting ROM...';
agentMessages.appendChild(loadingDiv);
agentAbortController = new AbortController();
agentSend.classList.add('hidden');
document.getElementById('agent-stop').classList.remove('hidden');
try {
const resp = await fetch("/api/chat", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + '{{ bootstrap_settings.SECRET_KEY }}'
},
body: JSON.stringify({
model: 'lollms',
messages: agentHistory,
stream: false
}),
signal: agentAbortController.signal
});
const data = await resp.json();
loadingDiv.remove();
if (!resp.ok) {
throw new Error(data.error || data.detail || `HTTP ${resp.status}`);
}
let answer = data.message?.content || data.response || "The agent processed the request but returned no text.";
// --- UI REMOTE CONTROL INTERCEPTION ---
if (answer.includes('<ui_move_to')) {
const path = answer.match(/path=['"](.*?)['"]/)[1];
setTimeout(() => window.location.href = path, 2000);
}
if (answer.includes('<ui_highlight')) {
const selector = answer.match(/selector=['"](.*?)['"]/)[1];
const el = document.querySelector(selector);
if (el) {
el.classList.add('wizard-highlight-zone');
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => el.classList.remove('wizard-highlight-zone'), 5000);
}
}
if (answer.includes('<ui_tour_start')) {
setTimeout(() => window.location.reload(), 1000);
}
agentHistory.push({role: 'assistant', content: answer});
localStorage.setItem(CORE_HISTORY_KEY, JSON.stringify(agentHistory));
renderCoreChat();
} catch (e) {
if (e.name === 'AbortController') {
loadingDiv.innerText = "GENERATION STOPPED";
} else {
loadingDiv.innerText = "ERROR: ROM OFFLINE";
}
} finally {
agentAbortController = null;
agentSend.classList.remove('hidden');
document.getElementById('agent-stop').classList.add('hidden');
}
};
agentSend.addEventListener('click', runAgentSummon);
agentInput.addEventListener('keydown', (e) => { if(e.key === 'Enter') runAgentSummon(); });
// Event listener for data-modal triggers
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-modal-title]');
if (target) {
const title = target.dataset.modalTitle;
const body = target.dataset.modalBody;
showModal(title, body);
}
});
});
</script>
</body>
</html>