klio 1.4.9 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,379 +3,920 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Klio GUI</title>
7
- <script src="https://cdn.tailwindcss.com"></script>
6
+ <title>Klio</title>
7
+ <link rel="stylesheet" href="/tailwind.generated.css">
8
8
  <style>
9
- body {
10
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
9
+ #sidebar {
10
+ transform: translateX(-100%);
11
11
  }
12
- .command-input {
13
- font-family: 'Courier New', monospace;
12
+ #sidebar.is-open {
13
+ transform: translateX(0);
14
14
  }
15
- .output-area {
16
- font-family: 'Courier New', monospace;
17
- white-space: pre-wrap;
18
- }
19
- .nav-link {
20
- transition: all 0.2s ease;
21
- }
22
- .nav-link:hover {
23
- background-color: rgba(59, 130, 246, 0.1);
24
- }
25
- .nav-link.active {
26
- background-color: rgba(59, 130, 246, 0.2);
27
- border-left: 3px solid #3b82f6;
15
+ @media (min-width: 768px) {
16
+ #sidebar {
17
+ transform: translateX(0);
18
+ }
28
19
  }
29
20
  </style>
30
21
  </head>
31
- <body class="bg-gray-50 min-h-screen">
32
- <div class="flex min-h-screen">
33
- <!-- Sidebar Navigation -->
34
- <div class="w-64 bg-white border-r border-gray-200 flex flex-col">
35
- <div class="p-4 border-b border-gray-200">
36
- <h1 class="text-xl font-bold text-gray-800">Klio GUI</h1>
37
- <p class="text-sm text-gray-600">Web Interface</p>
22
+ <body class="bg-[#1e1e1e] text-[#e0e0e0] font-mono m-0">
23
+ <div class="flex h-screen overflow-hidden">
24
+ <!-- Mobile Sidebar Backdrop -->
25
+ <div id="sidebarBackdrop" class="fixed inset-0 bg-black/60 z-20 hidden md:hidden" onclick="closeSidebar()"></div>
26
+ <!-- Sidebar -->
27
+ <div id="sidebar" class="fixed left-0 top-0 bottom-0 w-[85vw] max-w-[300px] bg-[#1e1e1e] border-r border-[#3d3d3d] overflow-y-auto p-4 z-30 transition-transform duration-300 ease-in-out flex flex-col md:w-[250px]">
28
+ <div class="flex items-center justify-between mb-4 md:hidden">
29
+ <span class="text-sm text-gray-400">Menu</span>
30
+ <button id="sidebarClose" class="text-gray-400 hover:text-white" aria-label="Close menu" onclick="closeSidebar()">
31
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
32
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
33
+ </svg>
34
+ </button>
35
+ </div>
36
+ <div class="mb-6">
37
+ <h1 class="text-xl font-bold text-white mb-2">Klio</h1>
38
+ <p class="text-sm text-gray-400">Online Astrological CLI</p>
39
+ <div id="userInfo" class="mt-2 text-xs text-gray-400">
40
+ <span id="userId">Loading user...</span>
41
+ <button id="logoutBtn" class="ml-2 text-blue-400 hover:text-blue-300 cursor-pointer" onclick="logout()" style="display: none;">
42
+ (Logout)
43
+ </button>
44
+ </div>
38
45
  </div>
39
46
 
40
- <nav class="flex-1 p-4">
41
- <ul class="space-y-2">
42
- <li>
43
- <a href="#" onclick="showPage('runner')" id="nav-runner" class="nav-link flex items-center p-3 rounded-lg text-gray-700 active">
44
- <svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
45
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path>
46
- </svg>
47
- Command Runner
48
- </a>
49
- </li>
50
- <li>
51
- <a href="#" onclick="showPage('commands')" id="nav-commands" class="nav-link flex items-center p-3 rounded-lg text-gray-700">
52
- <svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
53
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
54
- </svg>
55
- Commands Reference
56
- </a>
57
- </li>
58
- <li>
59
- <a href="#" onclick="showPage('history')" id="nav-history" class="nav-link flex items-center p-3 rounded-lg text-gray-700">
60
- <svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
61
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
62
- </svg>
63
- Command History
64
- </a>
65
- </li>
66
- </ul>
47
+ <nav class="space-y-2">
48
+ <div class="text-sm">
49
+ <div class="mb-4">
50
+ <h3 class="text-gray-300 font-semibold mb-2">Example Commands</h3>
51
+ <div class="space-y-1">
52
+ <button onclick="insertCommand('moon')" class="w-full text-left rounded bg-[#3d3d3d] px-2 py-1 text-sm transition-colors hover:bg-[#4a4a4a]">
53
+ <code class="text-[#4dabf7]">moon</code>
54
+ </button>
55
+ <button onclick="insertCommand('--help')" class="w-full text-left rounded bg-[#3d3d3d] px-2 py-1 text-sm transition-colors hover:bg-[#4a4a4a]">
56
+ <code class="text-[#4dabf7]">--help</code>
57
+ </button>
58
+ <button onclick="insertCommand('--c')" class="w-full text-left rounded bg-[#3d3d3d] px-2 py-1 text-sm transition-colors hover:bg-[#4a4a4a]">
59
+ <code class="text-[#4dabf7]">--c</code> Critical degrees
60
+ </button>
61
+ <button onclick="insertCommand('--rx')" class="w-full text-left rounded bg-[#3d3d3d] px-2 py-1 text-sm transition-colors hover:bg-[#4a4a4a]">
62
+ <code class="text-[#4dabf7]">--rx</code> Retrograde planets
63
+ </button>
64
+ <button onclick="insertCommand('--el')" class="w-full text-left rounded bg-[#3d3d3d] px-2 py-1 text-sm transition-colors hover:bg-[#4a4a4a]">
65
+ <code class="text-[#4dabf7]">--el</code> Element distribution
66
+ </button>
67
+ <button onclick="insertCommand('--s')" class="w-full text-left rounded bg-[#3d3d3d] px-2 py-1 text-sm transition-colors hover:bg-[#4a4a4a]">
68
+ <code class="text-[#4dabf7]">--s</code> All planet positions
69
+ </button>
70
+ </div>
71
+ </div>
72
+
73
+ <div class="mb-4">
74
+ <h3 class="text-gray-300 font-semibold mb-2">Configuration</h3>
75
+ <div class="space-y-1">
76
+ <button onclick="insertCommand('--status')" class="w-full text-left rounded bg-[#3d3d3d] px-2 py-1 text-sm transition-colors hover:bg-[#4a4a4a]">
77
+ <code class="text-[#4dabf7]">--status</code>
78
+ </button>
79
+ <button onclick="insertCommand('--setup')" class="w-full text-left rounded bg-[#3d3d3d] px-2 py-1 text-sm transition-colors hover:bg-[#4a4a4a]">
80
+ <code class="text-[#4dabf7]">--setup</code>
81
+ </button>
82
+ <button onclick="insertCommand('--people')" class="w-full text-left rounded bg-[#3d3d3d] px-2 py-1 text-sm transition-colors hover:bg-[#4a4a4a]">
83
+ <code class="text-[#4dabf7]">--people</code>
84
+ </button>
85
+ </div>
86
+ </div>
87
+ </div>
67
88
  </nav>
