mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-27 16:10:09 +00:00
- Add POST /api/llm/test_connection endpoint that validates connectivity and auth for OpenAI-compatible, Anthropic, and Ollama adapters (10s timeout, per-adapter request logic) - Add "Test" button next to Save/Cancel in the add-provider form; result shown inline with green/red styling - Hide delete button for the active provider instead of showing an error toast - Sort the active provider to the top of the provider list - Clear selected_model when switching providers to avoid model-not-supported errors on the new provider - Add i18n keys for test/testing states (en + zh-CN)
428 lines
21 KiB
HTML
428 lines
21 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||
<title>IronClaw</title>
|
||
<link rel="icon" href="/favicon.ico" type="image/x-icon">
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="/style.css">
|
||
|
||
<!-- i18n Modules -->
|
||
<script src="/i18n/index.js"></script>
|
||
<script src="/i18n/en.js"></script>
|
||
<script src="/i18n/zh-CN.js"></script>
|
||
|
||
<script
|
||
src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.2.3/purify.min.js"
|
||
integrity="sha384-osZDKVu4ipZP703HmPOhWdyBajcFyjX2Psjk//TG1Rc0AdwEtuToaylrmcK3LdAl"
|
||
crossorigin="anonymous"
|
||
></script>
|
||
<script
|
||
src="https://cdn.jsdelivr.net/npm/[email protected]/lib/marked.umd.min.js"
|
||
integrity="sha384-pN9zSKOnTZwXRtYZAu0PBPEgR2B7DOC1aeLxQ33oJ0oy5iN1we6gm57xldM2irDG"
|
||
crossorigin="anonymous"
|
||
></script>
|
||
</head>
|
||
<body>
|
||
<!-- Auth Screen -->
|
||
<div id="auth-screen">
|
||
<div class="auth-card-login">
|
||
<div class="auth-brand">
|
||
<h1 data-i18n="auth.title">IronClaw</h1>
|
||
<p class="auth-tagline" data-i18n="auth.tagline">Secure AI Assistant</p>
|
||
</div>
|
||
<div class="auth-form">
|
||
<label for="token-input" data-i18n="auth.tokenLabel">Gateway Token</label>
|
||
<input type="password" id="token-input" data-i18n="auth.tokenPlaceholder" data-i18n-attr="placeholder" placeholder="Paste your auth token" autofocus>
|
||
<button id="auth-connect-btn" data-i18n="auth.connect">Connect</button>
|
||
</div>
|
||
<div id="auth-error"></div>
|
||
<p class="auth-hint" data-i18n="auth.hint">Enter the GATEWAY_AUTH_TOKEN from your .env configuration.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Provider Add/Edit Dialog -->
|
||
<div id="provider-dialog" class="provider-dialog" style="display:none">
|
||
<div class="provider-dialog-overlay" id="provider-dialog-overlay"></div>
|
||
<div class="provider-dialog-content">
|
||
<div class="provider-dialog-header">
|
||
<h2 id="provider-form-title" data-i18n="config.newProvider">New Provider</h2>
|
||
<button class="provider-dialog-close" id="cancel-provider-btn" title="Close">×</button>
|
||
</div>
|
||
<div class="provider-dialog-body">
|
||
<div class="config-form">
|
||
<div class="config-form-row">
|
||
<label data-i18n="config.providerName">Display Name</label>
|
||
<input type="text" id="provider-name" data-i18n="config.providerNamePlaceholder" data-i18n-attr="placeholder" placeholder="My Provider">
|
||
</div>
|
||
<div class="config-form-row">
|
||
<label data-i18n="config.providerId">Provider ID</label>
|
||
<input type="text" id="provider-id" data-i18n="config.providerIdPlaceholder" data-i18n-attr="placeholder" placeholder="my-provider">
|
||
<span class="config-form-hint" data-i18n="config.providerIdHint">Lowercase letters, numbers, hyphens</span>
|
||
</div>
|
||
<div class="config-form-row">
|
||
<label data-i18n="config.providerAdapter">API Adapter</label>
|
||
<select id="provider-adapter">
|
||
<option value="open_ai_completions" data-i18n="config.adapterOpenAI">OpenAI Compatible</option>
|
||
<option value="anthropic" data-i18n="config.adapterAnthropic">Anthropic</option>
|
||
<option value="ollama" data-i18n="config.adapterOllama">Ollama</option>
|
||
</select>
|
||
</div>
|
||
<div class="config-form-row">
|
||
<label data-i18n="config.providerBaseUrl">Base URL</label>
|
||
<input type="text" id="provider-base-url" placeholder="https://api.example.com/v1">
|
||
</div>
|
||
<div class="config-form-row">
|
||
<label data-i18n="config.providerApiKey">API Key</label>
|
||
<input type="password" id="provider-api-key" placeholder="sk-...">
|
||
</div>
|
||
<div class="config-form-row">
|
||
<label data-i18n="config.providerModel">Default Model</label>
|
||
<div class="provider-model-input-group">
|
||
<input type="text" id="provider-model" placeholder="gpt-4o">
|
||
<button id="fetch-models-btn" class="btn-fetch-models" type="button" data-i18n-title="config.fetchModels" title="Fetch models">↻</button>
|
||
</div>
|
||
<select id="provider-model-select" style="display:none"></select>
|
||
</div>
|
||
<div id="test-connection-result" class="test-connection-result" style="display:none"></div>
|
||
</div>
|
||
</div>
|
||
<div class="provider-dialog-footer">
|
||
<button id="save-provider-btn" data-i18n="common.save">Save</button>
|
||
<button id="test-provider-btn" class="btn-secondary" data-i18n="config.testConnection">Test</button>
|
||
<button id="cancel-provider-footer-btn" class="btn-secondary" data-i18n="common.cancel">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Restart Confirmation Modal -->
|
||
<div id="restart-confirm-modal" class="restart-modal" style="display: none;">
|
||
<div class="restart-modal-overlay" id="restart-overlay"></div>
|
||
<div class="restart-modal-content">
|
||
<div class="restart-modal-header">
|
||
<h2 data-i18n="restart.title">Restart IronClaw Instance</h2>
|
||
<button class="restart-modal-close" id="restart-close-btn" data-i18n="restart.closeTooltip" data-i18n-attr="title"
|
||
title="Close">×</button>
|
||
</div>
|
||
<div class="restart-modal-body">
|
||
<p class="restart-modal-description" data-i18n="restart.description">
|
||
Are you sure you want to restart the IronClaw instance? This will gracefully restart the process.
|
||
</p>
|
||
<div class="restart-modal-warning">
|
||
<span class="restart-modal-warning-icon">⚠️</span>
|
||
<p data-i18n="restart.warning">Any in-progress jobs may be interrupted. The restart will complete within a few seconds.</p>
|
||
</div>
|
||
</div>
|
||
<div class="restart-modal-footer">
|
||
<button class="restart-modal-btn cancel" id="restart-cancel-btn" data-i18n="restart.cancel">Cancel</button>
|
||
<button class="restart-modal-btn confirm" id="restart-confirm-btn" data-i18n="restart.confirm">Confirm Restart</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Restart Progress Modal -->
|
||
<div id="restart-loader" class="restart-loader" style="display: none;">
|
||
<div class="restart-loader-overlay"></div>
|
||
<div class="restart-loader-content">
|
||
<div class="restart-spinner"></div>
|
||
<div class="restart-loader-text">
|
||
<p class="restart-title" data-i18n="restart.progressTitle">Restarting IronClaw</p>
|
||
<p class="restart-subtitle" data-i18n="restart.progressSubtitle">Please wait while the process restarts...</p>
|
||
</div>
|
||
<div class="restart-progress-bar">
|
||
<div class="restart-progress-fill"></div>
|
||
</div>
|
||
<p class="restart-modal-info" data-i18n="restart.checkLogs">
|
||
Check the Logs tab for details after the restart completes.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Main App (hidden until authenticated) -->
|
||
<div id="app">
|
||
<!-- Tab Bar -->
|
||
<div class="tab-bar">
|
||
<button class="active" data-tab="chat" data-i18n="tab.chat">Chat</button>
|
||
<button data-tab="memory" data-i18n="tab.memory">Memory</button>
|
||
<button data-tab="jobs" data-i18n="tab.jobs">Jobs</button>
|
||
<button data-tab="routines" data-i18n="tab.routines">Routines</button>
|
||
<button data-tab="extensions" data-i18n="tab.extensions">Extensions</button>
|
||
<button data-tab="skills" data-i18n="tab.skills">Skills</button>
|
||
<button data-tab="config" data-i18n="tab.config">Config</button>
|
||
<div class="spacer"></div>
|
||
|
||
<!-- Language Switcher -->
|
||
<div class="language-switcher">
|
||
<button class="language-btn" id="language-btn" type="button" title="Switch Language"
|
||
aria-label="Switch language" aria-haspopup="true" aria-expanded="false" aria-controls="language-menu">🌐</button>
|
||
<div class="language-menu" id="language-menu" style="display: none;">
|
||
<button type="button" class="language-option" data-action="switch-language" data-lang="en">English</button>
|
||
<button type="button" class="language-option" data-action="switch-language" data-lang="zh-CN">简体中文</button>
|
||
</div>
|
||
</div>
|
||
|
||
<button class="status-logs-btn" data-tab="logs" data-i18n="tab.logs" title="Logs">Logs</button>
|
||
<div class="tee-shield" id="tee-shield" style="display:none" title="Running in a Trusted Execution Environment">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||
</svg>
|
||
<span id="tee-shield-label" data-i18n="status.teeVerified">TEE Verified</span>
|
||
<div class="tee-popover" id="tee-popover"></div>
|
||
</div>
|
||
<div class="status" id="gateway-status-trigger">
|
||
<div class="dot" id="sse-dot"></div>
|
||
<span id="sse-status" data-i18n="status.connected">Connected</span>
|
||
<div class="gateway-popover" id="gateway-popover"></div>
|
||
</div>
|
||
<button class="restart-btn" id="restart-btn" data-i18n="status.restartTooltip"
|
||
data-i18n-attr="title" title="Gracefully restart the process" style="display: none;">
|
||
<svg id="restart-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M23 4v6h-6"></path>
|
||
<path d="M1 20v-6h6"></path>
|
||
<path d="M3.51 9a9 9 0 0114.85-3.36M20.49 15a9 9 0 01-14.85 3.36"></path>
|
||
</svg>
|
||
<span data-i18n="status.restart">Restart</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Chat Tab -->
|
||
<div class="tab-panel active" id="tab-chat">
|
||
<div class="thread-sidebar" id="thread-sidebar">
|
||
<div class="thread-sidebar-header">
|
||
<button class="thread-new-btn" id="thread-new-btn" data-i18n="chat.newThread" data-i18n-attr="title"
|
||
title="New thread (Ctrl/Cmd+N)">+</button>
|
||
<div class="spacer"></div>
|
||
<button class="thread-toggle-btn" id="thread-toggle-btn" data-i18n="chat.toggleSidebar"
|
||
data-i18n-attr="title" title="Toggle sidebar">«</button>
|
||
</div>
|
||
<div class="assistant-item" id="assistant-thread">
|
||
<span class="assistant-label" id="assistant-label" data-i18n="chat.assistant">Assistant</span>
|
||
<span class="assistant-meta" id="assistant-meta"></span>
|
||
</div>
|
||
<div class="threads-section-header">
|
||
<span data-i18n="chat.conversations">Conversations</span>
|
||
</div>
|
||
<div class="thread-list" id="thread-list"></div>
|
||
</div>
|
||
<div class="chat-container">
|
||
<div class="chat-messages" id="chat-messages"></div>
|
||
<div id="slash-autocomplete" class="slash-autocomplete" style="display:none"></div>
|
||
<div id="suggestion-chips" class="suggestion-chips" style="display:none"></div>
|
||
<div class="chat-input">
|
||
<div id="image-preview-strip" class="image-preview-strip"></div>
|
||
<div class="chat-input-wrapper">
|
||
<textarea id="chat-input" data-i18n="chat.inputPlaceholder" data-i18n-attr="placeholder" placeholder="Message or / for commands..." rows="1"></textarea>
|
||
<div id="ghost-text" class="ghost-text"></div>
|
||
</div>
|
||
<input type="file" id="image-file-input" accept="image/*" multiple style="display:none">
|
||
<button id="attach-btn" class="attach-btn" data-i18n="chat.attachImages" data-i18n-attr="title" title="Attach images"
|
||
aria-label="Attach images">📎</button>
|
||
<button id="send-btn" data-i18n="chat.send">Send</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Memory Tab -->
|
||
<div class="tab-panel" id="tab-memory">
|
||
<div class="memory-container">
|
||
<div class="memory-sidebar">
|
||
<div class="search-box">
|
||
<input type="text" id="memory-search" data-i18n="memory.searchPlaceholder" data-i18n-attr="placeholder" placeholder="Search memory...">
|
||
</div>
|
||
<div class="memory-tree" id="memory-tree"></div>
|
||
</div>
|
||
<div class="memory-content">
|
||
<div class="memory-breadcrumb" id="memory-breadcrumb">
|
||
<span id="memory-breadcrumb-path">workspace /</span>
|
||
<button class="memory-edit-btn" id="memory-edit-btn" style="display:none" data-i18n="memory.edit">Edit</button>
|
||
</div>
|
||
<div class="memory-viewer" id="memory-viewer">
|
||
<div class="empty" data-i18n="memory.selectFile">Select a file to view its contents</div>
|
||
</div>
|
||
<div class="memory-editor" id="memory-editor" style="display:none">
|
||
<textarea id="memory-edit-textarea"></textarea>
|
||
<div class="memory-editor-actions">
|
||
<button class="btn-save" id="memory-save-btn" data-i18n="memory.save">Save</button>
|
||
<button class="btn-cancel-edit" id="memory-cancel-btn" data-i18n="memory.cancel">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Jobs Tab -->
|
||
<div class="tab-panel" id="tab-jobs">
|
||
<div class="jobs-container">
|
||
<div class="jobs-summary" id="jobs-summary"></div>
|
||
<table class="jobs-table" id="jobs-table">
|
||
<thead>
|
||
<tr>
|
||
<th data-i18n="jobs.id">ID</th>
|
||
<th data-i18n="jobs.title">Title</th>
|
||
<th data-i18n="jobs.source">Source</th>
|
||
<th data-i18n="jobs.status">Status</th>
|
||
<th data-i18n="jobs.created">Created</th>
|
||
<th data-i18n="jobs.actions">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="jobs-tbody"></tbody>
|
||
</table>
|
||
<div class="empty-state" id="jobs-empty" style="display:none" data-i18n="jobs.empty">No jobs found</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Logs Tab -->
|
||
<div class="tab-panel" id="tab-logs">
|
||
<div class="logs-container">
|
||
<div class="logs-toolbar">
|
||
<select id="logs-server-level" title="Server-side log level (changes what the server emits)">
|
||
<option value="error">Server: ERROR</option>
|
||
<option value="warn">Server: WARN</option>
|
||
<option value="info" selected>Server: INFO</option>
|
||
<option value="debug">Server: DEBUG</option>
|
||
</select>
|
||
<select id="logs-level-filter">
|
||
<option value="all" data-i18n="logs.allLevels">All Levels</option>
|
||
<option value="ERROR" data-i18n="logs.error">Error</option>
|
||
<option value="WARN" data-i18n="logs.warn">Warn</option>
|
||
<option value="INFO" data-i18n="logs.info">Info</option>
|
||
<option value="DEBUG" data-i18n="logs.debug">Debug</option>
|
||
</select>
|
||
<input type="text" id="logs-target-filter" placeholder="Filter by target...">
|
||
<label class="logs-checkbox"><input type="checkbox" id="logs-autoscroll" checked> <span data-i18n="logs.autoScroll">Auto-scroll</span></label>
|
||
<button id="logs-pause-btn" data-i18n="logs.pause">Pause</button>
|
||
<button id="logs-clear-btn" data-i18n="logs.clear">Clear</button>
|
||
</div>
|
||
<div class="logs-output" id="logs-output"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Routines Tab -->
|
||
<div class="tab-panel" id="tab-routines">
|
||
<div class="routines-container">
|
||
<div class="routines-summary" id="routines-summary"></div>
|
||
<table class="routines-table" id="routines-table">
|
||
<thead>
|
||
<tr>
|
||
<th data-i18n="routines.name">Name</th>
|
||
<th data-i18n="routines.trigger">Trigger</th>
|
||
<th data-i18n="routines.action">Action</th>
|
||
<th data-i18n="routines.lastRun">Last Run</th>
|
||
<th data-i18n="routines.nextRun">Next Run</th>
|
||
<th data-i18n="routines.runs">Runs</th>
|
||
<th data-i18n="routines.status">Status</th>
|
||
<th data-i18n="routines.actions">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="routines-tbody"></tbody>
|
||
</table>
|
||
<div class="empty-state" id="routines-empty" style="display:none">
|
||
<span data-i18n="routines.noConfigured">No routines configured. Ask the assistant to create one.</span>
|
||
</div>
|
||
<div class="routine-detail" id="routine-detail" style="display:none"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Extensions Tab -->
|
||
<div class="tab-panel" id="tab-extensions">
|
||
<div class="extensions-container">
|
||
<div class="extensions-section">
|
||
<h3 data-i18n="extensions.installed">Installed Extensions</h3>
|
||
<div class="extensions-list" id="extensions-list">
|
||
<div class="empty-state" data-i18n="common.loading">Loading...</div>
|
||
</div>
|
||
</div>
|
||
<div class="extensions-section" id="available-wasm-section">
|
||
<h3 data-i18n="extensions.available">Available WASM Extensions</h3>
|
||
<div class="extensions-list" id="available-wasm-list">
|
||
<div class="empty-state" data-i18n="common.loading">Loading...</div>
|
||
</div>
|
||
</div>
|
||
<div class="extensions-section">
|
||
<h3 data-i18n="extensions.installWasm">Install WASM Extension</h3>
|
||
<div class="ext-install-form">
|
||
<input type="text" id="wasm-install-name" data-i18n-placeholder="common.name" placeholder="Extension name">
|
||
<input type="text" id="wasm-install-url" placeholder="URL to .tar.gz bundle">
|
||
<button id="wasm-install-btn" data-i18n="extensions.install">Install</button>
|
||
</div>
|
||
</div>
|
||
<div class="extensions-section">
|
||
<h3 data-i18n="mcp.servers">MCP Servers</h3>
|
||
<div class="extensions-list" id="mcp-servers-list">
|
||
<div class="empty-state" data-i18n="common.loading">Loading...</div>
|
||
</div>
|
||
<h4 data-i18n="mcp.addCustom">Add Custom MCP Server</h4>
|
||
<div class="ext-install-form">
|
||
<input type="text" id="mcp-install-name" data-i18n-placeholder="common.name" placeholder="Server name">
|
||
<input type="text" id="mcp-install-url" placeholder="MCP server URL (https://...)">
|
||
<button id="mcp-add-btn" data-i18n="mcp.add">Add</button>
|
||
</div>
|
||
</div>
|
||
<div class="extensions-section">
|
||
<h3 data-i18n="tools.registered">Registered Tools</h3>
|
||
<table class="tools-table" id="tools-table">
|
||
<thead><tr><th data-i18n="tools.name">Name</th><th data-i18n="tools.description">Description</th></tr></thead>
|
||
<tbody id="tools-tbody"></tbody>
|
||
</table>
|
||
<div class="empty-state" id="tools-empty" style="display:none" data-i18n="tools.empty">No tools registered</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Config Tab -->
|
||
<div class="tab-panel" id="tab-config">
|
||
<div class="extensions-container">
|
||
<div class="extensions-section">
|
||
<div class="config-section-header">
|
||
<h3 data-i18n="config.modelProviders">Model Providers</h3>
|
||
<button id="add-provider-btn" class="btn-add-provider" data-i18n="config.addProvider">+ Add Provider</button>
|
||
</div>
|
||
<div class="config-notice" id="config-restart-notice" style="display:none">
|
||
<span>⚠</span>
|
||
<span data-i18n="config.restartNotice">Changes take effect after restart.</span>
|
||
</div>
|
||
<div id="providers-list" class="providers-list">
|
||
<div class="empty-state" data-i18n="common.loading">Loading...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Skills Tab -->
|
||
<div class="tab-panel" id="tab-skills">
|
||
<div class="extensions-container">
|
||
<div class="extensions-section">
|
||
<h3 data-i18n="skills.searchClawHub">Search ClawHub</h3>
|
||
<div class="skill-search-box">
|
||
<input type="text" id="skill-search-input" data-i18n-placeholder="skills.searchPlaceholder" placeholder="Search...">
|
||
<button id="skill-search-btn" data-i18n="skills.search">Search</button>
|
||
</div>
|
||
<div class="extensions-list" id="skill-search-results"></div>
|
||
</div>
|
||
<div class="extensions-section">
|
||
<h3 data-i18n="skills.installed">Installed Skills</h3>
|
||
<div class="extensions-list" id="skills-list">
|
||
<div class="empty-state" data-i18n="skills.loading">Loading skills...</div>
|
||
</div>
|
||
</div>
|
||
<div class="extensions-section">
|
||
<h3 data-i18n="skills.installByUrl">Install Skill by URL</h3>
|
||
<div class="ext-install-form">
|
||
<input type="text" id="skill-install-name" data-i18n-placeholder="skills.namePlaceholder" placeholder="Skill name or slug">
|
||
<input type="text" id="skill-install-url" data-i18n-placeholder="skills.urlPlaceholder" placeholder="HTTPS URL to SKILL.md (optional)">
|
||
<button id="skill-install-btn" data-i18n="extensions.install">Install</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="toasts"></div>
|
||
<script src="/app.js"></script>
|
||
<script src="/i18n-app.js"></script>
|
||
</body>
|
||
</html>
|