mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
* feat(web): add follow-up suggestion chips and ghost text to chat UI The LLM now always generates 1-3 follow-up command suggestions via <suggestions> tags in its response. These are extracted server-side, broadcast as SSE events, and rendered as clickable chips above the chat input. The first suggestion also appears as ghost text in the input field (Tab to accept). Includes debug logging for LLM responses in the agentic loop and removes noisy NEAR AI status logging. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: resolve deferred review items from PR #1156 [skip-regression-check] - Remove literal backslashes from raw string prompt (reasoning.rs) - Make WASM channels skip Suggestions status (no-op instead of empty callback) - Add !e.shiftKey guard to Tab-to-accept ghost text handler - Cap extracted suggestions at 3 and trim whitespace-only entries - Extract suggestions in approval-resume path (prevents tag leaking) - Remove stale .has-ghost class during showSuggestionChips reset Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
354 lines
17 KiB
HTML
354 lines
17 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>
|
||
|
||
<!-- 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>
|
||
<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>
|
||
|
||
<!-- 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>
|