68
-
69
- <div class="p-4 border-t border-gray-200">
70
- <div class="text-xs text-gray-500">
71
- <p>Klio GUI v0.1</p>
72
- <p>GUI Interface</p>
89
+ </div>
90
+
91
+ <!-- Main Content -->
92
+ <div class="flex-1 flex flex-col md:ml-[250px]">
93
+ <div class="sticky top-0 z-20 bg-[#1e1e1e] border-b border-[#3d3d3d]">
94
+ <div class="flex items-center gap-3 px-4 py-2">
95
+ <button id="menuToggle" class="md:hidden text-gray-200 hover:text-white" aria-label="Open menu">
96
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
97
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
98
+ </svg>
99
+ </button>
100
+ <div id="webcontainerStatus" class="ml-auto text-xs text-red-200 bg-[#2d2d2d] border border-[#3d3d3d] rounded-full px-3 py-1">
101
+ <span id="wcStatusText">Checking WebContainer support...</span>
102
+ </div>
73
103
  </div>
74
104
  </div>
105
+ <div class="flex-1 flex flex-col m-4 bg-[#2d2d2d] rounded-lg shadow-[0_4px_20px_rgba(0,0,0,0.5)] min-h-0">
106
+ <div class="bg-[#3d3d3d] rounded-t-lg px-3 py-2 flex items-center gap-2">
107
+ <div class="w-3 h-3 rounded-full bg-[#ff5f56]"></div>
108
+ <div class="w-3 h-3 rounded-full bg-[#ffbd2e]"></div>
109
+ <div class="w-3 h-3 rounded-full bg-[#27ca3f]"></div>
110
+ <button id="copyTerminalBtn" class="ml-auto text-xs text-gray-200 bg-[#4a4a4a] hover:bg-[#555] px-2 py-1 rounded">Copy</button>
111
+ </div>
112
+
113
+ <div id="terminalContent" class="flex-1 min-h-0 overflow-y-auto p-4 select-text">
114
+ <div class="flex items-center gap-2 mb-1">
115
+ <span class="text-[#4dabf7]">klio@online:~$</span>
116
+ <span class="text-gray-400">Welcome to Klio Online</span>
117
+ </div>
118
+ <div class="flex items-center gap-2 mb-1">
119
+ <span class="text-[#4dabf7]">klio@online:~$</span>
120
+ <span class="text-gray-400">Type astrological commands and press Enter or run --help for example commands</span>
121
+ </div>
122
+ <div class="flex items-center gap-2 mb-1">
123
+ <span class="text-[#4dabf7]">klio@online:~$</span>
124
+ <span class="text-gray-400">Example: --el</span>
125
+ </div>
126
+ <div class="flex items-center gap-2 mt-4">
127
+ <span class="text-[#4dabf7]">klio@online:~$</span>
128
+ <input
129
+ type="text"
130
+ id="terminalInput"
131
+ class="flex-1 min-w-0 bg-transparent text-[#e0e0e0] outline-none font-mono"
132
+ autocomplete="off"
133
+ autocorrect="off"
134
+ autocapitalize="off"
135
+ spellcheck="false"
136
+ >
137
+ <span class="inline-block w-2.5 h-4 bg-[#e0e0e0] align-middle animate-pulse"></span>
138
+ </div>
139
+ </div>
140
+ </div>
141
+ </div>
142
+ </div>
143
+
144
+ <!-- Help Panel Toggle -->
145
+ <button id="helpToggle" class="fixed right-0 top-1/2 -translate-y-1/2 translate-x-0 bg-[#4dabf7] text-white border-0 px-2 py-2 rounded-l-md cursor-pointer transition-transform duration-300 ease-in-out [writing-mode:vertical-rl] [text-orientation:mixed]" onclick="toggleHelp()">
146
+ Help & Commands
147
+ </button>
148
+
149
+ <!-- Help Panel -->
150
+ <div id="helpPanel" class="fixed right-0 top-0 bottom-0 w-[85vw] md:w-[300px] bg-[#2d2d2d] border-l-2 border-[#4dabf7] p-4 overflow-y-auto transform translate-x-full transition-transform duration-300 ease-in-out">
151
+ <div class="flex justify-between items-center mb-4">
152
+ <h2 class="text-lg font-bold text-white">Command Reference</h2>
153
+ <button onclick="toggleHelp()" class="text-gray-400 hover:text-white">
154
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
155
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
156
+ </svg>
157
+ </button>
75
158
  </div>
76
159
 
77
- <!-- Main Content -->
78
- <div class="flex-1 flex flex-col">
79
- <!-- Header -->
80
- <header class="bg-white border-b border-gray-200 p-4">
81
- <div class="flex items-center justify-between">
82
- <h1 class="text-lg font-semibold text-gray-800" id="page-title">Command Runner</h1>
160
+ <div class="space-y-4 text-sm">
161
+ <div>
162
+ <h3 class="text-white font-semibold mb-2">Basic Commands</h3>
163
+ <div class="space-y-2">
164
+ <div class="rounded bg-[#3d3d3d] px-2 py-1 text-sm transition-colors hover:bg-[#4a4a4a] cursor-pointer" onclick="insertCommand('--help')">
165
+ <code class="text-[#4dabf7]">--help</code> - Show help
166
+ </div>
167
+ <div class="rounded bg-[#3d3d3d] px-2 py-1 text-sm transition-colors hover:bg-[#4a4a4a] cursor-pointer" onclick="insertCommand('moon')">
168
+ <code class="text-[#4dabf7]">moon</code> - Moon information
169
+ </div>
170
+ <div class="rounded bg-[#3d3d3d] px-2 py-1 text-sm transition-colors hover:bg-[#4a4a4a] cursor-pointer" onclick="insertCommand('sun')">
171
+ <code class="text-[#4dabf7]">sun</code> - Sun information
172
+ </div>
173
+ <div class="rounded bg-[#3d3d3d] px-2 py-1 text-sm transition-colors hover:bg-[#4a4a4a] cursor-pointer" onclick="insertCommand('mars --a')">
174
+ <code class="text-[#4dabf7]">mars --a</code> - Mars with aspects
175
+ </div>
83
176
  </div>
84
- </header>
177
+ </div>
85
178
 
