feat(actions): add playground to test actions (#74)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
committed by
GitHub
parent
b42ef27641
commit
d689bb4331
@@ -3,6 +3,7 @@ package services
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/mudler/LocalAgent/core/action"
|
"github.com/mudler/LocalAgent/core/action"
|
||||||
"github.com/mudler/LocalAgent/core/state"
|
"github.com/mudler/LocalAgent/core/state"
|
||||||
@@ -70,55 +71,70 @@ func Actions(a *state.AgentConfig) func(ctx context.Context, pool *state.AgentPo
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
switch a.Name {
|
a, err := Action(a.Name, config, pool)
|
||||||
case ActionCustom:
|
|
||||||
customAction, err := action.NewCustom(config, "")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error("Error creating custom action", "error", err)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
allActions = append(allActions, customAction)
|
allActions = append(allActions, a)
|
||||||
case ActionGenerateImage:
|
|
||||||
allActions = append(allActions, actions.NewGenImage(config))
|
|
||||||
case ActionSearch:
|
|
||||||
allActions = append(allActions, actions.NewSearch(config))
|
|
||||||
case ActionGithubIssueLabeler:
|
|
||||||
allActions = append(allActions, actions.NewGithubIssueLabeler(ctx, config))
|
|
||||||
case ActionGithubIssueOpener:
|
|
||||||
allActions = append(allActions, actions.NewGithubIssueOpener(ctx, config))
|
|
||||||
case ActionGithubIssueCloser:
|
|
||||||
allActions = append(allActions, actions.NewGithubIssueCloser(ctx, config))
|
|
||||||
case ActionGithubIssueSearcher:
|
|
||||||
allActions = append(allActions, actions.NewGithubIssueSearch(ctx, config))
|
|
||||||
case ActionGithubIssueReader:
|
|
||||||
allActions = append(allActions, actions.NewGithubIssueReader(ctx, config))
|
|
||||||
case ActionGithubIssueCommenter:
|
|
||||||
allActions = append(allActions, actions.NewGithubIssueCommenter(ctx, config))
|
|
||||||
case ActionGithubRepositoryGet:
|
|
||||||
allActions = append(allActions, actions.NewGithubRepositoryGetContent(ctx, config))
|
|
||||||
case ActionGithubRepositoryCreateOrUpdate:
|
|
||||||
allActions = append(allActions, actions.NewGithubRepositoryCreateOrUpdateContent(ctx, config))
|
|
||||||
case ActionGithubREADME:
|
|
||||||
allActions = append(allActions, actions.NewGithubRepositoryREADME(ctx, config))
|
|
||||||
case ActionScraper:
|
|
||||||
allActions = append(allActions, actions.NewScraper(config))
|
|
||||||
case ActionWikipedia:
|
|
||||||
allActions = append(allActions, actions.NewWikipedia(config))
|
|
||||||
case ActionBrowse:
|
|
||||||
allActions = append(allActions, actions.NewBrowse(config))
|
|
||||||
case ActionSendMail:
|
|
||||||
allActions = append(allActions, actions.NewSendMail(config))
|
|
||||||
case ActionTwitterPost:
|
|
||||||
allActions = append(allActions, actions.NewPostTweet(config))
|
|
||||||
case ActionCounter:
|
|
||||||
allActions = append(allActions, actions.NewCounter(config))
|
|
||||||
case ActionCallAgents:
|
|
||||||
allActions = append(allActions, actions.NewCallAgent(config, pool))
|
|
||||||
case ActionShellcommand:
|
|
||||||
allActions = append(allActions, actions.NewShell(config))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return allActions
|
return allActions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Action(name string, config map[string]string, pool *state.AgentPool) (agent.Action, error) {
|
||||||
|
var a agent.Action
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch name {
|
||||||
|
case ActionCustom:
|
||||||
|
a, err = action.NewCustom(config, "")
|
||||||
|
case ActionGenerateImage:
|
||||||
|
a = actions.NewGenImage(config)
|
||||||
|
case ActionSearch:
|
||||||
|
a = actions.NewSearch(config)
|
||||||
|
case ActionGithubIssueLabeler:
|
||||||
|
a = actions.NewGithubIssueLabeler(context.Background(), config)
|
||||||
|
case ActionGithubIssueOpener:
|
||||||
|
a = actions.NewGithubIssueOpener(context.Background(), config)
|
||||||
|
case ActionGithubIssueCloser:
|
||||||
|
a = actions.NewGithubIssueCloser(context.Background(), config)
|
||||||
|
case ActionGithubIssueSearcher:
|
||||||
|
a = actions.NewGithubIssueSearch(context.Background(), config)
|
||||||
|
case ActionGithubIssueReader:
|
||||||
|
a = actions.NewGithubIssueReader(context.Background(), config)
|
||||||
|
case ActionGithubIssueCommenter:
|
||||||
|
a = actions.NewGithubIssueCommenter(context.Background(), config)
|
||||||
|
case ActionGithubRepositoryGet:
|
||||||
|
a = actions.NewGithubRepositoryGetContent(context.Background(), config)
|
||||||
|
case ActionGithubRepositoryCreateOrUpdate:
|
||||||
|
a = actions.NewGithubRepositoryCreateOrUpdateContent(context.Background(), config)
|
||||||
|
case ActionGithubREADME:
|
||||||
|
a = actions.NewGithubRepositoryREADME(context.Background(), config)
|
||||||
|
case ActionScraper:
|
||||||
|
a = actions.NewScraper(config)
|
||||||
|
case ActionWikipedia:
|
||||||
|
a = actions.NewWikipedia(config)
|
||||||
|
case ActionBrowse:
|
||||||
|
a = actions.NewBrowse(config)
|
||||||
|
case ActionSendMail:
|
||||||
|
a = actions.NewSendMail(config)
|
||||||
|
case ActionTwitterPost:
|
||||||
|
a = actions.NewPostTweet(config)
|
||||||
|
case ActionCounter:
|
||||||
|
a = actions.NewCounter(config)
|
||||||
|
case ActionCallAgents:
|
||||||
|
a = actions.NewCallAgent(config, pool)
|
||||||
|
case ActionShellcommand:
|
||||||
|
a = actions.NewShell(config)
|
||||||
|
default:
|
||||||
|
xlog.Error("Action not found", "name", name)
|
||||||
|
return nil, fmt.Errorf("Action not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|||||||
44
webui/app.go
44
webui/app.go
@@ -1,6 +1,7 @@
|
|||||||
package webui
|
package webui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -9,8 +10,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mudler/LocalAgent/pkg/xlog"
|
"github.com/mudler/LocalAgent/pkg/xlog"
|
||||||
|
"github.com/mudler/LocalAgent/services"
|
||||||
"github.com/mudler/LocalAgent/webui/types"
|
"github.com/mudler/LocalAgent/webui/types"
|
||||||
|
|
||||||
|
"github.com/mudler/LocalAgent/core/action"
|
||||||
"github.com/mudler/LocalAgent/core/agent"
|
"github.com/mudler/LocalAgent/core/agent"
|
||||||
"github.com/mudler/LocalAgent/core/sse"
|
"github.com/mudler/LocalAgent/core/sse"
|
||||||
"github.com/mudler/LocalAgent/core/state"
|
"github.com/mudler/LocalAgent/core/state"
|
||||||
@@ -299,6 +302,47 @@ func (a *App) Chat(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) ExecuteAction(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
payload := struct {
|
||||||
|
Config map[string]string `json:"config"`
|
||||||
|
Params action.ActionParams `json:"params"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
if err := c.BodyParser(&payload); err != nil {
|
||||||
|
xlog.Error("Error parsing action payload", "error", err)
|
||||||
|
return errorJSONMessage(c, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
actionName := c.Params("name")
|
||||||
|
|
||||||
|
xlog.Debug("Executing action", "action", actionName, "config", payload.Config, "params", payload.Params)
|
||||||
|
a, err := services.Action(actionName, payload.Config, pool)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Error creating action", "error", err)
|
||||||
|
return errorJSONMessage(c, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
res, err := a.Run(ctx, payload.Params)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Error running action", "error", err)
|
||||||
|
return errorJSONMessage(c, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
xlog.Info("Action executed", "action", actionName, "result", res)
|
||||||
|
return c.JSON(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) ListActions() func(c *fiber.Ctx) error {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
return c.JSON(services.AvailableActions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) Responses(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
func (a *App) Responses(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
||||||
return func(c *fiber.Ctx) error {
|
return func(c *fiber.Ctx) error {
|
||||||
var request types.RequestBody
|
var request types.RequestBody
|
||||||
|
|||||||
@@ -130,10 +130,17 @@ func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
webapp.Get("/actions-playground", func(c *fiber.Ctx) error {
|
||||||
|
return c.Render("views/actions", fiber.Map{})
|
||||||
|
})
|
||||||
|
|
||||||
// New API endpoints for getting and updating agent configuration
|
// New API endpoints for getting and updating agent configuration
|
||||||
webapp.Get("/api/agent/:name/config", app.GetAgentConfig(pool))
|
webapp.Get("/api/agent/:name/config", app.GetAgentConfig(pool))
|
||||||
webapp.Put("/api/agent/:name/config", app.UpdateAgentConfig(pool))
|
webapp.Put("/api/agent/:name/config", app.UpdateAgentConfig(pool))
|
||||||
|
|
||||||
|
webapp.Post("/action/:name/run", app.ExecuteAction(pool))
|
||||||
|
webapp.Get("/actions", app.ListActions())
|
||||||
|
|
||||||
webapp.Post("/settings/import", app.ImportAgent(pool))
|
webapp.Post("/settings/import", app.ImportAgent(pool))
|
||||||
webapp.Get("/settings/export/:name", app.ExportAgent(pool))
|
webapp.Get("/settings/export/:name", app.ExportAgent(pool))
|
||||||
}
|
}
|
||||||
|
|||||||
291
webui/views/actions.html
Normal file
291
webui/views/actions.html
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Actions Playground</title>
|
||||||
|
{{template "views/partials/header"}}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{template "views/partials/menu"}}
|
||||||
|
|
||||||
|
<!-- Toast for notifications -->
|
||||||
|
<div id="toast" class="toast">
|
||||||
|
<span id="toast-message"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container mx-auto">
|
||||||
|
<header class="text-center mb-8">
|
||||||
|
<h1 class="text-4xl md:text-6xl font-bold">Actions Playground</h1>
|
||||||
|
<p class="mt-4 text-gray-400">Test and execute actions directly from the UI</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="section-box mb-8">
|
||||||
|
<h2 class="mb-4">Select an Action</h2>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="action-select" class="block mb-2">Available Actions:</label>
|
||||||
|
<select id="action-select" class="w-full">
|
||||||
|
<option value="">-- Select an action --</option>
|
||||||
|
<!-- Actions will be loaded here -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="config-section" class="section-box mb-8 hidden">
|
||||||
|
<h2 class="mb-4">Action Configuration</h2>
|
||||||
|
<form id="action-form">
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="config-json" class="block mb-2">Configuration (JSON):</label>
|
||||||
|
<textarea id="config-json" class="w-full" rows="5" placeholder='{"key": "value"}'>{}</textarea>
|
||||||
|
<p class="text-xs text-gray-400 mt-1">Enter JSON configuration for the action</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="params-json" class="block mb-2">Parameters (JSON):</label>
|
||||||
|
<textarea id="params-json" class="w-full" rows="5" placeholder='{"key": "value"}'>{}</textarea>
|
||||||
|
<p class="text-xs text-gray-400 mt-1">Enter JSON parameters for the action</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button type="submit" class="action-btn start-btn">
|
||||||
|
<i class="fas fa-play"></i> Execute Action
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="results-section" class="section-box mb-8 hidden">
|
||||||
|
<h2 class="mb-4">Action Results</h2>
|
||||||
|
<div id="action-results">
|
||||||
|
<!-- Results will appear here -->
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer class="text-center text-gray-500 text-sm mb-8">
|
||||||
|
<p>© 2025 LocalAgent.</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Load available actions
|
||||||
|
fetchActions();
|
||||||
|
|
||||||
|
// Handle action selection
|
||||||
|
document.getElementById('action-select').addEventListener('change', function() {
|
||||||
|
const actionId = this.value;
|
||||||
|
if (actionId) {
|
||||||
|
document.getElementById('config-section').classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
document.getElementById('config-section').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide results when changing actions
|
||||||
|
document.getElementById('results-section').classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle form submission
|
||||||
|
document.getElementById('action-form').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const actionId = document.getElementById('action-select').value;
|
||||||
|
if (actionId) {
|
||||||
|
executeAction(actionId);
|
||||||
|
} else {
|
||||||
|
showToast('Please select an action first', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function fetchActions() {
|
||||||
|
fetch('/actions')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(actions => {
|
||||||
|
const select = document.getElementById('action-select');
|
||||||
|
|
||||||
|
// Clear existing options except the first one
|
||||||
|
while (select.options.length > 1) {
|
||||||
|
select.remove(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actions.length === 0) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.text = 'No actions available';
|
||||||
|
option.disabled = true;
|
||||||
|
select.add(option);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add options for each action
|
||||||
|
actions.forEach(actionId => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = actionId;
|
||||||
|
option.text = actionId; // Using actionId as display text
|
||||||
|
select.add(option);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error fetching actions:', error);
|
||||||
|
showToast('Failed to load actions: ' + error.message, 'error');
|
||||||
|
|
||||||
|
const select = document.getElementById('action-select');
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.text = 'Error loading actions';
|
||||||
|
option.disabled = true;
|
||||||
|
|
||||||
|
// Clear existing options except the first one
|
||||||
|
while (select.options.length > 1) {
|
||||||
|
select.remove(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
select.add(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeAction(actionId) {
|
||||||
|
// Get the JSON data from textareas
|
||||||
|
let config = {};
|
||||||
|
let params = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const configText = document.getElementById('config-json').value.trim();
|
||||||
|
if (configText && configText !== '{}') {
|
||||||
|
config = JSON.parse(configText);
|
||||||
|
}
|
||||||
|
|
||||||
|
const paramsText = document.getElementById('params-json').value.trim();
|
||||||
|
if (paramsText && paramsText !== '{}') {
|
||||||
|
params = JSON.parse(paramsText);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showToast('Invalid JSON: ' + error.message, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the results section with loading indicator
|
||||||
|
const resultsSection = document.getElementById('results-section');
|
||||||
|
resultsSection.classList.remove('hidden');
|
||||||
|
|
||||||
|
const resultDiv = document.getElementById('action-results');
|
||||||
|
resultDiv.innerHTML = `
|
||||||
|
<div class="flex justify-center items-center py-8">
|
||||||
|
<div class="loader"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Execute the action
|
||||||
|
fetch(`/action/${actionId}/run`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
config: config,
|
||||||
|
params: params
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(result => {
|
||||||
|
if (result.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
// Display the results
|
||||||
|
showActionResult(result);
|
||||||
|
showToast('Action executed successfully!', 'success');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
resultDiv.innerHTML = `
|
||||||
|
<div class="alert alert-error" style="display: block;">
|
||||||
|
<i class="fas fa-exclamation-circle mr-2"></i> Error: ${error.message}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
showToast('Error executing action', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showActionResult(result) {
|
||||||
|
const resultDiv = document.getElementById('action-results');
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
// Display result
|
||||||
|
if (result.Result) {
|
||||||
|
html += `
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4 class="text-lg mb-2" style="color: var(--secondary);">Result:</h4>
|
||||||
|
<div class="code-terminal">
|
||||||
|
<pre>${escapeHtml(result.Result)}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display metadata if available
|
||||||
|
if (result.Metadata && Object.keys(result.Metadata).length > 0) {
|
||||||
|
html += `
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4 class="text-lg mb-2" style="color: var(--secondary);">Metadata:</h4>
|
||||||
|
<div class="code-terminal">
|
||||||
|
<pre>${escapeHtml(JSON.stringify(result.Metadata, null, 2))}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!html) {
|
||||||
|
html = '<p class="text-gray-400">No results returned from the action.</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
resultDiv.innerHTML = html;
|
||||||
|
|
||||||
|
// Scroll to results
|
||||||
|
resultDiv.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(unsafe) {
|
||||||
|
return unsafe
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function showToast(message, type) {
|
||||||
|
const toast = document.getElementById('toast');
|
||||||
|
const toastMessage = document.getElementById('toast-message');
|
||||||
|
|
||||||
|
toastMessage.textContent = message;
|
||||||
|
toast.className = 'toast toast-' + type;
|
||||||
|
toast.classList.add('toast-visible');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.remove('toast-visible');
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.loader {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border: 5px solid var(--tertiary);
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
animation: rotation 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotation {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-terminal {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -33,6 +33,12 @@
|
|||||||
<span class="absolute bottom-0 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300"
|
<span class="absolute bottom-0 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300"
|
||||||
style="background: linear-gradient(90deg, var(--secondary), var(--tertiary));"></span>
|
style="background: linear-gradient(90deg, var(--secondary), var(--tertiary));"></span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/actions-playground" class="px-3 py-2 rounded-md text-lg font-medium text-gray-400 hover:bg-gray-800 transition duration-300 relative overflow-hidden group">
|
||||||
|
<i class="fas fa-bolt mr-2"></i> Actions Playground
|
||||||
|
<!-- Underline animation -->
|
||||||
|
<span class="absolute bottom-0 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300"
|
||||||
|
style="background: linear-gradient(90deg, var(--tertiary), var(--primary));"></span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,6 +74,12 @@
|
|||||||
style="border-left: 3px solid var(--secondary);">
|
style="border-left: 3px solid var(--secondary);">
|
||||||
<i class="fas fa-users mr-2"></i> Agent List
|
<i class="fas fa-users mr-2"></i> Agent List
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/actions-playground" class="px-3 py-2 rounded-md text-lg font-medium text-gray-400 hover:bg-gray-800 transition duration-300 relative overflow-hidden group">
|
||||||
|
<i class="fas fa-bolt mr-2"></i> Actions Playground
|
||||||
|
<!-- Underline animation -->
|
||||||
|
<span class="absolute bottom-0 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300"
|
||||||
|
style="background: linear-gradient(90deg, var(--tertiary), var(--primary));"></span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
Reference in New Issue
Block a user