feat: improve Heroprompt UI and refactor modules

- Refactor all UI rendering logic into a single `ui` module
- Centralize static assets serving to `/static` directory
- Redesign Heroprompt page with Bootstrap 5 components
- Enhance workspace management and file tree interactions
- Add Bootstrap modal support for UI dialogs
This commit is contained in:
Mahmoud-Emad
2025-08-21 20:01:43 +03:00
parent 066f339b78
commit cf187d46b3
22 changed files with 1214 additions and 2216 deletions

View File

@@ -0,0 +1,489 @@
/* Chat Widget Styles */
/* Chat Container Layout */
.chat-container {
display: flex;
flex-direction: column;
height: calc(100vh - 44px - 32px);
background-color: var(--bg-primary);
border-radius: 8px;
border: 1px solid var(--border-primary);
overflow: hidden;
margin-bottom: 1rem;
}
.chat-header {
display: flex;
justify-content: between;
align-items: center;
padding: 1rem;
background-color: var(--bg-secondary);
border-bottom: 1px solid var(--border-primary);
}
.chat-controls {
display: flex;
gap: 0.5rem;
}
/* Chat Messages Area */
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
background-color: var(--bg-primary);
}
.message {
display: flex;
gap: 0.75rem;
max-width: 80%;
animation: messageSlideIn 0.3s ease-out;
}
.message.user {
align-self: flex-end;
flex-direction: row-reverse;
}
.message.assistant {
align-self: flex-start;
}
.message-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
flex-shrink: 0;
}
.message.user .message-avatar {
background-color: var(--link-color);
color: white;
}
.message.assistant .message-avatar {
background-color: var(--bg-tertiary);
color: var(--text-secondary);
}
.message-content {
flex: 1;
}
.message-text {
background-color: var(--card-bg);
padding: 0.75rem 1rem;
border-radius: 1rem;
border: 1px solid var(--border-primary);
color: var(--text-primary);
line-height: 1.4;
word-wrap: break-word;
}
.message.user .message-text {
background-color: var(--link-color);
color: white;
border-color: var(--link-color);
}
.message-time {
font-size: 0.75rem;
color: var(--text-muted);
margin-top: 0.25rem;
text-align: right;
}
.message.user .message-time {
text-align: left;
}
/* Chat Input Area */
.chat-input-container {
padding: 1rem;
background-color: var(--bg-secondary);
border-top: 1px solid var(--border-primary);
}
.chat-input-wrapper {
display: flex;
gap: 0.5rem;
align-items: flex-end;
}
.chat-input {
flex: 1;
resize: none;
min-height: 40px;
max-height: 120px;
background-color: var(--bg-primary);
border: 1px solid var(--border-primary);
color: var(--text-primary);
border-radius: 20px;
padding: 0.75rem 1rem;
}
.chat-input:focus {
border-color: var(--link-color);
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
}
.chat-input-actions {
display: flex;
gap: 0.25rem;
align-items: center;
}
.chat-status {
margin-top: 0.5rem;
font-size: 0.875rem;
color: var(--text-muted);
min-height: 1.2rem;
}
/* Recorder Container */
.recorder-container {
background-color: var(--bg-primary);
border: 1px solid var(--border-primary);
border-radius: 8px;
overflow: hidden;
}
.recorder-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background-color: var(--bg-secondary);
border-bottom: 1px solid var(--border-primary);
}
.recorder-controls {
display: flex;
gap: 0.5rem;
}
/* Recording Status */
.recording-status {
padding: 1rem;
display: none;
background-color: var(--bg-primary);
border-bottom: 1px solid var(--border-primary);
}
.recording-status.active {
display: block;
}
.recording-indicator {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.recording-dot {
width: 12px;
height: 12px;
background-color: var(--danger-color);
border-radius: 50%;
animation: recordingPulse 1s infinite;
}
.recording-time {
font-family: 'Courier New', monospace;
font-weight: bold;
color: var(--text-primary);
}
.recording-level {
height: 4px;
background-color: var(--bg-tertiary);
border-radius: 2px;
overflow: hidden;
}
.level-bar {
height: 100%;
background-color: var(--success-color);
width: 0%;
transition: width 0.1s ease;
}
/* Recordings Explorer */
.recordings-explorer {
padding: 1rem;
max-height: 300px;
overflow-y: auto;
}
.explorer-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.explorer-actions {
display: flex;
gap: 0.25rem;
}
.explorer-tree {
font-size: 0.875rem;
}
.tree-item {
margin-bottom: 0.25rem;
}
.tree-item-content {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0.5rem;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.tree-item-content:hover {
background-color: var(--bg-secondary);
}
.tree-item-content.selected {
background-color: var(--link-color);
color: white;
}
.tree-item-name {
flex: 1;
color: var(--text-primary);
}
.tree-item-size {
font-size: 0.75rem;
color: var(--text-muted);
}
.tree-item-children {
margin-left: 1rem;
border-left: 1px solid var(--border-primary);
padding-left: 0.5rem;
}
.tree-item.folder > .tree-item-content > i {
color: var(--warning-color);
}
.tree-item.file > .tree-item-content > i {
color: var(--info-color);
}
/* Context Menu */
.context-menu {
position: fixed;
background-color: var(--card-bg);
border: 1px solid var(--border-primary);
border-radius: 6px;
box-shadow: 0 4px 12px var(--card-shadow);
padding: 0.25rem 0;
min-width: 150px;
z-index: 1060;
display: none;
}
.context-menu-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
cursor: pointer;
color: var(--text-primary);
transition: background-color 0.2s ease;
}
.context-menu-item:hover {
background-color: var(--bg-secondary);
}
.context-menu-divider {
height: 1px;
background-color: var(--border-primary);
margin: 0.25rem 0;
}
/* Animations */
@keyframes messageSlideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes recordingPulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.3;
}
}
/* Voice Input States */
.voice-input-active {
background-color: var(--danger-color) !important;
border-color: var(--danger-color) !important;
color: white !important;
}
.voice-input-processing {
background-color: var(--warning-color) !important;
border-color: var(--warning-color) !important;
color: white !important;
}
/* Upload Progress */
.upload-progress {
margin-top: 1rem;
}
.progress {
height: 8px;
background-color: var(--bg-tertiary);
border-radius: 4px;
overflow: hidden;
}
.progress-bar {
background-color: var(--link-color);
transition: width 0.3s ease;
}
/* Responsive Design */
@media (max-width: 768px) {
.chat-container {
height: calc(100vh - 44px - 16px);
margin-bottom: 0.5rem;
}
.message {
max-width: 90%;
}
.chat-input-wrapper {
flex-wrap: wrap;
}
.chat-input-actions {
order: -1;
width: 100%;
justify-content: center;
margin-bottom: 0.5rem;
}
.recordings-explorer {
max-height: 200px;
}
}
/* Dark theme specific adjustments */
[data-theme="dark"] .chat-input {
background-color: var(--bg-secondary);
}
[data-theme="dark"] .message.user .message-text {
background-color: var(--link-color);
}
[data-theme="dark"] .context-menu {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
}
/* Bootstrap Icons Integration */
.bi {
font-size: 1em;
line-height: 1;
}
/* Typing Indicator */
.typing-indicator {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
color: var(--text-muted);
font-style: italic;
}
.typing-dots {
display: flex;
gap: 0.25rem;
}
.typing-dot {
width: 6px;
height: 6px;
background-color: var(--text-muted);
border-radius: 50%;
animation: typingBounce 1.4s infinite ease-in-out;
}
.typing-dot:nth-child(1) { animation-delay: -0.32s; }
.typing-dot:nth-child(2) { animation-delay: -0.16s; }
@keyframes typingBounce {
0%, 80%, 100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
/* File attachment preview */
.file-attachment {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
background-color: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 6px;
margin-bottom: 0.5rem;
}
.file-attachment-icon {
color: var(--info-color);
}
.file-attachment-info {
flex: 1;
}
.file-attachment-name {
font-weight: 500;
color: var(--text-primary);
}
.file-attachment-size {
font-size: 0.75rem;
color: var(--text-muted);
}
.file-attachment-remove {
color: var(--danger-color);
cursor: pointer;
padding: 0.25rem;
}

View File

@@ -0,0 +1,329 @@
/* Heroprompt specific styles using UI project theme system */
/* Tree view specific styles */
.tree {
font-size: 0.875rem;
}
.tree .chev {
transition: transform 0.15s ease;
display: inline-block;
font-size: 0.75rem;
opacity: var(--menu-chevron-opacity);
}
.tree .toggle:checked+.dir-label .chev {
transform: rotate(90deg);
}
.tree .children {
margin-left: 16px;
border-left: 1px solid var(--border-primary);
padding-left: 8px;
}
.tree .dir-label {
cursor: pointer;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
transition: all 0.2s ease;
color: var(--text-primary);
}
.tree .dir-label:hover {
background-color: var(--menu-item-hover-bg);
color: var(--menu-item-hover-text);
}
.tree .file {
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
transition: all 0.2s ease;
}
.tree .file:hover {
background-color: var(--menu-item-hover-bg);
}
.tree .file a {
color: var(--text-primary);
text-decoration: none;
transition: color 0.2s ease;
}
.tree .file a:hover {
color: var(--link-hover-color);
}
/* Tab content styling */
.tab-pane {
height: calc(100vh - 200px);
overflow-y: auto;
background-color: var(--bg-primary);
color: var(--text-primary);
}
/* Selection list styling */
#selected {
max-height: 300px;
overflow-y: auto;
background-color: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 0.375rem;
padding: 0.5rem;
}
#selected li {
background-color: var(--card-bg);
border: 1px solid var(--card-border);
color: var(--text-primary);
}
#selected li:hover {
background-color: var(--bg-tertiary);
}
/* Preview area styling */
#preview {
background-color: var(--bg-secondary);
border: 1px solid var(--border-primary);
color: var(--text-primary);
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
font-size: 0.75rem;
white-space: pre-wrap;
line-height: 1.4;
border-radius: 0.375rem;
padding: 0.75rem;
}
/* Prompt output styling */
#promptOutput {
background-color: var(--bg-secondary);
border: 1px solid var(--border-primary);
color: var(--text-primary);
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
font-size: 0.75rem;
white-space: pre-wrap;
line-height: 1.4;
border-radius: 0.375rem;
padding: 0.75rem;
}
/* Chat messages styling */
#chatMessages {
background-color: var(--bg-secondary);
border: 1px solid var(--border-primary);
color: var(--text-primary);
border-radius: 0.375rem;
padding: 0.75rem;
}
/* Form controls theme integration */
.form-control {
background-color: var(--bg-primary);
border-color: var(--border-primary);
color: var(--text-primary);
}
.form-control:focus {
background-color: var(--bg-primary);
border-color: var(--link-color);
color: var(--text-primary);
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
}
.form-control::placeholder {
color: var(--text-muted);
}
/* Select elements */
.form-select {
background-color: var(--bg-primary);
border-color: var(--border-primary);
color: var(--text-primary);
}
.form-select:focus {
background-color: var(--bg-primary);
border-color: var(--link-color);
color: var(--text-primary);
}
/* Button theme integration */
.btn-outline-secondary {
color: var(--text-secondary);
border-color: var(--border-primary);
}
.btn-outline-secondary:hover {
background-color: var(--bg-tertiary);
border-color: var(--border-secondary);
color: var(--text-primary);
}
.btn-outline-primary {
color: var(--link-color);
border-color: var(--link-color);
}
.btn-outline-primary:hover {
background-color: var(--link-color);
border-color: var(--link-color);
color: var(--text-light);
}
.btn-outline-danger {
color: var(--danger-color);
border-color: var(--danger-color);
}
.btn-outline-danger:hover {
background-color: var(--danger-color);
border-color: var(--danger-color);
color: var(--text-light);
}
/* Badge styling */
.badge {
background-color: var(--bg-tertiary);
color: var(--text-primary);
}
/* Modal theme integration */
.modal-content {
background-color: var(--card-bg);
border: 1px solid var(--card-border);
color: var(--text-primary);
}
.modal-header {
border-bottom-color: var(--border-primary);
}
.modal-footer {
border-top-color: var(--border-primary);
}
/* Alert styling */
.alert-success {
background-color: rgba(25, 135, 84, 0.1);
border-color: var(--success-color);
color: var(--success-color);
}
.alert-danger {
background-color: rgba(220, 53, 69, 0.1);
border-color: var(--danger-color);
color: var(--danger-color);
}
/* Tree node buttons */
.tree .btn {
font-size: 0.75rem;
padding: 0.125rem 0.375rem;
line-height: 1.2;
}
/* Workspace info styling */
.workspace-info {
background-color: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 0.375rem;
padding: 0.5rem;
font-size: 0.75rem;
color: var(--text-muted);
}
/* Loading states */
.loading {
opacity: 0.6;
pointer-events: none;
}
.loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
margin: -10px 0 0 -10px;
border: 2px solid var(--border-primary);
border-top-color: var(--link-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Error states */
.error-message {
color: var(--danger-color);
background-color: rgba(220, 53, 69, 0.1);
border: 1px solid var(--danger-color);
border-radius: 0.375rem;
padding: 0.5rem;
font-size: 0.875rem;
}
/* Success states */
.success-message {
color: var(--success-color);
background-color: rgba(25, 135, 84, 0.1);
border: 1px solid var(--success-color);
border-radius: 0.375rem;
padding: 0.5rem;
font-size: 0.875rem;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.tree {
font-size: 0.8rem;
}
.tab-pane {
height: calc(100vh - 150px);
}
#preview,
#promptOutput {
font-size: 0.7rem;
}
}
/* High contrast mode support */
@media (prefers-contrast: high) {
.tree .dir-label:hover,
.tree .file:hover {
background-color: var(--text-primary);
color: var(--bg-primary);
}
.btn-outline-primary,
.btn-outline-secondary,
.btn-outline-danger {
border-width: 2px;
}
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
.tree .chev,
.tree .dir-label,
.tree .file,
.form-control,
.btn {
transition: none;
}
.loading::after {
animation: none;
}
}