86
- <!-- Page Content -->
87
- <main class="flex-1 overflow-y-auto p-6">
88
- <!-- Command Runner Page -->
89
- <div id="runner-page">
90
- <div class="bg-white shadow-sm rounded-lg p-6 mb-6">
91
- <div class="mb-4">
92
- <label for="commandInput" class="block text-sm font-medium text-gray-700 mb-2">Command</label>
93
- <div class="flex gap-2">
94
- <input
95
- type="text"
96
- id="commandInput"
97
- placeholder="e.g., moon --s"
98
- class="flex-1 command-input px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
99
- >
100
- <button
101
- onclick="runCommand()"
102
- class="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
103
- >
104
- Run
105
- </button>
106
- </div>
107
- </div>
108
- <div class="text-sm text-gray-600">
109
- <p class="mb-2">Examples:</p>
110
- <div class="flex flex-wrap gap-4 text-xs">
111
- <button onclick="commandInput.value = '--v 3 saturn neptune'; commandInput.focus()" class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200">moon --s</button>
112
- <button onclick="commandInput.value = '--rx --i'; commandInput.focus()" class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200">--rx</button>
113
- <button onclick="commandInput.value = '--c'; commandInput.focus()" class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200">--c</button>
114
- <button onclick="commandInput.value = 'mars venus --k'; commandInput.focus()" class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200">mars venus --k</button>
115
- <button onclick="commandInput.value = '--el'; commandInput.focus()" class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200">--el</button>
116
- <button onclick="commandInput.value = '--s'; commandInput.focus()" class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200">--el</button>
117
- <button onclick="commandInput.value = '--a'; commandInput.focus()" class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200">--el</button>
118
-
119
-
120
- </div>
121
- </div>
179
+ <div>
180
+ <h3 class="text-white font-semibold mb-2">Analysis Commands</h3>
181
+ <div class="space-y-2">
182
+ <div class="rounded bg-[#3d3d3d] px-2 py-1 text-sm transition-colors hover:bg-[#4a4a4a] cursor-pointer" onclick="insertCommand('--c')">
183
+ <code class="text-[#4dabf7]">--c</code> - Critical degrees
122
184
  </div>
123
-
124
- <div class="bg-white shadow-sm rounded-lg p-6">
125
- <h2 class="text-lg font-semibold text-gray-800 mb-4">Output</h2>
126
- <div id="outputArea" class="output-area bg-gray-100 p-4 rounded border min-h-[300px]">
127
- Command output will appear here...
128
- </div>
185
+ <div class="rounded bg-[#3d3d3d] px-2 py-1 text-sm transition-colors hover:bg-[#4a4a4a] cursor-pointer" onclick="insertCommand('--rx')">
186
+ <code class="text-[#4dabf7]">--rx</code> - Retrograde planets
187
+ </div>
188
+ <div class="rounded bg-[#3d3d3d] px-2 py-1 text-sm transition-colors hover:bg-[#4a4a4a] cursor-pointer" onclick="insertCommand('--el')">
189
+ <code class="text-[#4dabf7]">--el</code> - Element distribution
190
+ </div>
191
+ <div class="rounded bg-[#3d3d3d] px-2 py-1 text-sm transition-colors hover:bg-[#4a4a4a] cursor-pointer" onclick="insertCommand('--af')">
192
+ <code class="text-[#4dabf7]">--af</code> - Aspect figures
129
193
  </div>
130
194
  </div>
131
-
132
- <!-- Commands Reference Page -->
133
- <div id="commands-page" class="hidden">
134
- <div class="bg-white shadow-sm rounded-lg p-6">
135
- <h2 class="text-lg font-semibold text-gray-800 mb-4">Available Commands</h2>
136
- <div class="prose max-w-none text-sm">
137
- <h3 class="text-base font-semibold text-gray-800 mt-6 mb-3">Basic Astrological Data</h3>
138
- <ul class="list-disc pl-6 space-y-2">
139
- <li><code class="bg-gray-100 px-2 py-1 rounded">klio [planet]</code> - Displays astrological data for a specific planet</li>
140
- <li><code class="bg-gray-100 px-2 py-1 rounded">klio [planet] --hs koch</code> - Shows planet data with house and house system</li>
141
- <li><code class="bg-gray-100 px-2 py-1 rounded">klio [planet] [planet2] --k</code> - Shows aspects between two planets</li>
142
- <li><code class="bg-gray-100 px-2 py-1 rounded">klio --a</code> - Shows all current aspects</li>
143
- <li><code class="bg-gray-100 px-2 py-1 rounded">klio --s</code> - Shows all planet positions</li>
144
- <li><code class="bg-gray-100 px-2 py-1 rounded">klio --s --hs placidus</code> - Different house systems</li>
145
- </ul>
146
-
147
- <h3 class="text-base font-semibold text-gray-800 mt-6 mb-3">House Systems</h3>
148
- <ul class="list-disc pl-6 space-y-2">
149
- <li><code class="bg-gray-100 px-2 py-1 rounded">--hs &lt;system&gt;</code> - Shows planet + house position (Placidus, Koch, Porphyry, Regiomontanus, Campanus, Equal, WholeSign, Gauquelin, Vehlow, Topocentric, Alcabitius, Morinus)</li>
150
- </ul>
151
-
152
- <h3 class="text-base font-semibold text-gray-800 mt-6 mb-3">Date and Time</h3>
153
- <ul class="list-disc pl-6 space-y-2">
154
- <li><code class="bg-gray-100 px-2 py-1 rounded">--d &lt;date&gt;</code> - Use a specific date (Format: DD.MM.YYYY or "DD.MM.YYYY HH:MM")</li>
155
- </ul>
156
-
157
- <h3 class="text-base font-semibold text-gray-800 mt-6 mb-3">Advanced Features</h3>
158
- <ul class="list-disc pl-6 space-y-2">
159
- <li><code class="bg-gray-100 px-2 py-1 rounded">--rx</code> - Shows all retrograde or stationary planets</li>
160
- <li><code class="bg-gray-100 px-2 py-1 rounded">--c</code> - Shows all planets on critical degrees</li>
161
- <li><code class="bg-gray-100 px-2 py-1 rounded">--el</code> - Shows element distribution of planets</li>
162
- <li><code class="bg-gray-100 px-2 py-1 rounded">--af</code> - Shows active aspect figures</li>
163
- <li><code class="bg-gray-100 px-2 py-1 rounded">--tr</code> - Shows personal transits based on birth data</li>
164
- </ul>
165
-
166
- <h3 class="text-base font-semibold text-gray-800 mt-6 mb-3">Past and Future Aspects</h3>
167
- <ul class="list-disc pl-6 space-y-2">
168
- <li><code class="bg-gray-100 px-2 py-1 rounded">--v &lt;count&gt; [planet1] [aspect-type] [planet2]</code> - Shows past aspects</li>
169
- <li><code class="bg-gray-100 px-2 py-1 rounded">--z &lt;count&gt; [planet1] [aspect-type] [planet2]</code> - Shows future aspects</li>
170
- </ul>
171
-
172
- <h3 class="text-base font-semibold text-gray-800 mt-6 mb-3">Configuration</h3>
173
- <ul class="list-disc pl-6 space-y-2">
174
- <li><code class="bg-gray-100 px-2 py-1 rounded">--setup</code> - Setup for a default chart</li>
175
- <li><code class="bg-gray-100 px-2 py-1 rounded">--status</code> - Shows the stored configuration data</li>
176
- <li><code class="bg-gray-100 px-2 py-1 rounded">--i</code> - Uses birth data from setup</li>
177
- <li><code class="bg-gray-100 px-2 py-1 rounded">--person &lt;id&gt; &lt;data&gt;</code> - Creates or updates any person</li>
178
- <li><code class="bg-gray-100 px-2 py-1 rounded">--people</code> - Lists all saved persons</li>
179
- </ul>
180
- </div>
195
+ </div>
196
+
197
+ <div>
198
+ <h3 class="text-white font-semibold mb-2">Chart Commands</h3>
199
+ <div class="space-y-2">
200
+ <div class="rounded bg-[#3d3d3d] px-2 py-1 text-sm transition-colors hover:bg-[#4a4a4a] cursor-pointer" onclick="insertCommand('--s')">
201
+ <code class="text-[#4dabf7]">--s</code> - All planet positions
202
+ </div>
203
+ <div class="rounded bg-[#3d3d3d] px-2 py-1 text-sm transition-colors hover:bg-[#4a4a4a] cursor-pointer" onclick="insertCommand('--s --hs placidus')">
204
+ <code class="text-[#4dabf7]">--s --hs placidus</code> - With house system
205
+ </div>
206
+ <div class="rounded bg-[#3d3d3d] px-2 py-1 text-sm transition-colors hover:bg-[#4a4a4a] cursor-pointer" onclick="insertCommand('--hl koch')">
207
+ <code class="text-[#4dabf7]">--hl koch</code> - House list
181
208
  </div>
