Spaces:
Running
Running
Amlan-109
feat: Initial commit of LocalAI Amlan Edition with premium branding and personalization
750bbe6 | <html lang="en"> | |
| {{template "views/partials/head" .}} | |
| <body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]"> | |
| <div class="flex flex-col min-h-screen" x-data="agentJobs()" x-init="init()"> | |
| {{template "views/partials/navbar" .}} | |
| <div class="container mx-auto px-4 py-8 flex-grow"> | |
| <!-- Header --> | |
| <div class="hero-section"> | |
| <div class="hero-content flex justify-between items-center"> | |
| <div> | |
| <h1 class="hero-title"> | |
| Agent Jobs | |
| </h1> | |
| <p class="hero-subtitle">Manage agent tasks and monitor job execution</p> | |
| </div> | |
| <a href="/agent-jobs/tasks/new" class="bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] text-white px-6 py-3 rounded-lg transition-colors" x-show="hasMCPModels"> | |
| <i class="fas fa-plus mr-2"></i>Create Task | |
| </a> | |
| </div> | |
| </div> | |
| <!-- Wizard: No Models --> | |
| <div class="bg-[var(--color-bg-secondary)] border border-[var(--color-accent-border)]/20 rounded-xl p-12 mb-8" x-show="!hasModels"> | |
| <div class="text-center max-w-4xl mx-auto"> | |
| <div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-[var(--color-accent)]/10 border border-[var(--color-accent-border)]/20 mb-6"> | |
| <i class="text-[var(--color-accent)] text-2xl fas fa-robot"></i> | |
| </div> | |
| <h2 class="h2 mb-4"> | |
| No Models Installed | |
| </h2> | |
| <p class="text-xl text-[var(--color-text-secondary)] mb-8"> | |
| To use Agent Jobs, you need to install a model first. Agent Jobs require models with MCP (Model Context Protocol) configuration. | |
| </p> | |
| <!-- Features Preview --> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-10"> | |
| <div class="bg-[var(--color-bg-primary)] border border-[var(--color-primary-border)]/20 rounded-lg p-4"> | |
| <div class="w-10 h-10 bg-[var(--color-primary-light)] rounded-lg flex items-center justify-center mx-auto mb-3"> | |
| <i class="fas fa-images text-[var(--color-primary)] text-xl"></i> | |
| </div> | |
| <h3 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2">Model Gallery</h3> | |
| <p class="text-xs text-[var(--color-text-secondary)]">Browse and install pre-configured models</p> | |
| </div> | |
| <div class="bg-[var(--color-bg-primary)] border border-[var(--color-accent-border)]/20 rounded-lg p-4"> | |
| <div class="w-10 h-10 bg-[var(--color-accent-light)] rounded-lg flex items-center justify-center mx-auto mb-3"> | |
| <i class="fas fa-upload text-[var(--color-accent)] text-xl"></i> | |
| </div> | |
| <h3 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2">Import Models</h3> | |
| <p class="text-xs text-[var(--color-text-secondary)]">Upload your own model files</p> | |
| </div> | |
| <div class="bg-[var(--color-bg-primary)] border border-green-500/20 rounded-lg p-4"> | |
| <div class="w-10 h-10 bg-green-500/10 rounded-lg flex items-center justify-center mx-auto mb-3"> | |
| <i class="fas fa-code text-green-400 text-xl"></i> | |
| </div> | |
| <h3 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2">API Download</h3> | |
| <p class="text-xs text-[var(--color-text-secondary)]">Use the API to download models programmatically</p> | |
| </div> | |
| </div> | |
| <!-- Setup Instructions --> | |
| <div class="bg-[var(--color-bg-primary)] border border-[var(--color-accent-border)]/20 rounded-xl p-6 mb-8 text-left"> | |
| <h3 class="text-lg font-bold text-[var(--color-text-primary)] mb-4 flex items-center"> | |
| <i class="fas fa-rocket text-[var(--color-accent)] mr-2"></i> | |
| How to Get Started | |
| </h3> | |
| <div class="space-y-4"> | |
| <div class="flex items-start"> | |
| <div class="flex-shrink-0 w-8 h-8 rounded-full bg-[var(--color-accent)]/20 flex items-center justify-center mr-3 mt-0.5"> | |
| <span class="text-[var(--color-accent)] font-bold text-sm">1</span> | |
| </div> | |
| <div class="flex-1"> | |
| <p class="text-[var(--color-text-primary)] font-medium mb-2">Browse the Model Gallery</p> | |
| <p class="text-[var(--color-text-secondary)] text-sm">Explore our curated collection of pre-configured models. Find models for chat, image generation, audio processing, and more.</p> | |
| </div> | |
| </div> | |
| <div class="flex items-start"> | |
| <div class="flex-shrink-0 w-8 h-8 rounded-full bg-[var(--color-accent)]/20 flex items-center justify-center mr-3 mt-0.5"> | |
| <span class="text-[var(--color-accent)] font-bold text-sm">2</span> | |
| </div> | |
| <div class="flex-1"> | |
| <p class="text-[var(--color-text-primary)] font-medium mb-2">Install a Model</p> | |
| <p class="text-[var(--color-text-secondary)] text-sm">Click on a model from the gallery to install it, or use the import feature to upload your own model files.</p> | |
| </div> | |
| </div> | |
| <div class="flex items-start"> | |
| <div class="flex-shrink-0 w-8 h-8 rounded-full bg-[var(--color-accent)]/20 flex items-center justify-center mr-3 mt-0.5"> | |
| <span class="text-[var(--color-accent)] font-bold text-sm">3</span> | |
| </div> | |
| <div class="flex-1"> | |
| <p class="text-[var(--color-text-primary)] font-medium mb-2">Configure MCP</p> | |
| <p class="text-[var(--color-text-secondary)] text-sm">After installing a model, configure MCP (Model Context Protocol) to enable Agent Jobs functionality.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex flex-wrap justify-center gap-4"> | |
| <a href="/browse/" | |
| class="inline-flex items-center bg-[var(--color-accent)] hover:bg-[var(--color-accent)]/90 text-white py-3 px-6 rounded-lg font-semibold transition-colors"> | |
| <i class="fas fa-images mr-2"></i> | |
| Browse Model Gallery | |
| </a> | |
| <a href="/import-model" | |
| class="inline-flex items-center bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90 text-white py-3 px-6 rounded-lg font-semibold transition-colors"> | |
| <i class="fas fa-upload mr-2"></i> | |
| Import Model | |
| </a> | |
| <a href="https://localai.io/basics/getting_started/" target="_blank" | |
| class="inline-flex items-center bg-[var(--color-bg-secondary)] hover:bg-[var(--color-bg-secondary)]/80 border border-[var(--color-accent-border)]/20 text-[var(--color-text-primary)] py-3 px-6 rounded-lg font-semibold transition-colors"> | |
| <i class="fas fa-graduation-cap mr-2"></i> | |
| Getting Started | |
| <i class="fas fa-external-link-alt ml-2 text-sm"></i> | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Wizard: Models but No MCP --> | |
| <div class="bg-[var(--color-bg-secondary)] border border-yellow-500/20 rounded-xl p-12 mb-8" x-show="hasModels && !hasMCPModels"> | |
| <div class="text-center max-w-4xl mx-auto"> | |
| <div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-yellow-500/10 border border-yellow-500/20 mb-6"> | |
| <i class="text-yellow-500 text-2xl fas fa-exclamation-triangle"></i> | |
| </div> | |
| <h2 class="h2 mb-4"> | |
| MCP Configuration Required | |
| </h2> | |
| <p class="text-xl text-[var(--color-text-secondary)] mb-8"> | |
| You have models installed, but none have MCP (Model Context Protocol) enabled. Agent Jobs require MCP to function. | |
| </p> | |
| <!-- Available Models List --> | |
| <div class="bg-[var(--color-bg-primary)] border border-yellow-500/20 rounded-xl p-6 mb-8 text-left"> | |
| <h3 class="text-lg font-bold text-[var(--color-text-primary)] mb-4 flex items-center"> | |
| <i class="fas fa-list text-yellow-500 mr-2"></i> | |
| Available Models | |
| </h3> | |
| <div class="space-y-3"> | |
| <template x-for="model in availableModels" :key="model.name"> | |
| <div class="flex items-center justify-between p-3 bg-[#0A0E1A] rounded-lg border border-[var(--color-primary-border)]/10"> | |
| <div class="flex items-center space-x-3"> | |
| <i class="fas fa-cube text-[var(--color-primary)]"></i> | |
| <span class="text-[var(--color-text-primary)] font-medium" x-text="model.name"></span> | |
| </div> | |
| <a :href="'/models/edit/' + model.name" | |
| class="inline-flex items-center bg-yellow-600 hover:bg-yellow-700 text-white px-4 py-2 rounded-lg transition-colors text-sm"> | |
| <i class="fas fa-edit mr-2"></i> | |
| Configure MCP | |
| </a> | |
| </div> | |
| </template> | |
| </div> | |
| </div> | |
| <!-- Setup Instructions --> | |
| <div class="bg-[var(--color-bg-primary)] border border-yellow-500/20 rounded-xl p-6 mb-8 text-left"> | |
| <h3 class="text-lg font-bold text-[var(--color-text-primary)] mb-4 flex items-center"> | |
| <i class="fas fa-cog text-yellow-500 mr-2"></i> | |
| How to Enable MCP | |
| </h3> | |
| <div class="space-y-4"> | |
| <div class="flex items-start"> | |
| <div class="flex-shrink-0 w-8 h-8 rounded-full bg-yellow-500/20 flex items-center justify-center mr-3 mt-0.5"> | |
| <span class="text-yellow-500 font-bold text-sm">1</span> | |
| </div> | |
| <div class="flex-1"> | |
| <p class="text-[var(--color-text-primary)] font-medium mb-2">Edit a Model Configuration</p> | |
| <p class="text-[var(--color-text-secondary)] text-sm">Click "Configure MCP" on any model above, or navigate to the model editor to add MCP configuration.</p> | |
| </div> | |
| </div> | |
| <div class="flex items-start"> | |
| <div class="flex-shrink-0 w-8 h-8 rounded-full bg-yellow-500/20 flex items-center justify-center mr-3 mt-0.5"> | |
| <span class="text-yellow-500 font-bold text-sm">2</span> | |
| </div> | |
| <div class="flex-1"> | |
| <p class="text-[var(--color-text-primary)] font-medium mb-2">Add MCP Configuration</p> | |
| <p class="text-[var(--color-text-secondary)] text-sm">In the model YAML, add MCP server or stdio configuration. See the documentation for detailed examples.</p> | |
| </div> | |
| </div> | |
| <div class="flex items-start"> | |
| <div class="flex-shrink-0 w-8 h-8 rounded-full bg-yellow-500/20 flex items-center justify-center mr-3 mt-0.5"> | |
| <span class="text-yellow-500 font-bold text-sm">3</span> | |
| </div> | |
| <div class="flex-1"> | |
| <p class="text-[var(--color-text-primary)] font-medium mb-2">Save and Return</p> | |
| <p class="text-[var(--color-text-secondary)] text-sm">After saving the MCP configuration, return to this page to create your first Agent Job task.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex flex-wrap justify-center gap-4"> | |
| <a href="https://localai.io/features/mcp/" target="_blank" | |
| class="inline-flex items-center bg-yellow-600 hover:bg-yellow-700 text-white py-3 px-6 rounded-lg font-semibold transition-colors"> | |
| <i class="fas fa-book mr-2"></i> | |
| MCP Documentation | |
| <i class="fas fa-external-link-alt ml-2 text-sm"></i> | |
| </a> | |
| <a href="/manage" | |
| class="inline-flex items-center bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90 text-white py-3 px-6 rounded-lg font-semibold transition-colors"> | |
| <i class="fas fa-cog mr-2"></i> | |
| Manage Models | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Tasks Section --> | |
| <div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary-border)]/20 rounded-xl p-8 mb-8" x-show="hasMCPModels"> | |
| <h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Tasks</h2> | |
| <div class="overflow-x-auto"> | |
| <table class="w-full"> | |
| <thead> | |
| <tr class="border-b border-[var(--color-primary-border)]/20"> | |
| <th class="text-left py-3 px-4 text-[var(--color-text-secondary)]">Name</th> | |
| <th class="text-left py-3 px-4 text-[var(--color-text-secondary)]">Model</th> | |
| <th class="text-left py-3 px-4 text-[var(--color-text-secondary)]">Cron</th> | |
| <th class="text-left py-3 px-4 text-[var(--color-text-secondary)]">Status</th> | |
| <th class="text-left py-3 px-4 text-[var(--color-text-secondary)]">Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <template x-for="task in tasks" :key="task.id"> | |
| <tr class="border-b border-[var(--color-primary-border)]/10 hover:bg-[var(--color-bg-primary)]"> | |
| <td class="py-3 px-4"> | |
| <a :href="'/agent-jobs/tasks/' + task.id" | |
| class="font-semibold text-[var(--color-primary)] hover:text-[var(--color-primary)]/80 hover:underline" | |
| x-text="task.name"></a> | |
| <div class="text-sm text-[var(--color-text-secondary)]" x-text="task.description || 'No description'"></div> | |
| </td> | |
| <td class="py-3 px-4"> | |
| <div class="flex items-center space-x-2"> | |
| <a :href="'/chat/' + task.model + '?mcp=true'" | |
| class="text-[var(--color-primary)] hover:text-[var(--color-primary)]/80 hover:underline" | |
| x-text="task.model"></a> | |
| <a :href="'/models/edit/' + task.model" | |
| class="text-yellow-400 hover:text-yellow-300" | |
| title="Edit model configuration"> | |
| <i class="fas fa-edit text-sm"></i> | |
| </a> | |
| </div> | |
| </td> | |
| <td class="py-3 px-4"> | |
| <span x-show="task.cron" class="text-[var(--color-primary)]" x-text="task.cron"></span> | |
| <span x-show="!task.cron" class="text-[var(--color-text-secondary)]">-</span> | |
| </td> | |
| <td class="py-3 px-4"> | |
| <span :class="task.enabled ? 'bg-green-500' : 'bg-gray-500'" | |
| class="px-2 py-1 rounded text-xs text-white" | |
| x-text="task.enabled ? 'Enabled' : 'Disabled'"></span> | |
| </td> | |
| <td class="py-3 px-4"> | |
| <div class="flex space-x-2"> | |
| <button @click="showExecuteModal(task)" | |
| class="text-blue-400 hover:text-blue-300" | |
| title="Execute task"> | |
| <i class="fas fa-play"></i> | |
| </button> | |
| <a :href="'/agent-jobs/tasks/' + task.id + '/edit'" | |
| class="text-yellow-400 hover:text-yellow-300" | |
| title="Edit task"> | |
| <i class="fas fa-edit"></i> | |
| </a> | |
| <button @click="deleteTask(task.id)" | |
| class="text-red-400 hover:text-red-300" | |
| title="Delete task"> | |
| <i class="fas fa-trash"></i> | |
| </button> | |
| </div> | |
| </td> | |
| </tr> | |
| </template> | |
| <tr x-show="tasks.length === 0"> | |
| <td colspan="5" class="py-8 text-center text-[var(--color-text-secondary)]"> | |
| No tasks found. <a href="/agent-jobs/tasks/new" class="text-blue-400 hover:text-blue-300">Create one</a> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <!-- Jobs Section --> | |
| <div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary-border)]/20 rounded-xl p-8" x-show="hasMCPModels"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <h2 class="text-2xl font-semibold text-[var(--color-text-primary)]">Job History</h2> | |
| <div class="flex space-x-4"> | |
| <select x-model="jobFilter" @change="fetchJobs()" | |
| class="bg-[var(--color-bg-primary)] border border-[var(--color-primary-border)]/20 rounded px-4 py-2 text-[var(--color-text-primary)]"> | |
| <option value="">All Status</option> | |
| <option value="pending">Pending</option> | |
| <option value="running">Running</option> | |
| <option value="completed">Completed</option> | |
| <option value="failed">Failed</option> | |
| <option value="cancelled">Cancelled</option> | |
| </select> | |
| <button @click="clearJobHistory()" | |
| class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg transition-colors" | |
| title="Clear all job history"> | |
| <i class="fas fa-trash mr-2"></i>Clear History | |
| </button> | |
| </div> | |
| </div> | |
| <div class="overflow-x-auto"> | |
| <table class="w-full"> | |
| <thead> | |
| <tr class="border-b border-[var(--color-primary-border)]/20"> | |
| <th class="text-left py-3 px-4 text-[var(--color-text-secondary)]">Job ID</th> | |
| <th class="text-left py-3 px-4 text-[var(--color-text-secondary)]">Task</th> | |
| <th class="text-left py-3 px-4 text-[var(--color-text-secondary)]">Status</th> | |
| <th class="text-left py-3 px-4 text-[var(--color-text-secondary)]">Created</th> | |
| <th class="text-left py-3 px-4 text-[var(--color-text-secondary)]">Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <template x-for="job in jobs" :key="job.id"> | |
| <tr class="border-b border-[var(--color-primary-border)]/10 hover:bg-[var(--color-bg-primary)]"> | |
| <td class="py-3 px-4"> | |
| <a :href="'/agent-jobs/jobs/' + job.id" | |
| class="font-mono text-sm text-[var(--color-primary)] hover:text-[var(--color-primary)]/80 hover:underline" | |
| x-text="job.id.substring(0, 8) + '...'" | |
| :title="job.id"></a> | |
| </td> | |
| <td class="py-3 px-4"> | |
| <a :href="'/agent-jobs/tasks/' + job.task_id" | |
| class="text-[var(--color-primary)] hover:text-[var(--color-primary)]/80 hover:underline" | |
| x-text="getTaskName(job.task_id)" | |
| :title="'Task ID: ' + job.task_id"></a> | |
| </td> | |
| <td class="py-3 px-4"> | |
| <span :class="{ | |
| 'bg-yellow-500': job.status === 'pending', | |
| 'bg-blue-500': job.status === 'running', | |
| 'bg-green-500': job.status === 'completed', | |
| 'bg-red-500': job.status === 'failed', | |
| 'bg-gray-500': job.status === 'cancelled' | |
| }" | |
| class="px-2 py-1 rounded text-xs text-white" | |
| x-text="job.status"></span> | |
| </td> | |
| <td class="py-3 px-4 text-[var(--color-text-secondary)] text-sm" x-text="formatDate(job.created_at)"></td> | |
| <td class="py-3 px-4"> | |
| <button x-show="job.status === 'pending' || job.status === 'running'" | |
| @click="cancelJob(job.id)" | |
| class="text-red-400 hover:text-red-300"> | |
| <i class="fas fa-stop"></i> | |
| </button> | |
| </td> | |
| </tr> | |
| </template> | |
| <tr x-show="jobs.length === 0"> | |
| <td colspan="5" class="py-8 text-center text-[var(--color-text-secondary)]">No jobs found</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Execute Task Modal --> | |
| <div x-show="showExecuteTaskModal" | |
| x-cloak | |
| @click.away="showExecuteTaskModal = false; selectedTaskForExecution = null; executionParameters = {}; executionParametersText = ''" | |
| class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"> | |
| <div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary-border)]/20 rounded-xl max-w-2xl w-full mx-4 max-h-[90vh] flex flex-col"> | |
| <div class="flex justify-between items-center p-8 pb-6 border-b border-[var(--color-primary-border)]/20"> | |
| <h3 class="text-2xl font-semibold text-[var(--color-text-primary)]">Execute Task</h3> | |
| <button @click="showExecuteTaskModal = false; selectedTaskForExecution = null; executionParameters = {}; executionParametersText = ''; executionMultimedia = {images: '', videos: '', audios: '', files: ''}; executeModalTab = 'parameters'" | |
| class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]"> | |
| <i class="fas fa-times text-xl"></i> | |
| </button> | |
| </div> | |
| <template x-if="selectedTaskForExecution"> | |
| <div class="flex flex-col flex-1 min-h-0"> | |
| <div class="flex-1 overflow-y-auto px-8 py-6 space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Task</label> | |
| <div class="text-[var(--color-text-secondary)]" x-text="selectedTaskForExecution.name"></div> | |
| </div> | |
| <!-- Tabs for Parameters and Multimedia --> | |
| <div class="border-b border-[var(--color-primary-border)]/20"> | |
| <div class="flex space-x-4"> | |
| <button @click="executeModalTab = 'parameters'" | |
| :class="executeModalTab === 'parameters' ? 'border-b-2 border-[var(--color-primary)] text-[var(--color-primary)]' : 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'" | |
| class="px-4 py-2 font-medium transition-colors"> | |
| Parameters | |
| </button> | |
| <button @click="executeModalTab = 'multimedia'" | |
| :class="executeModalTab === 'multimedia' ? 'border-b-2 border-[var(--color-primary)] text-[var(--color-primary)]' : 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'" | |
| class="px-4 py-2 font-medium transition-colors"> | |
| Multimedia | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Parameters Tab --> | |
| <div x-show="executeModalTab === 'parameters'"> | |
| <label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Parameters</label> | |
| <p class="text-xs text-[var(--color-text-secondary)] mb-3"> | |
| Enter parameters as key-value pairs (one per line, format: key=value). | |
| These will be used to template the prompt. | |
| </p> | |
| <textarea x-model="executionParametersText" | |
| rows="6" | |
| placeholder="user_name=Alice job_title=Software Engineer task_description=Review code changes" | |
| class="w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary-border)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] font-mono text-sm focus:border-[var(--color-primary-border)] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea> | |
| <p class="text-xs text-[var(--color-text-secondary)] mt-1"> | |
| Example: <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">user_name=Alice</code> | |
| </p> | |
| </div> | |
| <!-- Multimedia Tab --> | |
| <div x-show="executeModalTab === 'multimedia'" class="space-y-4"> | |
| <p class="text-xs text-[var(--color-text-secondary)] mb-3"> | |
| Provide multimedia content as URLs or base64-encoded data URIs. You can also upload files which will be converted to base64. | |
| </p> | |
| <!-- Images --> | |
| <div> | |
| <label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Images</label> | |
| <textarea x-model="executionMultimedia.images" | |
| rows="3" | |
| placeholder="https://example.com/image.png data:image/png;base64,iVBORw0KG..." | |
| class="w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary-border)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] font-mono text-sm focus:border-[var(--color-primary-border)] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea> | |
| <input type="file" @change="handleFileUpload($event, 'image')" accept="image/*" multiple | |
| class="mt-2 text-sm text-[var(--color-text-secondary)] file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-[var(--color-primary)] file:text-white hover:file:bg-[var(--color-primary-hover)]"> | |
| </div> | |
| <!-- Videos --> | |
| <div> | |
| <label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Videos</label> | |
| <textarea x-model="executionMultimedia.videos" | |
| rows="3" | |
| placeholder="https://example.com/video.mp4 data:video/mp4;base64,..." | |
| class="w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary-border)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] font-mono text-sm focus:border-[var(--color-primary-border)] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea> | |
| <input type="file" @change="handleFileUpload($event, 'video')" accept="video/*" multiple | |
| class="mt-2 text-sm text-[var(--color-text-secondary)] file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-[var(--color-primary)] file:text-white hover:file:bg-[var(--color-primary-hover)]"> | |
| </div> | |
| <!-- Audios --> | |
| <div> | |
| <label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Audios</label> | |
| <textarea x-model="executionMultimedia.audios" | |
| rows="3" | |
| placeholder="https://example.com/audio.mp3 data:audio/mpeg;base64,..." | |
| class="w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary-border)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] font-mono text-sm focus:border-[var(--color-primary-border)] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea> | |
| <input type="file" @change="handleFileUpload($event, 'audio')" accept="audio/*" multiple | |
| class="mt-2 text-sm text-[var(--color-text-secondary)] file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-[var(--color-primary)] file:text-white hover:file:bg-[var(--color-primary-hover)]"> | |
| </div> | |
| <!-- Files --> | |
| <div> | |
| <label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Files</label> | |
| <textarea x-model="executionMultimedia.files" | |
| rows="3" | |
| placeholder="https://example.com/file.pdf data:application/pdf;base64,..." | |
| class="w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary-border)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] font-mono text-sm focus:border-[var(--color-primary-border)] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea> | |
| <input type="file" @change="handleFileUpload($event, 'file')" multiple | |
| class="mt-2 text-sm text-[var(--color-text-secondary)] file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-[var(--color-primary)] file:text-white hover:file:bg-[var(--color-primary-hover)]"> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex justify-end space-x-4 p-8 pt-6 border-t border-[var(--color-primary-border)]/20 bg-[var(--color-bg-secondary)]"> | |
| <button @click="showExecuteTaskModal = false; selectedTaskForExecution = null; executionParameters = {}; executionParametersText = ''; executionMultimedia = {images: '', videos: '', audios: '', files: ''}; executeModalTab = 'parameters'" | |
| class="px-4 py-2 bg-[var(--color-bg-primary)] hover:bg-[#0A0E1A] text-[var(--color-text-primary)] rounded-lg transition-colors"> | |
| Cancel | |
| </button> | |
| <button @click="executeTaskWithParameters()" | |
| class="px-4 py-2 bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] text-white rounded-lg transition-colors"> | |
| <i class="fas fa-play mr-2"></i>Execute | |
| </button> | |
| </div> | |
| </div> | |
| </template> | |
| </div> | |
| </div> | |
| <!-- Models Data (hidden, for JavaScript) --> | |
| <script id="models-data" type="application/json"> | |
| </script> | |
| <script> | |
| function agentJobs() { | |
| return { | |
| tasks: [], | |
| jobs: [], | |
| jobFilter: '', | |
| loading: false, | |
| showExecuteTaskModal: false, | |
| selectedTaskForExecution: null, | |
| executionParameters: {}, | |
| executionParametersText: '', | |
| executionMultimedia: { | |
| images: '', | |
| videos: '', | |
| audios: '', | |
| files: '' | |
| }, | |
| executeModalTab: 'parameters', | |
| modelsConfig: [], | |
| hasModels: false, | |
| hasMCPModels: false, | |
| availableModels: [], | |
| init() { | |
| // Check models from template data | |
| this.checkModels(); | |
| this.fetchTasks(); | |
| this.fetchJobs(); | |
| // Poll for job updates every 2 seconds | |
| setInterval(() => { | |
| this.fetchJobs(); | |
| }, 2000); | |
| }, | |
| checkModels() { | |
| // Get models from template data | |
| const modelsDataElement = document.getElementById('models-data'); | |
| let modelsData = []; | |
| if (modelsDataElement) { | |
| try { | |
| modelsData = JSON.parse(modelsDataElement.textContent); | |
| } catch (e) { | |
| console.error('Failed to parse models data:', e); | |
| } | |
| } | |
| this.modelsConfig = modelsData; | |
| this.hasModels = modelsData.length > 0; | |
| // Check for MCP-enabled models | |
| const mcpModels = modelsData.filter(m => m.hasMCP); | |
| this.hasMCPModels = mcpModels.length > 0; | |
| // Get available models (without MCP) for the wizard | |
| this.availableModels = modelsData.filter(m => !m.hasMCP); | |
| }, | |
| async fetchTasks() { | |
| try { | |
| const response = await fetch('/api/agent/tasks'); | |
| this.tasks = await response.json(); | |
| } catch (error) { | |
| console.error('Failed to fetch tasks:', error); | |
| } | |
| }, | |
| async fetchJobs() { | |
| try { | |
| let url = '/api/agent/jobs?limit=50'; | |
| if (this.jobFilter) { | |
| url += '&status=' + this.jobFilter; | |
| } | |
| const response = await fetch(url); | |
| this.jobs = await response.json(); | |
| } catch (error) { | |
| console.error('Failed to fetch jobs:', error); | |
| } | |
| }, | |
| showExecuteModal(task) { | |
| this.selectedTaskForExecution = task; | |
| this.executionParameters = {}; | |
| this.executionParametersText = ''; | |
| this.showExecuteTaskModal = true; | |
| }, | |
| parseParameters(text) { | |
| const params = {}; | |
| if (!text || !text.trim()) { | |
| return params; | |
| } | |
| const lines = text.split('\n'); | |
| for (const line of lines) { | |
| const trimmed = line.trim(); | |
| if (!trimmed) continue; | |
| const equalIndex = trimmed.indexOf('='); | |
| if (equalIndex > 0) { | |
| const key = trimmed.substring(0, equalIndex).trim(); | |
| const value = trimmed.substring(equalIndex + 1).trim(); | |
| if (key) { | |
| params[key] = value; | |
| } | |
| } | |
| } | |
| return params; | |
| }, | |
| async executeTaskWithParameters() { | |
| if (!this.selectedTaskForExecution) return; | |
| // Parse parameters from text | |
| this.executionParameters = this.parseParameters(this.executionParametersText); | |
| // Parse multimedia from text (split by newlines, filter empty) | |
| const parseMultimedia = (text) => { | |
| if (!text || !text.trim()) return []; | |
| return text.split('\n') | |
| .map(line => line.trim()) | |
| .filter(line => line.length > 0); | |
| }; | |
| const requestBody = { | |
| task_id: this.selectedTaskForExecution.id, | |
| parameters: this.executionParameters, | |
| images: parseMultimedia(this.executionMultimedia.images), | |
| videos: parseMultimedia(this.executionMultimedia.videos), | |
| audios: parseMultimedia(this.executionMultimedia.audios), | |
| files: parseMultimedia(this.executionMultimedia.files) | |
| }; | |
| try { | |
| const response = await fetch('/api/agent/jobs/execute', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(requestBody) | |
| }); | |
| if (response.ok) { | |
| this.showExecuteTaskModal = false; | |
| this.selectedTaskForExecution = null; | |
| this.executionParameters = {}; | |
| this.executionParametersText = ''; | |
| this.executionMultimedia = {images: '', videos: '', audios: '', files: ''}; | |
| this.executeModalTab = 'parameters'; | |
| this.fetchJobs(); | |
| } else { | |
| const error = await response.json(); | |
| alert('Failed to execute task: ' + (error.error || 'Unknown error')); | |
| } | |
| } catch (error) { | |
| console.error('Failed to execute task:', error); | |
| alert('Failed to execute task: ' + error.message); | |
| } | |
| }, | |
| handleFileUpload(event, type) { | |
| const files = event.target.files; | |
| if (!files || files.length === 0) return; | |
| const dataURIs = []; | |
| let processed = 0; | |
| for (let i = 0; i < files.length; i++) { | |
| const file = files[i]; | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| const dataURI = e.target.result; | |
| dataURIs.push(dataURI); | |
| processed++; | |
| if (processed === files.length) { | |
| // Append to existing content | |
| const current = this.executionMultimedia[type + 's'] || ''; | |
| const newContent = current ? current + '\n' + dataURIs.join('\n') : dataURIs.join('\n'); | |
| this.executionMultimedia[type + 's'] = newContent; | |
| } | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| }, | |
| async deleteTask(taskId) { | |
| if (!confirm('Are you sure you want to delete this task?')) return; | |
| try { | |
| const response = await fetch('/api/agent/tasks/' + taskId, { | |
| method: 'DELETE' | |
| }); | |
| if (response.ok) { | |
| this.fetchTasks(); | |
| } | |
| } catch (error) { | |
| console.error('Failed to delete task:', error); | |
| } | |
| }, | |
| viewJob(jobId) { | |
| window.location.href = '/agent-jobs/jobs/' + jobId; | |
| }, | |
| async cancelJob(jobId) { | |
| try { | |
| const response = await fetch('/api/agent/jobs/' + jobId + '/cancel', { | |
| method: 'POST' | |
| }); | |
| if (response.ok) { | |
| this.fetchJobs(); | |
| } | |
| } catch (error) { | |
| console.error('Failed to cancel job:', error); | |
| } | |
| }, | |
| formatDate(dateStr) { | |
| if (!dateStr) return '-'; | |
| const date = new Date(dateStr); | |
| return date.toLocaleString(); | |
| }, | |
| getTaskName(taskId) { | |
| const task = this.tasks.find(t => t.id === taskId); | |
| return task ? task.name : taskId.substring(0, 8) + '...'; | |
| }, | |
| async clearJobHistory() { | |
| if (!confirm('Are you sure you want to clear all job history? This action cannot be undone.')) return; | |
| try { | |
| // Get all jobs (with a high limit to get all) | |
| const response = await fetch('/api/agent/jobs?limit=10000'); | |
| if (response.ok) { | |
| const jobs = await response.json(); | |
| // Delete each job | |
| let deleted = 0; | |
| let failed = 0; | |
| for (const job of jobs) { | |
| try { | |
| const deleteResponse = await fetch('/api/agent/jobs/' + job.id, { | |
| method: 'DELETE' | |
| }); | |
| if (deleteResponse.ok) { | |
| deleted++; | |
| } else { | |
| failed++; | |
| } | |
| } catch (error) { | |
| console.error('Failed to delete job:', job.id, error); | |
| failed++; | |
| } | |
| } | |
| // Refresh job list | |
| this.fetchJobs(); | |
| if (failed > 0) { | |
| alert(`Cleared ${deleted} jobs. ${failed} jobs could not be deleted.`); | |
| } else { | |
| alert(`Successfully cleared ${deleted} jobs.`); | |
| } | |
| } | |
| } catch (error) { | |
| console.error('Failed to clear job history:', error); | |
| alert('Failed to clear job history: ' + error.message); | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| </div> | |
| </body> | |
| </html> | |