View File

@@ -0,0 +1,529 @@
/* HeroScript Editor Specific Styles */
/* Full height layout for editor */
.main .container-fluid {
height: calc(100vh - 44px - 2rem); /* Account for header and padding */
}
/* Resizable container layout */
.resizable-container {
display: flex;
height: calc(100% - 60px); /* Account for header section */
gap: 0;
position: relative;
}
.editor-panel {
flex: 1;
min-width: 300px;
max-width: calc(100% - 250px); /* Ensure logs panel has minimum space */
display: flex;
flex-direction: column;
}
.logs-panel {
width: 350px;
min-width: 200px;
max-width: 600px;
display: flex;
flex-direction: column;
}
/* Resize handle styles */
.resize-handle {
background-color: var(--border-primary);
position: relative;
cursor: col-resize;
user-select: none;
transition: background-color 0.2s ease;
}
.resize-handle.horizontal {
width: 4px;
min-width: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.resize-handle:hover {
background-color: var(--link-color);
}
.resize-handle.dragging {
background-color: var(--link-color);
}
.resize-handle-line {
width: 2px;
height: 40px;
background-color: var(--border-secondary);
border-radius: 1px;
transition: background-color 0.2s ease;
}
.resize-handle:hover .resize-handle-line,
.resize-handle.dragging .resize-handle-line {
background-color: white;
}
/* Prevent text selection during resize */
.resizing {
user-select: none;
pointer-events: none;
}
.resizing * {
user-select: none;
pointer-events: none;
}
/* Editor Panel Styles */
#editor-container {
position: relative;
background-color: var(--bg-primary);
height: 100%;
}
#script-editor {
border: none;
resize: none;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 16px;
line-height: 1.6;
padding: 1.5rem;
border-radius: 0;
background-color: var(--bg-primary) !important;
color: var(--text-primary) !important;
outline: none;
box-shadow: none;
white-space: pre;
word-wrap: break-word;
overflow-wrap: break-word;
min-height: 500px;
height: 100%;
}
/* Highlighted overlay for syntax highlighting */
.highlighted-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 16px;
line-height: 1.6;
padding: 1.5rem;
margin: 0;
border: none;
background: transparent;
white-space: pre;
word-wrap: break-word;
overflow-wrap: break-word;
overflow: hidden;
z-index: 1;
}
#script-editor:focus {
outline: none;
box-shadow: 0 0 0 2px var(--link-color);
background-color: var(--bg-primary) !important;
color: var(--text-primary) !important;
}
#script-editor::placeholder {
color: var(--text-muted);
opacity: 0.7;
}
/* Simple syntax highlighting for basic keywords */
.hljs {
background-color: var(--bg-primary) !important;
color: var(--text-primary) !important;
padding: 1.5rem;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 16px;
line-height: 1.6;
}
/* Dark theme syntax highlighting */
[data-theme="dark"] .hljs {
background-color: var(--bg-primary) !important;
}
[data-theme="dark"] .hljs-keyword {
color: #569cd6;
}
[data-theme="dark"] .hljs-string {
color: #ce9178;
}
[data-theme="dark"] .hljs-comment {
color: #6a9955;
}
[data-theme="dark"] .hljs-number {
color: #b5cea8;
}
[data-theme="dark"] .hljs-function {
color: #dcdcaa;
}
/* Light theme syntax highlighting */
[data-theme="light"] .hljs-keyword {
color: #0000ff;
}
[data-theme="light"] .hljs-string {
color: #a31515;
}
[data-theme="light"] .hljs-comment {
color: #008000;
}
[data-theme="light"] .hljs-number {
color: #098658;
}
[data-theme="light"] .hljs-function {
color: #795e26;
}
/* Logs Panel Styles */
#logs-container {
background-color: var(--bg-secondary);
border-radius: 0;
}
#logs-content {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 13px;
line-height: 1.4;
white-space: pre-wrap;
word-wrap: break-word;
}
/* Log Entry Styles */
.log-entry {
display: block;
margin-bottom: 0.25rem;
padding: 0.125rem 0.5rem;
font-size: 13px;
line-height: 1.3;
}
.log-entry:last-child {
border-bottom: none;
}
/* Compact log format: cat: message */
.log-entry.compact {
display: flex;
align-items: baseline;
gap: 0.5rem;
}
.log-entry.compact .log-category {
font-weight: 600;
min-width: 80px;
flex-shrink: 0;
}
.log-entry.compact .log-message {
color: var(--text-primary);
flex: 1;
}
/* Legacy log entry styles (for backward compatibility) */
.log-entry .timestamp {
color: var(--text-muted);
font-size: 11px;
margin-right: 0.5rem;
}
.log-entry .level {
display: inline-block;
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
margin-right: 0.5rem;
min-width: 50px;
text-align: center;
}
.log-entry .level.info {
background-color: var(--info-color);
color: white;
}
.log-entry .level.success {
background-color: var(--success-color);
color: white;
}
.log-entry .level.warning {
background-color: var(--warning-color);
color: black;
}
.log-entry .level.error {
background-color: var(--danger-color);
color: white;
}
.log-entry .level.debug {
background-color: var(--text-muted);
color: white;
}
.log-entry .message {
color: var(--text-primary);
}
.log-entry.system {
background-color: var(--bg-tertiary);
padding: 0.5rem;
border-radius: 0.25rem;
border: 1px solid var(--border-secondary);
}
.log-entry.error {
background-color: rgba(220, 53, 69, 0.1);
border-left: 3px solid var(--danger-color);
padding-left: 0.75rem;
}
.log-entry.warning {
background-color: rgba(255, 193, 7, 0.1);
border-left: 3px solid var(--warning-color);
padding-left: 0.75rem;
}
/* Card Header Styles */
.card-header {
background-color: var(--bg-tertiary);
border-bottom: 1px solid var(--border-primary);
padding: 0.75rem 1rem;
}
.card-header h6 {
color: var(--text-primary);
font-weight: 600;
}
/* Control Buttons */
.editor-controls select {
width: 120px;
}
.log-controls .badge {
font-size: 0.7rem;
}
#connection-status.bg-success {
background-color: var(--success-color) !important;
}
#connection-status.bg-danger {
background-color: var(--danger-color) !important;
}
#connection-status.bg-warning {
background-color: var(--warning-color) !important;
color: black !important;
}
/* Auto-scroll button states */
#auto-scroll[data-active="true"] {
background-color: var(--link-color);
border-color: var(--link-color);
color: white;
}
#auto-scroll[data-active="false"] {
background-color: transparent;
border-color: var(--border-primary);
color: var(--text-secondary);
}
/* Action Buttons */
.btn-primary {
background-color: var(--link-color);
border-color: var(--link-color);
}
.btn-primary:hover {
background-color: var(--link-hover-color);
border-color: var(--link-hover-color);
}
.btn-success {
background-color: var(--success-color);
border-color: var(--success-color);
}
.btn-outline-secondary {
border-color: var(--border-primary);
color: var(--text-secondary);
}
.btn-outline-secondary:hover {
background-color: var(--bg-tertiary);
border-color: var(--border-secondary);
color: var(--text-primary);
}
/* Responsive Design */
@media (max-width: 768px) {
.resizable-container {
flex-direction: column;
height: auto;
}
.editor-panel {
height: 65vh;
margin-bottom: 1rem;
max-width: none;
min-width: auto;
}
.logs-panel {
height: 35vh;
margin-bottom: 0;
width: 100%;
min-width: auto;
max-width: none;
}
.resize-handle.horizontal {
display: none; /* Hide horizontal resize handle on mobile */
}
#script-editor {
font-size: 14px;
padding: 1rem;
min-height: 300px;
}
.d-flex.align-items-center.mb-3 {
flex-direction: column;
align-items: stretch !important;
}
.ms-auto {
margin-top: 1rem !important;
margin-left: 0 !important;
}
}
/* Medium screens - vertical layout with resizable height */
@media (min-width: 769px) and (max-width: 1200px) {
.resizable-container {
flex-direction: column;
height: calc(100% - 60px);
}
.editor-panel {
flex: 1;
min-height: 300px;
max-height: calc(100% - 200px);
max-width: none;
min-width: auto;
}
.logs-panel {
height: 250px;
min-height: 150px;
max-height: 400px;
width: 100%;
min-width: auto;
max-width: none;
}
.resize-handle.horizontal {
display: none;
}
.resize-handle.vertical {
display: flex;
width: 100%;
height: 4px;
min-height: 4px;
cursor: row-resize;
align-items: center;
justify-content: center;
}
.resize-handle.vertical .resize-handle-line {
width: 40px;
height: 2px;
}
}
/* Loading States */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.loading-spinner {
color: white;
font-size: 2rem;
}
/* Scrollbar Styling */
#logs-container::-webkit-scrollbar {
width: 8px;
}
#logs-container::-webkit-scrollbar-track {
background-color: var(--bg-tertiary);
}
#logs-container::-webkit-scrollbar-thumb {
background-color: var(--border-primary);
border-radius: 4px;
}
#logs-container::-webkit-scrollbar-thumb:hover {
background-color: var(--border-secondary);
}
/* Animation for new log entries */
@keyframes slideInLog {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.log-entry.new {
animation: slideInLog 0.3s ease-out;
}
/* Focus indicators for accessibility */
.btn:focus,
.form-select:focus,
#script-editor:focus {
outline: 2px solid var(--link-color);
outline-offset: 2px;
}