182
209
  </div>
183
-
184
- <!-- Command History Page -->
185
- <div id="history-page" class="hidden">
186
- <div class="bg-white shadow-sm rounded-lg p-6">
187
- <h2 class="text-lg font-semibold text-gray-800 mb-4">Command History</h2>
188
- <div id="historyContainer" class="space-y-2 max-h-[500px] overflow-y-auto pr-2"></div>
210
+ </div>
211
+
212
+ <div>
213
+ <h3 class="text-white font-semibold mb-2">Configuration</h3>
214
+ <div class="space-y-2">
215
+ <div class="rounded bg-[#3d3d3d] px-2 py-1 text-sm transition-colors hover:bg-[#4a4a4a] cursor-pointer" onclick="insertCommand('--status')">
216
+ <code class="text-[#4dabf7]">--status</code> - Show config
217
+ </div>
218
+ <div class="rounded bg-[#3d3d3d] px-2 py-1 text-sm transition-colors hover:bg-[#4a4a4a] cursor-pointer" onclick="insertCommand('--people')">
219
+ <code class="text-[#4dabf7]">--people</code> - List people
189
220
  </div>
190
221
  </div>
191
- </main>
222
+ </div>
192
223
  </div>
193
224
  </div>
194
-
195
- <div id="toast" class="fixed bottom-4 right-4 bg-gray-800 text-white px-4 py-2 rounded-md shadow-lg opacity-0 transform translate-y-4 transition-all duration-300"></div>
196
-
225
+
226
+ <!-- WebContainer API -->
227
+ <script type="module">
228
+ try {
229
+ const { WebContainer } = await import('https://cdn.jsdelivr.net/npm/@webcontainer/api@1/+esm');
230
+
231
+ // Make WebContainer available globally for our service
232
+ window.WebContainer = WebContainer;
233
+ console.log('WebContainer API loaded successfully');
234
+
235
+ // Now load the WebContainer service
236
+ const { webcontainerService, WebContainerService } = await import('./webcontainerService.js');
237
+ window.webcontainerService = webcontainerService;
238
+ window.WebContainerService = WebContainerService;
239
+ console.log('WebContainer service loaded successfully');
240
+ } catch (error) {
241
+ console.error('Failed to load WebContainer API or service:', error);
242
+ // WebContainer will not be available
243
+
244
+ // Load fallback WebContainer service
245
+ const script = document.createElement('script');
246
+ script.src = 'webcontainerService.js';
247
+ script.type = 'module';
248
+ document.body.appendChild(script);
249
+ }
250
+ </script>
251
+
197
252
  <script>
253
+ const terminalContent = document.getElementById('terminalContent');
254
+ const terminalInput = document.getElementById('terminalInput');
255
+ const copyTerminalBtn = document.getElementById('copyTerminalBtn');
256
+ const helpPanel = document.getElementById('helpPanel');
257
+ const helpToggle = document.getElementById('helpToggle');
258
+ const sidebar = document.getElementById('sidebar');
259
+ const sidebarBackdrop = document.getElementById('sidebarBackdrop');
260
+ const menuToggle = document.getElementById('menuToggle');
261
+ const sidebarClose = document.getElementById('sidebarClose');
262
+
198
263
  let commandHistory = [];
199
- const maxHistory = 50;
264
+ let historyIndex = -1;
265
+ let webcontainerAvailable = false;
266
+
200
267
 
201
- // DOM elements
202
- const commandInput = document.getElementById('commandInput');
203
- const historyContainer = document.getElementById('historyContainer');
204
- const outputArea = document.getElementById('outputArea');
205
- const toastElement = document.getElementById('toast');
268
+ function openSidebar() {
269
+ if (!sidebar || !sidebarBackdrop) return;
270
+ sidebar.classList.add('is-open');
271
+ sidebarBackdrop.classList.remove('hidden');
272
+ }
206
273
 