View File

@@ -0,0 +1,583 @@
/**
* Chat Widget JavaScript
* Handles chat functionality, voice recording, and file management
*/
class ChatWidget {
constructor() {
this.messages = [];
this.isRecording = false;
this.mediaRecorder = null;
this.audioChunks = [];
this.recordingStartTime = null;
this.recordingTimer = null;
this.selectedFiles = [];
this.recordings = [];
this.init();
}
init() {
this.bindEvents();
this.initializeRecorder();
this.loadRecordings();
this.setupContextMenu();
this.autoResizeTextarea();
}
bindEvents() {
// Chat input events
const chatInput = document.getElementById('chatInput');
const sendButton = document.getElementById('sendMessage');
chatInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.sendMessage();
}
});
chatInput.addEventListener('input', () => {
this.autoResizeTextarea();
});
sendButton.addEventListener('click', () => {
this.sendMessage();
});
// Voice input button
document.getElementById('voiceInput').addEventListener('click', () => {
this.toggleVoiceInput();
});
// File attachment
document.getElementById('attachFile').addEventListener('click', () => {
this.showFileUploadModal();
});
// Clear chat
document.getElementById('clearChat').addEventListener('click', () => {
this.clearChat();
});
// Recording controls
document.getElementById('recordBtn').addEventListener('click', () => {
this.startRecording();
});
document.getElementById('stopBtn').addEventListener('click', () => {
this.stopRecording();
});
document.getElementById('playBtn').addEventListener('click', () => {
this.playLastRecording();
});
// Explorer actions
document.getElementById('newFolderBtn').addEventListener('click', () => {
this.createNewFolder();
});
document.getElementById('refreshBtn').addEventListener('click', () => {
this.refreshRecordings();
});
// File upload modal
document.getElementById('uploadBtn').addEventListener('click', () => {
this.uploadFile();
});
// Tree item clicks
document.addEventListener('click', (e) => {
if (e.target.closest('.tree-item-content')) {
this.handleTreeItemClick(e);
}
});
// Context menu
document.addEventListener('contextmenu', (e) => {
if (e.target.closest('.tree-item-content')) {
e.preventDefault();
this.showContextMenu(e);
}
});
// Hide context menu on click outside
document.addEventListener('click', () => {
this.hideContextMenu();
});
}
sendMessage() {
const input = document.getElementById('chatInput');
const message = input.value.trim();
if (!message && this.selectedFiles.length === 0) return;
// Add user message
this.addMessage('user', message, this.selectedFiles);
// Clear input and files
input.value = '';
this.selectedFiles = [];
this.autoResizeTextarea();
// Show typing indicator
this.showTypingIndicator();
// Simulate AI response (replace with actual API call)
setTimeout(() => {
this.hideTypingIndicator();
this.addMessage('assistant', this.generateAIResponse(message));
}, 1000 + Math.random() * 2000);
}
addMessage(sender, text, files = []) {
const messagesContainer = document.getElementById('chatMessages');
const messageDiv = document.createElement('div');
messageDiv.className = `message ${sender}`;
const avatar = document.createElement('div');
avatar.className = 'message-avatar';
avatar.innerHTML = sender === 'user' ? '<i class="bi bi-person"></i>' : '<i class="bi bi-robot"></i>';
const content = document.createElement('div');
content.className = 'message-content';
// Add file attachments if any
if (files.length > 0) {
files.forEach(file => {
const fileDiv = document.createElement('div');
fileDiv.className = 'file-attachment';
fileDiv.innerHTML = `
<i class="bi bi-file-earmark file-attachment-icon"></i>
<div class="file-attachment-info">
<div class="file-attachment-name">${file.name}</div>
<div class="file-attachment-size">${this.formatFileSize(file.size)}</div>
</div>
`;
content.appendChild(fileDiv);
});
}
if (text) {
const textDiv = document.createElement('div');
textDiv.className = 'message-text';
textDiv.textContent = text;
content.appendChild(textDiv);
}
const timeDiv = document.createElement('div');
timeDiv.className = 'message-time';
timeDiv.textContent = new Date().toLocaleTimeString();
content.appendChild(timeDiv);
messageDiv.appendChild(avatar);
messageDiv.appendChild(content);
messagesContainer.appendChild(messageDiv);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
this.messages.push({ sender, text, files, timestamp: new Date() });
}
showTypingIndicator() {
const messagesContainer = document.getElementById('chatMessages');
const typingDiv = document.createElement('div');
typingDiv.className = 'message assistant typing-indicator';
typingDiv.id = 'typingIndicator';
typingDiv.innerHTML = `
<div class="message-avatar">
<i class="bi bi-robot"></i>
</div>
<div class="message-content">
<div class="typing-indicator">
AI is typing
<div class="typing-dots">
<div class="typing-dot"></div>
<div class="typing-dot"></div>
<div class="typing-dot"></div>
</div>
</div>
</div>
`;
messagesContainer.appendChild(typingDiv);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
hideTypingIndicator() {
const typingIndicator = document.getElementById('typingIndicator');
if (typingIndicator) {
typingIndicator.remove();
}
}
generateAIResponse(userMessage) {
const responses = [
"I understand your question. Let me help you with that.",
"That's an interesting point. Here's what I think...",
"Based on what you've shared, I'd suggest...",
"I can help you with that. Here are some options...",
"Thank you for the information. Let me process that...",
"I see what you're asking. Here's my response..."
];
return responses[Math.floor(Math.random() * responses.length)];
}
clearChat() {
if (confirm('Are you sure you want to clear all messages?')) {
document.getElementById('chatMessages').innerHTML = `
<div class="message assistant">
<div class="message-avatar">
<i class="bi bi-robot"></i>
</div>
<div class="message-content">
<div class="message-text">Hello! I'm your AI assistant. How can I help you today?</div>
<div class="message-time">Just now</div>
</div>
</div>
`;
this.messages = [];
}
}
autoResizeTextarea() {
const textarea = document.getElementById('chatInput');
textarea.style.height = 'auto';
textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
}
// Voice Recording Functions
async initializeRecorder() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this.mediaRecorder = new MediaRecorder(stream);
this.mediaRecorder.ondataavailable = (event) => {
this.audioChunks.push(event.data);
};
this.mediaRecorder.onstop = () => {
this.saveRecording();
};
} catch (error) {
console.error('Error accessing microphone:', error);
this.showStatus('Microphone access denied', 'error');
}
}
startRecording() {
if (!this.mediaRecorder) {
this.showStatus('Microphone not available', 'error');
return;
}
this.audioChunks = [];
this.mediaRecorder.start();
this.isRecording = true;
this.recordingStartTime = Date.now();
// Update UI
document.getElementById('recordBtn').disabled = true;
document.getElementById('stopBtn').disabled = false;
document.getElementById('recordingStatus').classList.add('active');
// Start timer
this.recordingTimer = setInterval(() => {
this.updateRecordingTime();
}, 1000);
this.showStatus('Recording started...', 'success');
}
stopRecording() {
if (!this.isRecording) return;
this.mediaRecorder.stop();
this.isRecording = false;
// Update UI
document.getElementById('recordBtn').disabled = false;
document.getElementById('stopBtn').disabled = true;
document.getElementById('playBtn').disabled = false;
document.getElementById('recordingStatus').classList.remove('active');
// Stop timer
clearInterval(this.recordingTimer);
this.showStatus('Recording stopped', 'success');
}
updateRecordingTime() {
const elapsed = Math.floor((Date.now() - this.recordingStartTime) / 1000);
const minutes = Math.floor(elapsed / 60);
const seconds = elapsed % 60;
const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
document.querySelector('.recording-time').textContent = timeString;
}
saveRecording() {
const audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' });
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `recording-${timestamp}.wav`;
// Create download link (in real implementation, upload to server)
const url = URL.createObjectURL(audioBlob);
const recording = {
name: filename,
url: url,
size: audioBlob.size,
timestamp: new Date()
};
this.recordings.push(recording);
this.updateRecordingsTree();
this.showStatus(`Recording saved as ${filename}`, 'success');
}
playLastRecording() {
if (this.recordings.length === 0) {
this.showStatus('No recordings available', 'warning');
return;
}
const lastRecording = this.recordings[this.recordings.length - 1];
const audio = new Audio(lastRecording.url);
audio.play();
this.showStatus(`Playing ${lastRecording.name}`, 'info');
}
// File Management Functions
showFileUploadModal() {
const modal = new bootstrap.Modal(document.getElementById('fileUploadModal'));
modal.show();
}
uploadFile() {
const fileInput = document.getElementById('fileInput');
const files = Array.from(fileInput.files);
if (files.length === 0) return;
// Simulate upload progress
const progressContainer = document.getElementById('uploadProgress');
const progressBar = progressContainer.querySelector('.progress-bar');
progressContainer.style.display = 'block';
let progress = 0;
const interval = setInterval(() => {
progress += Math.random() * 20;
if (progress >= 100) {
progress = 100;
clearInterval(interval);
// Add files to selected files
this.selectedFiles = [...this.selectedFiles, ...files];
this.showStatus(`${files.length} file(s) attached`, 'success');
// Close modal
bootstrap.Modal.getInstance(document.getElementById('fileUploadModal')).hide();
progressContainer.style.display = 'none';
progressBar.style.width = '0%';
fileInput.value = '';
}
progressBar.style.width = progress + '%';
}, 100);
}
// Tree and Context Menu Functions
loadRecordings() {
// Load sample recordings (replace with actual data loading)
this.recordings = [
{ name: 'sample1.mp3', size: 2097152, timestamp: new Date() },
{ name: 'sample2.wav', size: 5242880, timestamp: new Date() }
];
this.updateRecordingsTree();
}
updateRecordingsTree() {
const tree = document.getElementById('explorerTree');
const childrenContainer = tree.querySelector('.tree-item-children');
// Clear existing items except samples
childrenContainer.innerHTML = '';
// Add recordings
this.recordings.forEach(recording => {
const item = document.createElement('div');
item.className = 'tree-item file';
item.dataset.path = `/${recording.name}`;
item.innerHTML = `
<div class="tree-item-content">
<i class="bi bi-file-earmark-music"></i>
<span class="tree-item-name">${recording.name}</span>
<span class="tree-item-size">${this.formatFileSize(recording.size)}</span>
</div>
`;
childrenContainer.appendChild(item);
});
}
handleTreeItemClick(e) {
// Remove previous selection
document.querySelectorAll('.tree-item-content.selected').forEach(item => {
item.classList.remove('selected');
});
// Add selection to clicked item
e.target.closest('.tree-item-content').classList.add('selected');
}
setupContextMenu() {
const contextMenu = document.getElementById('contextMenu');
contextMenu.addEventListener('click', (e) => {
const action = e.target.dataset.action;
if (action) {
this.handleContextAction(action);
this.hideContextMenu();
}
});
}
showContextMenu(e) {
const contextMenu = document.getElementById('contextMenu');
contextMenu.style.display = 'block';
contextMenu.style.left = e.pageX + 'px';
contextMenu.style.top = e.pageY + 'px';
}
hideContextMenu() {
document.getElementById('contextMenu').style.display = 'none';
}
handleContextAction(action) {
const selectedItem = document.querySelector('.tree-item-content.selected');
if (!selectedItem) return;
const filename = selectedItem.querySelector('.tree-item-name').textContent;
switch (action) {
case 'transcribe':
this.showStatus(`Transcribing ${filename}...`, 'info');
break;
case 'translate':
this.showStatus(`Translating ${filename}...`, 'info');
break;
case 'open':
this.showStatus(`Opening ${filename}...`, 'info');
break;
case 'move':
this.showStatus(`Moving ${filename}...`, 'info');
break;
case 'rename':
this.renameFile(filename);
break;
case 'export':
this.exportFile(filename);
break;
}
}
renameFile(oldName) {
const newName = prompt('Enter new name:', oldName);
if (newName && newName !== oldName) {
this.showStatus(`Renamed ${oldName} to ${newName}`, 'success');
// Update the recording name in the array and refresh tree
const recording = this.recordings.find(r => r.name === oldName);
if (recording) {
recording.name = newName;
this.updateRecordingsTree();
}
}
}
exportFile(filename) {
const recording = this.recordings.find(r => r.name === filename);
if (recording && recording.url) {
const a = document.createElement('a');
a.href = recording.url;
a.download = filename;
a.click();
this.showStatus(`Exported ${filename}`, 'success');
}
}
createNewFolder() {
const folderName = prompt('Enter folder name:');
if (folderName) {
this.showStatus(`Created folder: ${folderName}`, 'success');
// In real implementation, create folder in tree
}
}
refreshRecordings() {
this.showStatus('Refreshing recordings...', 'info');
this.loadRecordings();
}
// Voice Input Functions
toggleVoiceInput() {
const button = document.getElementById('voiceInput');
if (button.classList.contains('voice-input-active')) {
this.stopVoiceInput();
} else {
this.startVoiceInput();
}
}
startVoiceInput() {
const button = document.getElementById('voiceInput');
button.classList.add('voice-input-active');
button.innerHTML = '<i class="bi bi-mic-fill"></i>';
this.showStatus('Listening...', 'info');
// Simulate voice recognition (replace with actual implementation)
setTimeout(() => {
this.stopVoiceInput();
document.getElementById('chatInput').value = 'This is a voice input example';
this.autoResizeTextarea();
}, 3000);
}
stopVoiceInput() {
const button = document.getElementById('voiceInput');
button.classList.remove('voice-input-active');
button.innerHTML = '<i class="bi bi-mic"></i>';
this.showStatus('Voice input stopped', 'info');
}
// Utility Functions
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
showStatus(message, type = 'info') {
const statusElement = document.getElementById('chatStatus');
statusElement.textContent = message;
statusElement.className = `chat-status text-${type === 'error' ? 'danger' : type === 'success' ? 'success' : type === 'warning' ? 'warning' : 'info'}`;
// Clear status after 3 seconds
setTimeout(() => {
statusElement.textContent = '';
statusElement.className = 'chat-status';
}, 3000);
}
}
// Initialize chat widget when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.chatWidget = new ChatWidget();
});
// Export for external use
if (typeof module !== 'undefined' && module.exports) {
module.exports = ChatWidget;
}

View File

@@ -0,0 +1,576 @@
console.log('Heroprompt UI loaded');
let currentWs = localStorage.getItem('heroprompt-current-ws') || 'default';
let selected = [];
const el = (id) => document.getElementById(id);
async function api(url) {
try {
const r = await fetch(url);
if (!r.ok) {
console.warn(`API call failed: ${url} - ${r.status}`);
return { error: `HTTP ${r.status}` };
}
return await r.json();
}
catch (e) {
console.warn(`API call error: ${url}`, e);
return { error: 'request failed' };
}
}
async function post(url, data) {
const form = new FormData();
Object.entries(data).forEach(([k, v]) => form.append(k, v));
try {
const r = await fetch(url, { method: 'POST', body: form });
if (!r.ok) {
console.warn(`POST failed: ${url} - ${r.status}`);
return { error: `HTTP ${r.status}` };
}
return await r.json();
}
catch (e) {
console.warn(`POST error: ${url}`, e);
return { error: 'request failed' };
}
}
// Bootstrap modal helpers
function showModal(id) {
const modalEl = el(id);
if (modalEl) {
const modal = new bootstrap.Modal(modalEl);
modal.show();
}
}
function hideModal(id) {
const modalEl = el(id);
if (modalEl) {
const modal = bootstrap.Modal.getInstance(modalEl);
if (modal) modal.hide();
}
}
// Tab switching with Bootstrap
function switchTab(tabName) {
// Hide all tab panes
document.querySelectorAll('.tab-pane').forEach(pane => {
pane.style.display = 'none';
pane.classList.remove('active');
});
// Remove active class from all tabs
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
});
// Show selected tab pane
const targetPane = el(`tab-${tabName}`);
if (targetPane) {
targetPane.style.display = 'block';
targetPane.classList.add('active');
}
// Add active class to clicked tab
const targetTab = document.querySelector(`.tab[data-tab="${tabName}"]`);
if (targetTab) {
targetTab.classList.add('active');
}
}
// Initialize tab switching
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', function (e) {
e.preventDefault();
const tabName = this.getAttribute('data-tab');
switchTab(tabName);
});
});
});
// Checkbox-based collapsible tree
let nodeId = 0;
function renderTree(displayName, fullPath) {
const c = document.createElement('div');
c.className = 'tree';
const ul = document.createElement('ul');
ul.className = 'tree-root list-unstyled';
const root = buildDirNode(displayName, fullPath, true);
ul.appendChild(root);
c.appendChild(ul);
return c;
}
function buildDirNode(name, fullPath, expanded = false) {
const li = document.createElement('li');
li.className = 'dir mb-1';
const id = `tn_${nodeId++}`;
const toggle = document.createElement('input');
toggle.type = 'checkbox';
toggle.className = 'toggle d-none';
toggle.id = id;
if (expanded) toggle.checked = true;
const label = document.createElement('label');
label.htmlFor = id;
label.className = 'dir-label d-flex align-items-center text-decoration-none';
label.style.cursor = 'pointer';
const icon = document.createElement('span');
icon.className = 'chev me-1';
icon.innerHTML = expanded ? '📂' : '📁';
const text = document.createElement('span');
text.className = 'name flex-grow-1';
text.textContent = name;
label.appendChild(icon);
label.appendChild(text);
const add = document.createElement('button');
add.className = 'btn btn-sm btn-outline-primary ms-1';
add.textContent = '+';
add.title = 'Add directory to selection';
add.onclick = (e) => {
e.stopPropagation();
addDirToSelection(fullPath);
};
const children = document.createElement('ul');
children.className = 'children list-unstyled ms-3';
children.style.display = expanded ? 'block' : 'none';
toggle.addEventListener('change', async () => {
if (toggle.checked) {
children.style.display = 'block';
icon.innerHTML = '📂';
if (!li.dataset.loaded) {
await loadChildren(fullPath, children);
li.dataset.loaded = '1';
}
} else {
children.style.display = 'none';
icon.innerHTML = '📁';
}
});
// Load immediately if expanded by default
if (expanded) {
setTimeout(async () => {
await loadChildren(fullPath, children);
li.dataset.loaded = '1';
}, 0);
}
li.appendChild(toggle);
li.appendChild(label);
li.appendChild(add);
li.appendChild(children);
return li;
}
function createFileNode(name, fullPath) {
const li = document.createElement('li');
li.className = 'file d-flex align-items-center mb-1';
const icon = document.createElement('span');
icon.className = 'me-2';
icon.innerHTML = '📄';
const a = document.createElement('a');
a.href = '#';
a.className = 'text-decoration-none flex-grow-1';
a.textContent = name;
a.onclick = (e) => {
e.preventDefault();
previewFile(fullPath);
};
const add = document.createElement('button');
add.className = 'btn btn-sm btn-outline-primary ms-1';
add.textContent = '+';
add.title = 'Add file to selection';
add.onclick = (e) => {
e.stopPropagation();
addFileToSelection(fullPath);
};
li.appendChild(icon);
li.appendChild(a);
li.appendChild(add);
return li;
}
async function previewFile(filePath) {
const previewEl = el('preview');
if (!previewEl) return;
previewEl.innerHTML = '<div class="loading">Loading...</div>';
const r = await api(`/api/heroprompt/file?name=${currentWs}&path=${encodeURIComponent(filePath)}`);
if (r.error) {
previewEl.innerHTML = `<div class="error-message">Error: ${r.error}</div>`;
return;
}
previewEl.textContent = r.content || 'No content';
}
async function loadChildren(parentPath, ul) {
const r = await api(`/api/heroprompt/directory?name=${currentWs}&path=${encodeURIComponent(parentPath)}`);
if (r.error) {
ul.innerHTML = `<li class="text-danger small">${r.error}</li>`;
return;
}
ul.innerHTML = '';
for (const it of r.items || []) {
const full = parentPath.endsWith('/') ? parentPath + it.name : parentPath + '/' + it.name;
if (it.type === 'directory') {
ul.appendChild(buildDirNode(it.name, full, false));
} else {
ul.appendChild(createFileNode(it.name, full));
}
}
}
async function loadDir(p) {
const treeEl = el('tree');
if (!treeEl) return;
treeEl.innerHTML = '<div class="loading">Loading workspace...</div>';
const display = p.split('/').filter(Boolean).slice(-1)[0] || p;
treeEl.appendChild(renderTree(display, p));
updateSelectionList();
}
function updateSelectionList() {
const selCountEl = el('selCount');
const tokenCountEl = el('tokenCount');
const selectedEl = el('selected');
if (selCountEl) selCountEl.textContent = String(selected.length);
if (selectedEl) {
selectedEl.innerHTML = '';
if (selected.length === 0) {
selectedEl.innerHTML = '<li class="text-muted small">No files selected</li>';
} else {
for (const p of selected) {
const li = document.createElement('li');
li.className = 'd-flex justify-content-between align-items-center mb-1 p-2 border rounded';
const span = document.createElement('span');
span.className = 'small';
span.textContent = p;
const btn = document.createElement('button');
btn.className = 'btn btn-sm btn-outline-danger';
btn.textContent = '×';
btn.onclick = () => {
selected = selected.filter(x => x !== p);
updateSelectionList();
};
li.appendChild(span);
li.appendChild(btn);
selectedEl.appendChild(li);
}
}
}
// naive token estimator ~ 4 chars/token
const tokens = Math.ceil(selected.join('\n').length / 4);
if (tokenCountEl) tokenCountEl.textContent = String(Math.ceil(tokens));
}
function addToSelection(p) {
if (!selected.includes(p)) {
selected.push(p);
updateSelectionList();
}
}
async function addDirToSelection(p) {
const r = await fetch(`/api/heroprompt/workspaces/${currentWs}/dirs`, {
method: 'POST',
body: new URLSearchParams({ path: p })
});
const j = await r.json().catch(() => ({ error: 'request failed' }));
if (j && j.ok !== false && !j.error) {
if (!selected.includes(p)) selected.push(p);
updateSelectionList();
} else {
console.warn('Failed to add directory:', j.error || 'Unknown error');
}
}
async function addFileToSelection(p) {
if (selected.includes(p)) return;
const r = await fetch(`/api/heroprompt/workspaces/${currentWs}/files`, {
method: 'POST',
body: new URLSearchParams({ path: p })
});
const j = await r.json().catch(() => ({ error: 'request failed' }));
if (j && j.ok !== false && !j.error) {
selected.push(p);
updateSelectionList();
} else {
console.warn('Failed to add file:', j.error || 'Unknown error');
}
}
// Workspaces list + selector
async function reloadWorkspaces() {
const sel = el('workspaceSelect');
if (!sel) return;
sel.innerHTML = '<option>Loading...</option>';
const names = await api('/api/heroprompt/workspaces');
sel.innerHTML = '';
if (names.error || !Array.isArray(names)) {
sel.innerHTML = '<option>Error loading workspaces</option>';
console.warn('Failed to load workspaces:', names);
return;
}
for (const n of names) {
const opt = document.createElement('option');
opt.value = n;
opt.textContent = n;
sel.appendChild(opt);
}
// ensure current ws name exists or select first
if (names.includes(currentWs)) {
sel.value = currentWs;
} else if (names.length > 0) {
currentWs = names[0];
sel.value = currentWs;
localStorage.setItem('heroprompt-current-ws', currentWs);
}
}
// On initial load: pick current or first workspace and load its base
async function initWorkspace() {
const names = await api('/api/heroprompt/workspaces');
if (names.error || !Array.isArray(names) || names.length === 0) {
console.warn('No workspaces available');
const treeEl = el('tree');
if (treeEl) {
treeEl.innerHTML = '<div class="text-muted small">No workspaces available. Create one to get started.</div>';
}
return;
}
if (!currentWs || !names.includes(currentWs)) {
currentWs = names[0];
localStorage.setItem('heroprompt-current-ws', currentWs);
}
const sel = el('workspaceSelect');
if (sel) sel.value = currentWs;
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
const base = info?.base_path || '';
if (base) await loadDir(base);
}
// Initialize everything when DOM is ready
document.addEventListener('DOMContentLoaded', function () {
// Initialize workspaces
initWorkspace();
reloadWorkspaces();
// Workspace selector change handler
const workspaceSelect = el('workspaceSelect');
if (workspaceSelect) {
workspaceSelect.addEventListener('change', async (e) => {
currentWs = e.target.value;
localStorage.setItem('heroprompt-current-ws', currentWs);
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
const base = info?.base_path || '';
if (base) await loadDir(base);
});
}
// Create workspace modal handlers
const wsCreateBtn = el('wsCreateBtn');
if (wsCreateBtn) {
wsCreateBtn.addEventListener('click', () => {
const nameEl = el('wcName');
const pathEl = el('wcPath');
const errorEl = el('wcError');
if (nameEl) nameEl.value = '';
if (pathEl) pathEl.value = '';
if (errorEl) errorEl.textContent = '';
showModal('wsCreate');
});
}
const wcCreateBtn = el('wcCreate');
if (wcCreateBtn) {
wcCreateBtn.addEventListener('click', async () => {
const name = el('wcName')?.value?.trim() || '';
const path = el('wcPath')?.value?.trim() || '';
const errorEl = el('wcError');
if (!path) {
if (errorEl) errorEl.textContent = 'Path is required.';
return;
}
const formData = { base_path: path };
if (name) formData.name = name;
const resp = await post('/api/heroprompt/workspaces', formData);
if (resp.error) {
if (errorEl) errorEl.textContent = resp.error;
return;
}
currentWs = resp.name || currentWs;
localStorage.setItem('heroprompt-current-ws', currentWs);
await reloadWorkspaces();
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
const base = info?.base_path || '';
if (base) await loadDir(base);
hideModal('wsCreate');
});
}
// Refresh workspace handler
const refreshBtn = el('refreshWs');
if (refreshBtn) {
refreshBtn.addEventListener('click', async () => {
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
const base = info?.base_path || '';
if (base) await loadDir(base);
});
}
// Search handler
const searchBtn = el('doSearch');
if (searchBtn) {
searchBtn.onclick = async () => {
const q = el('search')?.value?.trim();
if (!q) return;
// For now, just show a message since search endpoint might not exist
const tree = el('tree');
if (tree) {
tree.innerHTML = '<div class="text-muted small">Search functionality coming soon...</div>';
}
};
}
// Copy prompt handler
const copyPromptBtn = el('copyPrompt');
if (copyPromptBtn) {
copyPromptBtn.addEventListener('click', async () => {
const text = el('promptText')?.value || '';
try {
const r = await fetch(`/api/heroprompt/workspaces/${currentWs}/prompt`, {
method: 'POST',
body: new URLSearchParams({ text })
});
const out = await r.text();
await navigator.clipboard.writeText(out);
// Show success feedback
const outputEl = el('promptOutput');
if (outputEl) {
outputEl.innerHTML = '<div class="success-message">Prompt copied to clipboard!</div>';
setTimeout(() => {
outputEl.innerHTML = '<div class="text-muted small">Generated prompt will appear here</div>';
}, 3000);
}
} catch (e) {
console.warn('copy prompt failed', e);
const outputEl = el('promptOutput');
if (outputEl) {
outputEl.innerHTML = '<div class="error-message">Failed to copy prompt</div>';
setTimeout(() => {
outputEl.innerHTML = '<div class="text-muted small">Generated prompt will appear here</div>';
}, 3000);
}
}
});
}
// Workspace details modal handler
const wsDetailsBtn = el('wsDetailsBtn');
if (wsDetailsBtn) {
wsDetailsBtn.addEventListener('click', async () => {
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
if (info && !info.error) {
const nameEl = el('wdName');
const pathEl = el('wdPath');
const errorEl = el('wdError');
if (nameEl) nameEl.value = info.name || currentWs;
if (pathEl) pathEl.value = info.base_path || '';
if (errorEl) errorEl.textContent = '';
showModal('wsDetails');
}
});
}
// Workspace manage modal handler
const openWsManageBtn = el('openWsManage');
if (openWsManageBtn) {
openWsManageBtn.addEventListener('click', async () => {
const list = el('wmList');
const err = el('wmError');
if (!list) return;
if (err) err.textContent = '';
list.innerHTML = '<div class="text-muted">Loading workspaces...</div>';
const names = await api('/api/heroprompt/workspaces');
list.innerHTML = '';
if (names.error || !Array.isArray(names)) {
list.innerHTML = '<div class="error-message">Failed to load workspaces</div>';
return;
}
for (const n of names) {
const item = document.createElement('div');
item.className = 'list-group-item d-flex justify-content-between align-items-center';
const span = document.createElement('span');
span.textContent = n;
const btn = document.createElement('button');
btn.className = 'btn btn-sm btn-primary';
btn.textContent = 'Use';
btn.onclick = async () => {
currentWs = n;
localStorage.setItem('heroprompt-current-ws', currentWs);
await reloadWorkspaces();
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
const base = info?.base_path || '';
if (base) await loadDir(base);
hideModal('wsManage');
};
item.appendChild(span);
item.appendChild(btn);
list.appendChild(item);
}
showModal('wsManage');
});
}
});