207
- // Initialize the app
208
- function init() {
209
- loadHistory();
210
-
211
- // Add event listener for Enter key
212
- commandInput.addEventListener('keypress', (e) => {
213
- if (e.key === 'Enter') {
214
- runCommand();
274
+ function closeSidebar() {
275
+ if (!sidebar || !sidebarBackdrop) return;
276
+ sidebar.classList.remove('is-open');
277
+ sidebarBackdrop.classList.add('hidden');
278
+ }
279
+
280
+ function handleSidebarResize() {
281
+ if (window.innerWidth >= 768) {
282
+ if (sidebarBackdrop) {
283
+ sidebarBackdrop.classList.add('hidden');
215
284
  }
216
- });
285
+ if (sidebar) {
286
+ sidebar.classList.remove('is-open');
287
+ }
288
+ } else {
289
+ if (sidebar && !sidebar.classList.contains('is-open')) {
290
+ sidebar.classList.remove('is-open');
291
+ }
292
+ }
293
+ }
294
+
295
+ window.openSidebar = openSidebar;
296
+ window.closeSidebar = closeSidebar;
297
+
298
+
299
+ // Handle key down
300
+ function handleKeyDown(e) {
301
+ if (e.key === 'Enter') {
302
+ executeCommand();
303
+ } else if (e.key === 'ArrowUp') {
304
+ navigateHistory('up');
305
+ } else if (e.key === 'ArrowDown') {
306
+ navigateHistory('down');
307
+ }
308
+ }
309
+
310
+ // Handle key up to preserve text selection
311
+ function handleKeyUp(e) {
312
+ // Check if there's currently a text selection in the terminal content
313
+ const selection = window.getSelection();
314
+ const hasTerminalSelection = selection && selection.toString().length > 0 &&
315
+ terminalContent.contains(selection.anchorNode);
217
316
 
218
- // Focus on command input
219
- commandInput.focus();
317
+ // If there's an active selection in terminal content, don't steal focus
318
+ if (hasTerminalSelection) {
319
+ return;
320
+ }
321
+
322
+ // Only focus the input if no text is selected and it's a relevant key
323
+ if (e.key !== 'Shift' && e.key !== 'Control' && e.key !== 'Alt' && e.key !== 'Meta' &&
324
+ e.key !== 'Tab' && document.activeElement !== terminalInput) {
325
+ terminalInput.focus();
326
+ }
327
+ }
328
+
329
+ // Navigate command history
330
+ function navigateHistory(direction) {
331
+ if (direction === 'up') {
332
+ if (historyIndex < commandHistory.length - 1) {
333
+ historyIndex++;
334
+ terminalInput.value = commandHistory[historyIndex];
335
+ }
336
+ } else if (direction === 'down') {
337
+ if (historyIndex > 0) {
338
+ historyIndex--;
339
+ terminalInput.value = commandHistory[historyIndex];
340
+ } else if (historyIndex === 0) {
341
+ historyIndex = -1;
342
+ terminalInput.value = '';
343
+ }
344
+ }
345
+ }
346
+
347
+ // Execute command with WebContainer fallback
348
+ function shouldSyncConfigFromCommand(command) {
349
+ return command === '--setup' ||
350
+ command.includes('--person') ||
351
+ command.includes('--person1') ||
352
+ command.includes('--person2') ||
353
+ command.includes('--delete-person') ||
354
+ command.includes('--ai') ||
355
+ command.includes('--system');
220
356
  }
221
357
 
222
- // Page navigation function
223
- function showPage(pageName) {
224
- // Hide all pages
225
- document.getElementById('runner-page').classList.add('hidden');
226
- document.getElementById('commands-page').classList.add('hidden');
227
- document.getElementById('history-page').classList.add('hidden');
358
+ async function executeCommand() {
359
+ const command = terminalInput.value.trim();
360
+
361
+ if (!command) {
362
+ return;
363
+ }
364
+
365
+ // Add to history
366
+ if (command && !commandHistory.includes(command)) {
367
+ commandHistory.unshift(command);
368
+ if (commandHistory.length > 50) {
369
+ commandHistory.pop();
370
+ }
371
+ saveHistory();
372
+ }
373
+
374
+ // Reset history index
375
+ historyIndex = -1;
376
+
377
+ // Sync config from localStorage to WebContainer before executing command
378
+ await syncConfigToWebContainer();
379
+
380
+ // Create command line
381
+ const commandLine = document.createElement('div');
382
+ commandLine.className = 'flex items-center gap-2 mb-1';
383
+ commandLine.innerHTML = `<span class="text-[#4dabf7]">klio@online:~$</span> ${escapeHtml(command)}`;
384
+
385
+ // Add to terminal
386
+ terminalContent.insertBefore(commandLine, terminalInput.parentElement);
387
+
388
+ // Clear input
389
+ terminalInput.value = '';
390
+
391
+ // Create output line
392
+ const outputLine = document.createElement('div');
393
+ outputLine.className = 'flex items-center gap-2 mb-1';
394
+ outputLine.innerHTML = '<span class="text-[#4dabf7]">klio@online:~$</span> <span class="text-gray-400">Running command...</span>';
395
+
396
+ // Add to terminal
397
+ terminalContent.insertBefore(outputLine, terminalInput.parentElement);
228
398
 
229
- // Remove active state from all nav items
230
- document.getElementById('nav-runner').classList.remove('active');
231
- document.getElementById('nav-commands').classList.remove('active');
232
- document.getElementById('nav-history').classList.remove('active');
399
+ // Scroll to bottom
400
+ terminalContent.scrollTop = terminalContent.scrollHeight;
233
401
 
234
- // Show selected page
235
- if (pageName === 'runner') {
236
- document.getElementById('runner-page').classList.remove('hidden');
237
- document.getElementById('nav-runner').classList.add('active');
238
- document.getElementById('page-title').textContent = 'Command Runner';
239
- } else if (pageName === 'commands') {
240
- document.getElementById('commands-page').classList.remove('hidden');
241
- document.getElementById('nav-commands').classList.add('active');
242
- document.getElementById('page-title').textContent = 'Commands Reference';
243
- } else if (pageName === 'history') {
244
- document.getElementById('history-page').classList.remove('hidden');
245
- document.getElementById('nav-history').classList.add('active');
246
- document.getElementById('page-title').textContent = 'Command History';
402
+ try {
403
+ if (command === '--setup') {
404
+ outputLine.innerHTML = await runSetupFlow();
405
+ } else {
406
+ const result = await executeCommandInWebContainer(command);
407
+ outputLine.innerHTML = result;
408
+ }
409
+
410
+ if (shouldSyncConfigFromCommand(command)) {
411
+ await syncConfigFromWebContainer();
412
+ }
413
+ } catch (error) {
414
+ outputLine.innerHTML = `<span class="text-[#4dabf7]">klio@online:~$</span><pre class="whitespace-pre-wrap mb-4 text-red-400">Error: ${escapeHtml(error.message)}</pre>`;
415
+ }
416
+
417
+ // Scroll to bottom
418
+ terminalContent.scrollTop = terminalContent.scrollHeight;
419
+
420
+ // Focus input again
421
+ terminalInput.focus();
422
+ }
423
+
424
+ function normalizeOutput(rawOutput) {
425
+ return (rawOutput || '')
426
+ .replace(/\r\n/g, '\n')
427
+ .replace(/\n{2,}/g, '\n')
428
+ .replace(/^\n+|\n+$/g, '');
429
+ }
430
+
431
+ function formatCommandOutput(output, className) {
432
+ const normalizedOutput = normalizeOutput(output);
433
+ return `<pre class="whitespace-pre-wrap mb-4 ${className}">${escapeHtml(normalizedOutput)}</pre>`;
434
+ }
435
+
436
+ // Execute command in WebContainer
437
+ async function executeCommandInWebContainer(command) {
438
+ if (!window.webcontainerService || !window.WebContainerService ||
439
+ typeof window.WebContainerService.isSupported !== 'function' ||
440
+ typeof window.webcontainerService.initialize !== 'function' ||
441
+ typeof window.webcontainerService.executeCommand !== 'function') {
442
+ return `<pre class="whitespace-pre-wrap mb-4 text-yellow-400">⚠️ WebContainer service is not available.</pre>`;
443
+ }
444
+
445
+ if (!window.WebContainerService.isSupported()) {
446
+ return `<pre class="whitespace-pre-wrap mb-4 text-yellow-400">⚠️ WebContainer is not supported in this browser or context.</pre>`;
447
+ }
448
+
449
+ let isInitialized = false;
450
+ try {
451
+ isInitialized = await window.webcontainerService.initialize();
452
+ } catch (initError) {
453
+ return `<pre class="whitespace-pre-wrap mb-4 text-yellow-400">⚠️ WebContainer initialization failed: ${escapeHtml(initError.message)}</pre>`;
454
+ }
455
+
456
+ if (!isInitialized) {
457
+ return `<pre class="whitespace-pre-wrap mb-4 text-yellow-400">⚠️ WebContainer is not available in this environment.</pre>`;
458
+ }
459
+
460
+ const result = await window.webcontainerService.executeCommand(command);
461
+ if (result && result.success) {
462
+ return formatCommandOutput(result.output || '', 'text-green-400');
463
+ }
464
+
465
+ return formatCommandOutput(result && result.output ? result.output : 'WebContainer command failed.', 'text-red-400');
466
+ }
467
+
468
+ function parseCoordinates(rawValue) {
469
+ if (!rawValue) {
470
+ return null;
471
+ }
472
+ const match = rawValue.trim().match(/^([-+]?\d+\.?\d*),\s*([-+]?\d+\.?\d*)$/);
473
+ if (!match) {
474
+ return null;
475
+ }
476
+ return {
477
+ latitude: parseFloat(match[1]),
478
+ longitude: parseFloat(match[2])
479
+ };
480
+ }
481
+
482
+ async function geocodeLocation(locationName) {
483
+ const url = new URL('https://geocoding-api.open-meteo.com/v1/search');
484
+ url.searchParams.set('name', locationName);
485
+ url.searchParams.set('count', '1');
486
+ url.searchParams.set('language', 'en');
487
+ url.searchParams.set('format', 'json');
488
+
489
+ const response = await fetch(url.toString());
490
+ if (!response.ok) {
491
+ throw new Error('Geocoding request failed.');
492
+ }
493
+ const data = await response.json();
494
+ if (!data || !data.results || data.results.length === 0) {
495
+ return null;
496
+ }
497
+
498
+ const result = data.results[0];
499
+ return {
500
+ latitude: result.latitude,
501
+ longitude: result.longitude,
502
+ name: result.name,
503
+ country: result.country,
504
+ timezone: result.timezone
505
+ };
506
+ }
507
+
508
+ // LocalStorage config management functions
509
+ function saveConfigToLocalStorage(config) {
510
+ try {
511
+ localStorage.setItem('astrocliConfig', JSON.stringify(config));
512
+ return true;
513
+ } catch (error) {
514
+ console.error('Failed to save config to localStorage:', error);
515
+ return false;
516
+ }
517
+ }
518
+
519
+ function loadConfigFromLocalStorage() {
520
+ try {
521
+ const configData = localStorage.getItem('astrocliConfig');
522
+ return configData ? JSON.parse(configData) : null;
523
+ } catch (error) {
524
+ console.error('Failed to load config from localStorage:', error);
525
+ return null;
526
+ }
527
+ }
528
+
529
+ // Function to sync config from localStorage to WebContainer
530
+ async function syncConfigToWebContainer() {
531
+ if (!window.webcontainerService || !window.WebContainerService) {
532
+ return false;
533
+ }
534
+
535
+ try {
536
+ const config = loadConfigFromLocalStorage();
537
+ if (!config) {
538
+ return false;
539
+ }
540
+
541
+ const initialized = await window.webcontainerService.initialize();
542
+ if (!initialized) {
543
+ return false;
544
+ }
545
+
546
+ await window.webcontainerService.writeConfig(config);
547
+ return true;
548
+ } catch (error) {
549
+ console.error('Failed to sync config to WebContainer:', error);
550
+ return false;
551
+ }
552
+ }
553
+
554
+ async function syncConfigFromWebContainer() {
555
+ if (!window.webcontainerService || !window.WebContainerService) {
556
+ return false;
557
+ }
558
+
559
+ try {
560
+ const config = await window.webcontainerService.readConfig();
561
+ if (!config) {
562
+ return false;
563
+ }
564
+
565
+ return saveConfigToLocalStorage(config);
566
+ } catch (error) {
567
+ console.error('Failed to sync config from WebContainer:', error);
568
+ return false;
247
569
  }
248
570
  }
249
571
 
572
+ async function runSetupFlow() {
573
+ if (!window.webcontainerService || !window.WebContainerService) {
574
+ return `<pre class="whitespace-pre-wrap mb-4 text-yellow-400">⚠️ WebContainer service is not available.</pre>`;
575
+ }
576
+
577
+ const locationInput = prompt('Location (e.g., "Berlin, Germany" or "52.52,13.41"):');
578
+ if (!locationInput || !locationInput.trim()) {
579
+ return `<pre class="whitespace-pre-wrap mb-4 text-yellow-400">Setup cancelled.</pre>`;
580
+ }
581
+
582
+ let geoData = null;
583
+ const coords = parseCoordinates(locationInput);
584
+ if (coords) {
585
+ geoData = {
586
+ latitude: coords.latitude,
587
+ longitude: coords.longitude,
588
+ name: `Custom location (${coords.latitude}°, ${coords.longitude}°)`,
589
+ country: 'Custom',
590
+ timezone: 'UTC'
591
+ };
592
+ } else {
593
+ try {
594
+ geoData = await geocodeLocation(locationInput.trim());
595
+ } catch (error) {
596
+ return `<pre class="whitespace-pre-wrap mb-4 text-red-400">Geocoding failed: ${escapeHtml(error.message)}</pre>`;
597
+ }
598
+ if (!geoData) {
599
+ return `<pre class="whitespace-pre-wrap mb-4 text-red-400">Location not found. Setup cancelled.</pre>`;
600
+ }
601
+ }
602
+
603
+ const birthDate = prompt('Birth date (DD.MM.YYYY):') || '';
604
+ const birthTime = prompt('Birth time (HH:MM):') || '';
605
+ const birthLocationInput = prompt('Birth location (optional, e.g., "Hamburg, Germany" or "53.55,10.00"):') || '';
606
+
607
+ let birthLocation = null;
608
+ const warnings = [];
609
+ if (birthLocationInput.trim()) {
610
+ const birthCoords = parseCoordinates(birthLocationInput);
611
+ if (birthCoords) {
612
+ birthLocation = {
613
+ name: `Custom location (${birthCoords.latitude}°, ${birthCoords.longitude}°)`,
614
+ country: 'Custom',
615
+ latitude: birthCoords.latitude,
616
+ longitude: birthCoords.longitude
617
+ };
618
+ } else {
619
+ try {
620
+ const birthGeoData = await geocodeLocation(birthLocationInput.trim());
621
+ if (birthGeoData) {
622
+ birthLocation = {
623
+ name: birthGeoData.name,
624
+ country: birthGeoData.country,
625
+ latitude: birthGeoData.latitude,
626
+ longitude: birthGeoData.longitude
627
+ };
628
+ } else {
629
+ warnings.push('Birth location not found; continuing without birth location.');
630
+ }
631
+ } catch (error) {
632
+ warnings.push('Birth location lookup failed; continuing without birth location.');
633
+ }
634
+ }
635
+ }
636
+
637
+ const config = {
638
+ currentLocation: {
639
+ name: geoData.name,
640
+ country: geoData.country,
641
+ latitude: geoData.latitude,
642
+ longitude: geoData.longitude,
643
+ timezone: geoData.timezone
644
+ },
645
+ birthData: {
646
+ date: birthDate,
647
+ time: birthTime,
648
+ location: birthLocation
649
+ },
650
+ aiConfiguration: null,
651
+ setupDate: new Date().toISOString()
652
+ };
653
+
654
+ try {
655
+ const initialized = await window.webcontainerService.initialize();
656
+ if (initialized) {
657
+ await window.webcontainerService.writeConfig(config);
658
+ }
659
+
660
+ const saved = saveConfigToLocalStorage(config);
661
+ if (!saved) {
662
+ return `<pre class="whitespace-pre-wrap mb-4 text-red-400">Setup failed: Could not save configuration.</pre>`;
663
+ }
664
+
665
+ const warningText = warnings.length ? `\n${warnings.join('\n')}` : '';
666
+ return `<pre class="whitespace-pre-wrap mb-4 text-green-400">✅ Setup saved.${escapeHtml(warningText)}</pre>`;
667
+ } catch (error) {
668
+ return `<pre class="whitespace-pre-wrap mb-4 text-red-400">Setup failed: ${escapeHtml(error.message)}</pre>`;
669
+ }
670
+ }
671
+
672
+ // Insert command from sidebar/help
673
+ function insertCommand(cmd) {
674
+ terminalInput.value = cmd;
675
+ terminalInput.focus();
676
+ // Move cursor to end
677
+ terminalInput.selectionStart = terminalInput.selectionEnd = terminalInput.value.length;
678
+ }
679
+
680
+ // Toggle help panel
681
+ function toggleHelp() {
682
+ const isOpen = helpPanel.classList.contains('translate-x-0');
683
+
684
+ if (isOpen) {
685
+ helpPanel.classList.remove('translate-x-0');
686
+ helpPanel.classList.add('translate-x-full');
687
+ helpToggle.classList.remove('-translate-x-[85vw]', 'md:-translate-x-[300px]');
688
+ } else {
689
+ helpPanel.classList.remove('translate-x-full');
690
+ helpPanel.classList.add('translate-x-0');
691
+ helpToggle.classList.add('-translate-x-[85vw]', 'md:-translate-x-[300px]');
692
+ }
693
+ }
694
+
250
695
  // Load history from localStorage
251
696
  function loadHistory() {
252
697
  try {
253
- const savedHistory = localStorage.getItem('astrocliHistory');
698
+ const savedHistory = localStorage.getItem('klioTerminalHistory');
254
699
  if (savedHistory) {
255
700
  commandHistory = JSON.parse(savedHistory);
256
- renderHistory();
257
701
  }
258
702
  } catch (error) {
259
703
  console.error('Error loading history:', error);
260
704
  }
261
705
  }
262
-
706
+
263
707
  // Save history to localStorage
264
708
  function saveHistory() {
265
709
  try {
266
- localStorage.setItem('astrocliHistory', JSON.stringify(commandHistory));
710
+ localStorage.setItem('klioTerminalHistory', JSON.stringify(commandHistory));
267
711
  } catch (error) {
268
712
  console.error('Error saving history:', error);
269
713
  }
270
714
  }
271
-
272
- // Add command to history
273
- function addToHistory(command) {
274
- // Remove duplicate if exists
275
- commandHistory = commandHistory.filter(cmd => cmd !== command);
276
-
277
- // Add new command to beginning
278
- commandHistory.unshift(command);
279
-
280
- // Limit history size
281
- if (commandHistory.length > maxHistory) {
282
- commandHistory = commandHistory.slice(0, maxHistory);
283
- }
284
-
285
- saveHistory();
286
- renderHistory();
715
+
716
+ // Escape HTML for security
717
+ function escapeHtml(text) {
718
+ const div = document.createElement('div');
719
+ div.textContent = text;
720
+ return div.innerHTML
721
+ .replace(/\n/g, '<br>')
722
+ .replace(/ /g, '&nbsp;');
287
723
  }
288
-
289
- // Render history
290
- function renderHistory() {
291
- if (commandHistory.length === 0) {
292
- historyContainer.innerHTML = '<div class="text-gray-500 text-sm">No command history yet</div>';
293
- return;
724
+
725
+ // Function to load user session information
726
+ async function loadUserSession() {
727
+ try {
728
+ const response = await fetch('/api/user-session');
729
+ const sessionData = await response.json();
730
+
731
+ if (sessionData.userId) {
732
+ document.getElementById('userId').textContent = `User: ${sessionData.userId}`;
733
+ document.getElementById('logoutBtn').style.display = 'inline';
734
+ } else {
735
+ document.getElementById('userId').textContent = 'User: Guest';
736
+ }
737
+ } catch (error) {
738
+ console.error('Error loading user session:', error);
739
+ document.getElementById('userId').textContent = 'User: Error loading session';
294
740
  }
295
-
296
- let html = '<div class="space-y-1">';
297
- commandHistory.forEach((cmd, index) => {
298
- html += `<div
299
- class="command-input px-3 py-2 bg-gray-100 rounded cursor-pointer hover:bg-gray-200 text-sm truncate"
300
- title="${cmd}"
301
- onclick="commandInput.value = '${cmd.replace(/'/g, "\\'")}'; showPage('runner'); commandInput.focus()"
302
- >${cmd}</div>`;
303
- });
304
- html += '</div>';
305
-
306
- historyContainer.innerHTML = html;
307
741
  }
308
-
309
- // Run command
310
- async function runCommand() {
311
- const command = commandInput.value.trim();
312
-
313
- if (!command) {
314
- showToast('Please enter a command');
315
- return;
316
- }
317
-
742
+
743
+ // Function to logout
744
+ async function logout() {
318
745
  try {
319
- // Show loading state
320
- outputArea.textContent = 'Running command...\n\n';
321
-
322
- // Add to history
323
- addToHistory(command);
324
-
325
- // Clear input for next command
326
- commandInput.value = '';
327
-
328
- // Run the command via API
329
- const response = await fetch('/api/run-command', {
746
+ const response = await fetch('/api/logout', {
330
747
  method: 'POST',
331
748
  headers: {
332
749
  'Content-Type': 'application/json'
333
- },
334
- body: JSON.stringify({ command })
750
+ }
335
751
  });
336
752
 
337
753
  const result = await response.json();
754
+ if (result.success) {
755
+ // Reload the page to get a new session
756
+ window.location.reload();
757
+ } else {
758
+ console.error('Logout failed:', result.error);
759
+ }
760
+ } catch (error) {
761
+ console.error('Error during logout:', error);
762
+ }
763
+ }
764
+
765
+ // Initialize WebContainer status
766
+ async function initializeWebContainerStatus() {
767
+ try {
768
+ const statusElement = document.getElementById('wcStatusText');
769
+ const statusListener = (event) => {
770
+ if (!event || !event.detail) {
771
+ return;
772
+ }
773
+ statusElement.textContent = `WebContainer: ${event.detail}`;
774
+ statusElement.className = 'text-red-400';
775
+ };
776
+
777
+ window.addEventListener('webcontainer-status', statusListener);
778
+
779
+ // Check if page is cross-origin isolated (required for WebContainers)
780
+ if (!window.crossOriginIsolated) {
781
+ statusElement.textContent = 'WebContainer: Not isolated (COOP/COEP headers missing)';
782
+ statusElement.className = 'text-red-400';
783
+ console.warn('Page is not cross-origin isolated. WebContainers require COOP and COEP headers.');
784
+
785
+ // Check if we're on localhost (which allows COOP/COEP without HTTPS)
786
+ if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
787
+ console.log('Running on localhost - COOP/COEP headers should work without HTTPS');
788
+ } else if (window.location.protocol !== 'https:') {
789
+ console.warn('For non-localhost domains, HTTPS is required for COOP/COEP headers to work.');
790
+ }
791
+
792
+ return;
793
+ }
338
794
 
339
- if (response.ok) {
340
- // Display the result with better formatting
341
- if (result.output) {
342
- const cleanedOutput = result.output
343
- .replace(/\n{3,}/g, '\n\n')
344
- .trim();
345
- outputArea.textContent = cleanedOutput;
795
+ // Check if WebContainer service is available
796
+ let retries = 0;
797
+ const maxRetries = 5;
798
+
799
+ while ((!window.webcontainerService || !window.WebContainerService) && retries < maxRetries) {
800
+ if (retries === 0) {
801
+ statusElement.textContent = 'WebContainer: Service loading...';
346
802
  } else {
347
- outputArea.textContent = 'Command executed successfully';
803
+ statusElement.textContent = 'WebContainer: Service loading... (retry ' + retries + ')';
348
804
  }
349
- } else {
350
- let errorMessage = 'Error: ' + result.error;
351
- if (result.details) {
352
- errorMessage += '\n' + result.details;
805
+ statusElement.className = 'text-blue-400';
806
+
807
+ // Wait longer for the service to load (WebContainer API is large)
808
+ await new Promise(resolve => setTimeout(resolve, 1000));
809
+ retries++;
810
+ }
811
+
812
+ if (!window.webcontainerService || !window.WebContainerService) {
813
+ statusElement.textContent = 'WebContainer: Service not available';
814
+ statusElement.className = 'text-yellow-400';
815
+ return;
816
+ }
817
+
818
+ // Check if WebContainers are supported
819
+ if (!window.WebContainerService ||
820
+ typeof window.WebContainerService.isSupported !== 'function' ||
821
+ !window.webcontainerService ||
822
+ typeof window.webcontainerService.initialize !== 'function' ||
823
+ typeof window.webcontainerService.executeCommand !== 'function') {
824
+ statusElement.textContent = 'WebContainer: Service class not available';
825
+ statusElement.className = 'text-yellow-400';
826
+ console.warn('WebContainerService is not defined or not properly loaded');
827
+ return;
828
+ }
829
+
830
+ if (!window.WebContainerService.isSupported()) {
831
+ statusElement.textContent = 'WebContainer: Not supported in this browser';
832
+ statusElement.className = 'text-yellow-400';
833
+ return;
834
+ }
835
+
836
+ statusElement.textContent = 'WebContainer: Supported, initializing...';
837
+ statusElement.className = 'text-blue-400';
838
+
839
+ // Try to initialize WebContainer
840
+ try {
841
+ const initialized = await window.webcontainerService.initialize();
842
+ if (initialized) {
843
+ statusElement.textContent = 'WebContainer: Ready!';
844
+ statusElement.className = 'text-green-400';
845
+ } else {
846
+ statusElement.textContent = 'WebContainer: Not available in this environment';
847
+ statusElement.className = 'text-red-400';
353
848
  }
354
- outputArea.textContent = errorMessage;
355
- showToast('Command failed');
849
+ } catch (error) {
850
+ console.error('WebContainer initialization error:', error);
851
+ statusElement.textContent = 'WebContainer: Initialization error - ' + error.message;
852
+ statusElement.className = 'text-red-400';
356
853
  }
357
854
 
358
855
  } catch (error) {
359
- console.error('Error running command:', error);
360
- outputArea.textContent = 'Error: ' + error.message;
361
- showToast('Command failed');
856
+ console.error('Error initializing WebContainer status:', error);
857
+ const statusElement = document.getElementById('wcStatusText');
858
+ statusElement.textContent = 'WebContainer: Error checking support - ' + error.message;
859
+ statusElement.className = 'text-red-400';
362
860
  }
363
861
  }
862
+
863
+ // Initialize when DOM is loaded
864
+ document.addEventListener('DOMContentLoaded', async function() {
865
+ terminalInput.focus();
866
+ loadHistory();
867
+ await loadUserSession();
868
+
869
+ // Sync config from localStorage to WebContainer on page load
870
+ await syncConfigToWebContainer();
871
+ await syncConfigFromWebContainer();
872
+
873
+ // Initialize WebContainer status with delay to allow service to load
874
+ setTimeout(async function() {
875
+ await initializeWebContainerStatus();
876
+ }, 1000);
877
+
878
+ // Handle input
879
+ terminalInput.addEventListener('keydown', handleKeyDown);
880
+
881
+ // Handle keyup events globally to preserve text selection
882
+ document.addEventListener('keyup', handleKeyUp);
364
883
 
365
- // Show toast message
366
- function showToast(message) {
367
- toastElement.textContent = message;
368
- toastElement.classList.remove('opacity-0', 'translate-y-4');
369
- toastElement.classList.add('opacity-100', 'translate-y-0');
884
+ if (menuToggle) {
885
+ menuToggle.addEventListener('click', openSidebar);
886
+ }
887
+ if (sidebarClose) {
888
+ sidebarClose.addEventListener('click', closeSidebar);
889
+ }
890
+ if (sidebarBackdrop) {
891
+ sidebarBackdrop.addEventListener('click', closeSidebar);
892
+ }
893
+ window.addEventListener('resize', handleSidebarResize);
894
+ handleSidebarResize();
370
895
 
371
- setTimeout(() => {
372
- toastElement.classList.add('opacity-0', 'translate-y-4');
373
- toastElement.classList.remove('opacity-100', 'translate-y-0');
374
- }, 3000);
375
- }
896
+ // Auto-focus when clicking anywhere in terminal, but preserve text selection
897
+ terminalContent.addEventListener('click', (e) => {
898
+ // If the click was on selected text, don't steal focus
899
+ const selection = window.getSelection();
900
+ if (selection && selection.toString().length > 0 &&
901
+ terminalContent.contains(selection.anchorNode)) {
902
+ return;
903
+ }
904
+ terminalInput.focus();
905
+ });
376
906
 
377
- // Initialize the app when DOM is loaded
378
- document.addEventListener('DOMContentLoaded', init);
907
+ copyTerminalBtn.addEventListener('click', async () => {
908
+ try {
909
+ const text = terminalContent.innerText;
910
+ await navigator.clipboard.writeText(text);
911
+ copyTerminalBtn.textContent = 'Copied';
912
+ setTimeout(() => {
913
+ copyTerminalBtn.textContent = 'Copy';
914
+ }, 1200);
915
+ } catch (error) {
916
+ console.error('Failed to copy terminal contents:', error);
917
+ }
918
+ });
919
+ });
379
920
  </script>
380
921
  </body>
381
- </html>
922
+ </html>