View File

@@ -0,0 +1,867 @@
/**
* HeroScript Editor JavaScript
* Handles code editing, syntax highlighting, script execution, and real-time logging
*/
class ResizablePanel {
constructor() {
this.isResizing = false;
this.resizeDirection = 'horizontal'; // 'horizontal' or 'vertical'
this.startX = 0;
this.startY = 0;
this.startWidth = 0;
this.startHeight = 0;
this.editorPanel = null;
this.logsPanel = null;
this.horizontalHandle = null;
this.verticalHandle = null;
this.currentLayout = 'horizontal'; // 'horizontal' or 'vertical'
this.init();
}
init() {
this.editorPanel = document.getElementById('editor-panel');
this.logsPanel = document.getElementById('logs-panel');
this.horizontalHandle = document.getElementById('horizontal-resize-handle');
this.verticalHandle = document.getElementById('vertical-resize-handle');
if (!this.editorPanel || !this.logsPanel) {
console.warn('ResizablePanel: Required elements not found');
return;
}
this.detectLayout();
this.setupResizeHandlers();
this.loadSavedSizes();
// Listen for window resize to detect layout changes
window.addEventListener('resize', () => {
this.detectLayout();
});
}
detectLayout() {
const containerStyle = window.getComputedStyle(this.editorPanel.parentElement);
const isVertical = containerStyle.flexDirection === 'column';
this.currentLayout = isVertical ? 'vertical' : 'horizontal';
// Show/hide appropriate resize handles
if (this.horizontalHandle) {
this.horizontalHandle.style.display = isVertical ? 'none' : 'flex';
}
if (this.verticalHandle) {
this.verticalHandle.style.display = isVertical ? 'flex' : 'none';
}
}
setupResizeHandlers() {
// Setup horizontal resize handle
if (this.horizontalHandle) {
this.horizontalHandle.addEventListener('mousedown', (e) => this.startResize(e, 'horizontal'));
this.horizontalHandle.addEventListener('touchstart', (e) => this.startResize(e.touches[0], 'horizontal'));
this.horizontalHandle.addEventListener('selectstart', (e) => e.preventDefault());
}
// Setup vertical resize handle
if (this.verticalHandle) {
this.verticalHandle.addEventListener('mousedown', (e) => this.startResize(e, 'vertical'));
this.verticalHandle.addEventListener('touchstart', (e) => this.startResize(e.touches[0], 'vertical'));
this.verticalHandle.addEventListener('selectstart', (e) => e.preventDefault());
}
// Global mouse/touch events
document.addEventListener('mousemove', (e) => this.doResize(e));
document.addEventListener('mouseup', () => this.stopResize());
document.addEventListener('touchmove', (e) => this.doResize(e.touches[0]));
document.addEventListener('touchend', () => this.stopResize());
}
startResize(e, direction) {
this.isResizing = true;
this.resizeDirection = direction;
this.startX = e.clientX;
this.startY = e.clientY;
if (direction === 'horizontal') {
this.startWidth = parseInt(window.getComputedStyle(this.logsPanel).width, 10);
this.horizontalHandle?.classList.add('dragging');
} else {
this.startHeight = parseInt(window.getComputedStyle(this.logsPanel).height, 10);
this.verticalHandle?.classList.add('dragging');
}
document.body.classList.add('resizing');
e.preventDefault();
}
doResize(e) {
if (!this.isResizing) return;
if (this.resizeDirection === 'horizontal') {
this.doHorizontalResize(e);
} else {
this.doVerticalResize(e);
}
e.preventDefault();
}
doHorizontalResize(e) {
const deltaX = this.startX - e.clientX; // Reversed because we're resizing from right
const newWidth = this.startWidth + deltaX;
// Get container width for percentage calculation
const containerWidth = this.editorPanel.parentElement.offsetWidth;
const minWidth = 200; // Minimum width for logs panel
const maxWidth = Math.min(600, containerWidth * 0.6); // Maximum 60% of container
// Constrain the width
const constrainedWidth = Math.max(minWidth, Math.min(maxWidth, newWidth));
// Apply the new width
this.logsPanel.style.width = constrainedWidth + 'px';
// Update editor panel flex to fill remaining space
const remainingWidth = containerWidth - constrainedWidth - 4; // 4px for resize handle
this.editorPanel.style.flex = 'none';
this.editorPanel.style.width = remainingWidth + 'px';
}
doVerticalResize(e) {
const deltaY = e.clientY - this.startY;
const newHeight = this.startHeight + deltaY;
// Get container height for percentage calculation
const containerHeight = this.editorPanel.parentElement.offsetHeight;
const minHeight = 150; // Minimum height for logs panel
const maxHeight = Math.min(400, containerHeight * 0.6); // Maximum 60% of container
// Constrain the height
const constrainedHeight = Math.max(minHeight, Math.min(maxHeight, newHeight));
// Apply the new height
this.logsPanel.style.height = constrainedHeight + 'px';
// Update editor panel to fill remaining space
const remainingHeight = containerHeight - constrainedHeight - 4; // 4px for resize handle
this.editorPanel.style.flex = 'none';
this.editorPanel.style.height = remainingHeight + 'px';
}
stopResize() {
if (!this.isResizing) return;
this.isResizing = false;
// Remove visual feedback
this.horizontalHandle?.classList.remove('dragging');
this.verticalHandle?.classList.remove('dragging');
document.body.classList.remove('resizing');
// Save the current sizes
this.saveSizes();
}
saveSizes() {
if (this.currentLayout === 'horizontal') {
const logsWidth = this.logsPanel.offsetWidth;
const editorWidth = this.editorPanel.offsetWidth;
localStorage.setItem('heroscript_logs_width', logsWidth.toString());
localStorage.setItem('heroscript_editor_width', editorWidth.toString());
} else {
const logsHeight = this.logsPanel.offsetHeight;
const editorHeight = this.editorPanel.offsetHeight;
localStorage.setItem('heroscript_logs_height', logsHeight.toString());
localStorage.setItem('heroscript_editor_height', editorHeight.toString());
}
}
loadSavedSizes() {
if (this.currentLayout === 'horizontal') {
const savedLogsWidth = localStorage.getItem('heroscript_logs_width');
const savedEditorWidth = localStorage.getItem('heroscript_editor_width');
if (savedLogsWidth && savedEditorWidth) {
const containerWidth = this.editorPanel.parentElement.offsetWidth;
const logsWidth = parseInt(savedLogsWidth, 10);
const editorWidth = parseInt(savedEditorWidth, 10);
// Validate that saved sizes fit in current container
if (logsWidth + editorWidth + 4 <= containerWidth) {
this.logsPanel.style.width = logsWidth + 'px';
this.editorPanel.style.flex = 'none';
this.editorPanel.style.width = editorWidth + 'px';
}
}
} else {
const savedLogsHeight = localStorage.getItem('heroscript_logs_height');
const savedEditorHeight = localStorage.getItem('heroscript_editor_height');
if (savedLogsHeight && savedEditorHeight) {
const containerHeight = this.editorPanel.parentElement.offsetHeight;
const logsHeight = parseInt(savedLogsHeight, 10);
const editorHeight = parseInt(savedEditorHeight, 10);
// Validate that saved sizes fit in current container
if (logsHeight + editorHeight + 4 <= containerHeight) {
this.logsPanel.style.height = logsHeight + 'px';
this.editorPanel.style.flex = 'none';
this.editorPanel.style.height = editorHeight + 'px';
}
}
}
}
resetToDefault() {
if (this.currentLayout === 'horizontal') {
// Reset to default horizontal sizes
this.logsPanel.style.width = '350px';
this.logsPanel.style.height = 'auto';
this.editorPanel.style.flex = '1';
this.editorPanel.style.width = 'auto';
this.editorPanel.style.height = 'auto';
// Clear saved horizontal sizes
localStorage.removeItem('heroscript_logs_width');
localStorage.removeItem('heroscript_editor_width');
} else {
// Reset to default vertical sizes
this.logsPanel.style.height = '250px';
this.logsPanel.style.width = '100%';
this.editorPanel.style.flex = '1';
this.editorPanel.style.height = 'auto';
this.editorPanel.style.width = 'auto';
// Clear saved vertical sizes
localStorage.removeItem('heroscript_logs_height');
localStorage.removeItem('heroscript_editor_height');
}
}
}
class HeroScriptEditor {
constructor() {
this.editor = null;
this.logsContainer = null;
this.autoScroll = true;
this.redisConnection = null;
this.currentSyntax = 'javascript';
this.resizablePanel = null;
this.init();
}
/**
* Initialize the HeroScript editor
*/
init() {
this.setupEditor();
this.setupLogging();
this.setupEventListeners();
this.connectToRedis();
this.applySyntaxHighlighting();
this.setupResizablePanels();
}
/**
* Setup resizable panels
*/
setupResizablePanels() {
this.resizablePanel = new ResizablePanel();
}
/**
* Setup the code editor
*/
setupEditor() {
this.editor = document.getElementById('script-editor');
this.logsContainer = document.getElementById('logs-content');
if (!this.editor || !this.logsContainer) {
console.error('HeroScript: Required elements not found');
return;
}
// Set initial content if empty
if (!this.editor.value.trim()) {
this.editor.value = this.getDefaultScript();
}
// Setup basic editor features
this.setupEditorFeatures();
}
/**
* Setup syntax highlighting
*/
setupEditorFeatures() {
// Handle syntax selection change
const syntaxSelect = document.getElementById('syntax-select');
if (syntaxSelect) {
syntaxSelect.addEventListener('change', (e) => {
this.currentSyntax = e.target.value;
this.addLogEntry('user', `Syntax changed to ${this.currentSyntax}`, 'info');
});
}
// Add tab support for better code editing
this.editor.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
e.preventDefault();
const start = this.editor.selectionStart;
const end = this.editor.selectionEnd;
// Insert tab character
this.editor.value = this.editor.value.substring(0, start) +
' ' +
this.editor.value.substring(end);
// Move cursor
this.editor.selectionStart = this.editor.selectionEnd = start + 4;
}
});
// Auto-resize editor based on content
this.editor.addEventListener('input', () => {
this.autoResizeEditor();
});
// Note: Scroll sync not needed with contenteditable approach
// Initial resize
this.autoResizeEditor();
}
/**
* Auto-resize editor to fit content
*/
autoResizeEditor() {
// Reset height to auto to get the correct scrollHeight
this.editor.style.height = 'auto';
// Set height based on content, with minimum height
const minHeight = 500;
const contentHeight = this.editor.scrollHeight;
this.editor.style.height = Math.max(minHeight, contentHeight) + 'px';
}
/**
* Apply syntax highlighting to the editor content
*/
applySyntaxHighlighting() {
// Replace textarea with a contenteditable div for better highlighting
this.replaceTextareaWithHighlightedEditor();
// Update highlighting when syntax changes
const syntaxSelect = document.getElementById('syntax-select');
if (syntaxSelect) {
syntaxSelect.addEventListener('change', () => {
this.currentSyntax = syntaxSelect.value;
this.updateHighlighting();
});
}
// Initial highlighting
this.updateHighlighting();
}
/**
* Replace textarea with a highlighted contenteditable div
*/
replaceTextareaWithHighlightedEditor() {
const container = document.getElementById('editor-container');
if (!container || !this.editor) return;
// Get current content
const content = this.editor.value || this.getDefaultScript();
// Create new highlighted editor
const highlightedEditor = document.createElement('pre');
highlightedEditor.id = 'highlighted-editor';
highlightedEditor.className = 'hljs';
highlightedEditor.style.cssText = `
margin: 0;
padding: 1.5rem;
border: none;
outline: none;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 16px;
line-height: 1.6;
background-color: var(--bg-primary) !important;
color: var(--text-primary) !important;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
min-height: 500px;
height: 100%;
width: 100%;
box-sizing: border-box;
resize: none;
`;
const codeElement = document.createElement('code');
codeElement.className = `language-${this.currentSyntax}`;
codeElement.contentEditable = 'true';
codeElement.textContent = content;
codeElement.style.cssText = `
display: block;
background: transparent !important;
padding: 0;
margin: 0;
outline: none;
border: none;
`;
highlightedEditor.appendChild(codeElement);
// Replace the textarea
this.editor.style.display = 'none';
container.appendChild(highlightedEditor);
// Update editor reference
this.highlightedEditor = highlightedEditor;
this.codeElement = codeElement;
// Add event listeners for the new editor
this.setupHighlightedEditorEvents();
}
/**
* Setup event listeners for the highlighted editor
*/
setupHighlightedEditorEvents() {
if (!this.codeElement) return;
// Update highlighting on input
this.codeElement.addEventListener('input', () => {
// Update the hidden textarea value
this.editor.value = this.codeElement.textContent;
// Debounce highlighting updates
clearTimeout(this.highlightTimeout);
this.highlightTimeout = setTimeout(() => {
this.updateHighlighting();
}, 300);
});
// Handle tab key for indentation
this.codeElement.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
e.preventDefault();
document.execCommand('insertText', false, ' ');
}
});
// Focus the new editor
this.codeElement.focus();
}
/**
* Update syntax highlighting
*/
updateHighlighting() {
if (!this.codeElement || !window.hljs) return;
const content = this.codeElement.textContent;
// Store cursor position
const selection = window.getSelection();
const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
const cursorOffset = range ? range.startOffset : 0;
// Update language class
this.codeElement.className = `language-${this.currentSyntax}`;
// Apply highlighting
window.hljs.highlightElement(this.codeElement);
// Restore cursor position
if (range && this.codeElement.firstChild) {
try {
const newRange = document.createRange();
const textNode = this.codeElement.firstChild;
const maxOffset = textNode.textContent ? textNode.textContent.length : 0;
newRange.setStart(textNode, Math.min(cursorOffset, maxOffset));
newRange.setEnd(textNode, Math.min(cursorOffset, maxOffset));
selection.removeAllRanges();
selection.addRange(newRange);
} catch (e) {
// Cursor restoration failed, that's okay
}
}
}
/**
* Setup logging system
*/
setupLogging() {
this.addLogEntry('system', 'HeroScript Editor initialized', 'info');
this.addLogEntry('system', 'Connecting to Redis queue: hero.gui.logs', 'info');
}
/**
* Setup event listeners
*/
setupEventListeners() {
// Run script button
const runButton = document.getElementById('run-script');
if (runButton) {
runButton.addEventListener('click', () => this.runScript());
}
// Clear logs button
const clearButton = document.getElementById('clear-logs');
if (clearButton) {
clearButton.addEventListener('click', () => this.clearLogs());
}
// Save script button
const saveButton = document.getElementById('save-script');
if (saveButton) {
saveButton.addEventListener('click', () => this.saveScript());
}
// Auto-scroll toggle
const autoScrollButton = document.getElementById('auto-scroll');
if (autoScrollButton) {
autoScrollButton.addEventListener('click', () => this.toggleAutoScroll());
}
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey)) {
switch (e.key) {
case 'Enter':
e.preventDefault();
this.runScript();
break;
case 's':
e.preventDefault();
this.saveScript();
break;
case 'l':
e.preventDefault();
this.clearLogs();
break;
}
}
});
}
/**
* Connect to Redis queue for real-time logging
*/
connectToRedis() {
// Simulate Redis connection with WebSocket or Server-Sent Events
// In a real implementation, you'd connect to a WebSocket endpoint
// that subscribes to the Redis queue
this.simulateRedisConnection();
}
/**
* Simulate Redis connection for demo purposes
*/
simulateRedisConnection() {
// Update connection status
this.updateConnectionStatus('connected');
// Generate demo logs with different categories
const logCategories = [
{ cat: 'system', messages: ['service started', 'health check ok', 'memory usage normal'], color: 'blue' },
{ cat: 'database', messages: ['connection established', 'query executed', 'backup completed'], color: 'green' },
{ cat: 'api', messages: ['request processed', 'rate limit applied', 'cache hit'], color: 'cyan' },
{ cat: 'security', messages: ['auth successful', 'token refreshed', 'access granted'], color: 'yellow' },
{ cat: 'error', messages: ['connection timeout', 'invalid request', 'service unavailable'], color: 'red' },
{ cat: 'warning', messages: ['high memory usage', 'slow query detected', 'retry attempt'], color: 'orange' }
];
// Generate logs every 2-5 seconds
setInterval(() => {
if (Math.random() < 0.7) { // 70% chance
const category = logCategories[Math.floor(Math.random() * logCategories.length)];
const message = category.messages[Math.floor(Math.random() * category.messages.length)];
this.addCompactLogEntry(category.cat, message, category.color);
}
}, Math.random() * 3000 + 2000); // 2-5 seconds
}
/**
* Update connection status indicator
*/
updateConnectionStatus(status) {
const statusElement = document.getElementById('connection-status');
if (!statusElement) return;
statusElement.className = 'badge';
switch (status) {
case 'connected':
statusElement.classList.add('bg-success');
statusElement.textContent = 'Connected';
break;
case 'connecting':
statusElement.classList.add('bg-warning');
statusElement.textContent = 'Connecting...';
break;
case 'disconnected':
statusElement.classList.add('bg-danger');
statusElement.textContent = 'Disconnected';
break;
}
}
/**
* Run the script in the editor
*/
async runScript() {
const script = this.editor.value.trim();
if (!script) {
this.addLogEntry('user', 'No script to execute', 'warning');
return;
}
this.addLogEntry('user', 'Starting script execution...', 'info');
try {
// Simulate script execution
await this.executeScript(script);
} catch (error) {
this.addLogEntry('user', `Script execution failed: ${error.message}`, 'error');
}
}
/**
* Execute the script (simulation)
*/
async executeScript(script) {
// Simulate script execution with delays
const lines = script.split('\n').filter(line => line.trim());
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (!line || line.startsWith('//')) continue;
// Simulate processing time
await new Promise(resolve => setTimeout(resolve, 200 + Math.random() * 300));
// Simulate different types of output
if (line.includes('console.log')) {
const match = line.match(/console\.log\(['"`](.+?)['"`]\)/);
if (match) {
this.addLogEntry('script', match[1], 'info');
}
} else if (line.includes('error') || line.includes('Error')) {
this.addLogEntry('script', `Error in line: ${line}`, 'error');
} else if (line.includes('warn')) {
this.addLogEntry('script', `Warning in line: ${line}`, 'warning');
} else {
this.addLogEntry('script', `Executed: ${line}`, 'success');
}
}
this.addLogEntry('user', 'Script execution completed', 'success');
}
/**
* Save the current script
*/
saveScript() {
const script = this.editor.value;
// Simulate saving to server
this.addLogEntry('user', 'Saving script...', 'info');
setTimeout(() => {
// Save to localStorage for demo
localStorage.setItem('heroscript_content', script);
localStorage.setItem('heroscript_syntax', this.currentSyntax);
this.addLogEntry('user', 'Script saved successfully', 'success');
}, 500);
}
/**
* Load saved script
*/
loadScript() {
const savedScript = localStorage.getItem('heroscript_content');
const savedSyntax = localStorage.getItem('heroscript_syntax');
if (savedScript) {
this.editor.value = savedScript;
this.addLogEntry('user', 'Script loaded from storage', 'info');
}
if (savedSyntax) {
this.currentSyntax = savedSyntax;
const syntaxSelect = document.getElementById('syntax-select');
if (syntaxSelect) {
syntaxSelect.value = savedSyntax;
}
}
// Auto-resize after loading
this.autoResizeEditor();
}
/**
* Clear all logs
*/
clearLogs() {
if (this.logsContainer) {
this.logsContainer.innerHTML = '';
this.addLogEntry('user', 'Logs cleared', 'info');
}
}
/**
* Toggle auto-scroll functionality
*/
toggleAutoScroll() {
this.autoScroll = !this.autoScroll;
const button = document.getElementById('auto-scroll');
if (button) {
button.setAttribute('data-active', this.autoScroll.toString());
button.innerHTML = this.autoScroll
? '<i class="fas fa-arrow-down"></i> Auto-scroll'
: '<i class="fas fa-pause"></i> Manual';
}
this.addLogEntry('user', `Auto-scroll ${this.autoScroll ? 'enabled' : 'disabled'}`, 'info');
}
/**
* Add a compact log entry in the format: cat: loginfo
*/
addCompactLogEntry(category, message, color = 'blue') {
if (!this.logsContainer) return;
const logEntry = document.createElement('div');
logEntry.className = `log-entry compact new`;
logEntry.innerHTML = `
<span class="log-category" style="color: ${this.getLogColor(color)}">${this.escapeHtml(category)}:</span>
<span class="log-message">${this.escapeHtml(message)}</span>
`;
this.logsContainer.appendChild(logEntry);
// Remove 'new' class after animation
setTimeout(() => {
logEntry.classList.remove('new');
}, 300);
// Auto-scroll if enabled
if (this.autoScroll) {
this.scrollToBottom();
}
// Limit log entries to prevent memory issues
const maxEntries = 500;
const entries = this.logsContainer.children;
if (entries.length > maxEntries) {
for (let i = 0; i < entries.length - maxEntries; i++) {
entries[i].remove();
}
}
}
/**
* Add a log entry to the logs panel (legacy format for system messages)
*/
addLogEntry(source, message, level = 'info') {
// Use compact format for better display
const colorMap = {
'info': 'blue',
'success': 'green',
'warning': 'orange',
'error': 'red',
'debug': 'gray'
};
this.addCompactLogEntry(source, message, colorMap[level] || 'blue');
}
/**
* Get color value for log categories
*/
getLogColor(colorName) {
const colors = {
'red': '#ff4444',
'blue': '#4488ff',
'green': '#44ff44',
'yellow': '#ffff44',
'orange': '#ff8844',
'cyan': '#44ffff',
'purple': '#ff44ff',
'gray': '#888888'
};
return colors[colorName] || colors['blue'];
}
/**
* Scroll logs to bottom
*/
scrollToBottom() {
const container = document.getElementById('logs-container');
if (container) {
container.scrollTop = container.scrollHeight;
}
}
/**
* Escape HTML to prevent XSS
*/
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Get default script content
*/
getDefaultScript() {
return `// Welcome to HeroScript Editor!
// This is a powerful script execution environment
console.log('Hello from HeroScript!');
// Example: Simple loop with logging
for (let i = 1; i <= 5; i++) {
console.log(\`Step \${i}: Processing...\`);
}
// Example: Conditional logic
if (new Date().getHours() < 12) {
console.log('Good morning!');
} else {
console.log('Good afternoon!');
}
console.log('Script execution completed!');
// Try editing this script and click "Run Script" to see it in action
// Use Ctrl+Enter to run, Ctrl+S to save, Ctrl+L to clear logs`;
}
}
/**
* Initialize HeroScript Editor when DOM is ready
*/
document.addEventListener('DOMContentLoaded', () => {
// Only initialize if we're on the HeroScript page
if (document.getElementById('script-editor')) {
window.heroScriptEditor = new HeroScriptEditor();
// Load any saved script
window.heroScriptEditor.loadScript();
console.log('HeroScript Editor initialized');
}
});
/**
* Export for external use
*/
if (typeof module !== 'undefined' && module.exports) {
module.exports = { HeroScriptEditor };
}