ps-claw 1.0.6

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.
@@ -0,0 +1,1179 @@
1
+ <!DOCTYPE html>
2
+ <html lang="pt-BR">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>PS Claw</title>
7
+ <style>
8
+ :root {
9
+ --bg: #1a1a2e;
10
+ --bg-deep: #0f0f1e;
11
+ --sidebar-bg: #12122a;
12
+ --input-bg: #1e1e3a;
13
+ --border: #2a2a4a;
14
+ --text: #e8e8f0;
15
+ --text-muted: #7a7a9e;
16
+ --accent: #10a37f;
17
+ --accent-hover: #0d8c6d;
18
+ --accent-glow: rgba(16,163,127,0.3);
19
+ --user-bubble: #252550;
20
+ --ai-bubble: transparent;
21
+ --danger: #e53e3e;
22
+ --warning: #f59e0b;
23
+ --info: #3b82f6;
24
+ --radius: 12px;
25
+ --radius-sm: 8px;
26
+ --transition: 0.2s ease;
27
+ }
28
+ * { box-sizing: border-box; margin: 0; padding: 0; }
29
+ body { font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--text); display: flex; height: 100vh; overflow: hidden; }
30
+
31
+ /* SIDEBAR */
32
+ #sidebar {
33
+ width: 280px; min-width: 280px; background: var(--sidebar-bg);
34
+ display: flex; flex-direction: column; border-right: 1px solid var(--border);
35
+ transition: transform 0.25s; z-index: 10;
36
+ }
37
+ #sidebar-header {
38
+ padding: 16px; display: flex; align-items: center; justify-content: space-between;
39
+ border-bottom: 1px solid var(--border);
40
+ }
41
+ #sidebar-header h1 { font-size: 18px; font-weight: 700; color: var(--accent); display: flex; align-items: center; gap: 8px; }
42
+
43
+ /* TAB NAVIGATION */
44
+ .tab-nav {
45
+ display: flex; border-bottom: 1px solid var(--border); background: var(--bg-deep);
46
+ }
47
+ .tab-btn {
48
+ flex: 1; padding: 10px 4px; background: transparent; border: none;
49
+ color: var(--text-muted); font-size: 11px; cursor: pointer;
50
+ transition: all var(--transition); display: flex; flex-direction: column;
51
+ align-items: center; gap: 3px; border-bottom: 2px solid transparent;
52
+ position: relative;
53
+ }
54
+ .tab-btn:hover { color: var(--text); background: rgba(255,255,255,0.03); }
55
+ .tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
56
+ .tab-btn svg { width: 18px; height: 18px; }
57
+ .tab-btn .tab-label { font-weight: 500; }
58
+
59
+ /* TAB PANELS */
60
+ .tab-panel { display: none; flex: 1; flex-direction: column; overflow: hidden; }
61
+ .tab-panel.active { display: flex; }
62
+
63
+ /* CHAT TAB */
64
+ #new-chat-btn {
65
+ width: calc(100% - 16px); margin: 8px 8px 4px; padding: 10px 14px;
66
+ background: transparent; border: 1px dashed var(--border);
67
+ color: var(--text); border-radius: var(--radius); cursor: pointer;
68
+ font-size: 13px; text-align: left; display: flex; align-items: center; gap: 8px;
69
+ transition: all var(--transition);
70
+ }
71
+ #new-chat-btn:hover { background: var(--input-bg); border-color: var(--accent); }
72
+ #chat-list { flex: 1; overflow-y: auto; padding: 4px 8px; }
73
+ .chat-item {
74
+ padding: 8px 10px; border-radius: var(--radius-sm); cursor: pointer;
75
+ font-size: 13px; color: var(--text-muted); white-space: nowrap;
76
+ overflow: hidden; text-overflow: ellipsis; transition: background var(--transition);
77
+ display: flex; align-items: center; justify-content: space-between;
78
+ }
79
+ .chat-item:hover { background: var(--input-bg); color: var(--text); }
80
+ .chat-item.active { background: var(--input-bg); color: var(--text); border-left: 2px solid var(--accent); }
81
+ .chat-item-del { opacity: 0; font-size: 11px; padding: 2px 5px; border-radius: 4px; border: none; background: transparent; color: var(--text-muted); cursor: pointer; }
82
+ .chat-item:hover .chat-item-del { opacity: 1; }
83
+ .chat-item-del:hover { color: var(--danger); }
84
+
85
+ /* GATEWAYS TAB */
86
+ .gw-section { padding: 12px; }
87
+ .gw-section-title {
88
+ font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px;
89
+ color: var(--text-muted); margin-bottom: 10px; display: flex; align-items: center; justify-content: space-between;
90
+ }
91
+ .gw-card {
92
+ background: var(--input-bg); border: 1px solid var(--border); border-radius: var(--radius-sm);
93
+ padding: 12px; margin-bottom: 8px; transition: border-color var(--transition);
94
+ }
95
+ .gw-card:hover { border-color: var(--accent); }
96
+ .gw-card.connected { border-color: var(--accent); box-shadow: 0 0 8px var(--accent-glow); }
97
+ .gw-card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
98
+ .gw-card-name { font-size: 13px; font-weight: 600; display: flex; align-items: center; gap: 6px; }
99
+ .gw-status-dot { width: 8px; height: 8px; border-radius: 50%; background: #888; flex-shrink: 0; }
100
+ .gw-status-dot.online { background: var(--accent); box-shadow: 0 0 6px var(--accent-glow); }
101
+ .gw-status-dot.offline { background: var(--danger); }
102
+ .gw-card-url { font-size: 11px; color: var(--text-muted); word-break: break-all; margin-bottom: 6px; }
103
+ .gw-card-actions { display: flex; gap: 6px; }
104
+ .gw-btn {
105
+ padding: 4px 10px; font-size: 11px; border-radius: 6px; border: 1px solid var(--border);
106
+ background: transparent; color: var(--text-muted); cursor: pointer; transition: all var(--transition);
107
+ }
108
+ .gw-btn:hover { border-color: var(--accent); color: var(--accent); }
109
+ .gw-btn.primary { background: var(--accent); color: white; border-color: var(--accent); }
110
+ .gw-btn.primary:hover { background: var(--accent-hover); }
111
+ .gw-btn.danger:hover { border-color: var(--danger); color: var(--danger); }
112
+ .gw-add-btn {
113
+ width: 100%; padding: 10px; border: 1px dashed var(--border); border-radius: var(--radius-sm);
114
+ background: transparent; color: var(--text-muted); cursor: pointer; font-size: 13px;
115
+ display: flex; align-items: center; justify-content: center; gap: 6px;
116
+ transition: all var(--transition);
117
+ }
118
+ .gw-add-btn:hover { border-color: var(--accent); color: var(--accent); }
119
+ .gw-list { overflow-y: auto; flex: 1; }
120
+
121
+ /* MODELS TAB */
122
+ .model-section { padding: 12px; overflow-y: auto; flex: 1; }
123
+ .provider-group { margin-bottom: 16px; }
124
+ .provider-header {
125
+ display: flex; align-items: center; gap: 8px; padding: 8px 10px;
126
+ background: var(--input-bg); border-radius: var(--radius-sm); margin-bottom: 8px;
127
+ cursor: pointer; transition: all var(--transition);
128
+ }
129
+ .provider-header:hover { background: rgba(255,255,255,0.05); }
130
+ .provider-icon { font-size: 18px; }
131
+ .provider-name { font-size: 13px; font-weight: 600; flex: 1; }
132
+ .provider-count { font-size: 11px; color: var(--text-muted); background: var(--bg-deep); padding: 2px 8px; border-radius: 10px; }
133
+ .provider-toggle { font-size: 12px; color: var(--text-muted); transition: transform var(--transition); }
134
+ .provider-toggle.open { transform: rotate(180deg); }
135
+ .model-list { display: none; padding-left: 4px; }
136
+ .model-list.open { display: block; }
137
+ .model-item {
138
+ display: flex; align-items: center; gap: 8px; padding: 7px 10px;
139
+ border-radius: 6px; cursor: pointer; font-size: 13px; transition: background var(--transition);
140
+ }
141
+ .model-item:hover { background: var(--input-bg); }
142
+ .model-item.selected { background: rgba(16,163,127,0.1); color: var(--accent); }
143
+ .model-item.selected .model-radio { border-color: var(--accent); background: var(--accent); }
144
+ .model-radio {
145
+ width: 16px; height: 16px; border-radius: 50%; border: 2px solid var(--border);
146
+ flex-shrink: 0; position: relative; transition: all var(--transition);
147
+ }
148
+ .model-item.selected .model-radio::after {
149
+ content: ''; position: absolute; top: 3px; left: 3px; width: 6px; height: 6px;
150
+ border-radius: 50%; background: white;
151
+ }
152
+ .model-name { flex: 1; }
153
+ .model-tag { font-size: 10px; padding: 2px 6px; border-radius: 4px; background: var(--bg-deep); color: var(--text-muted); }
154
+
155
+ /* SETTINGS TAB */
156
+ .settings-section { padding: 12px; overflow-y: auto; flex: 1; }
157
+ .settings-group { margin-bottom: 20px; }
158
+ .settings-group-title {
159
+ font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px;
160
+ color: var(--text-muted); margin-bottom: 10px;
161
+ }
162
+ .settings-field { margin-bottom: 12px; }
163
+ .settings-field label { display: block; font-size: 12px; color: var(--text-muted); margin-bottom: 5px; font-weight: 500; }
164
+ .settings-field input, .settings-field select, .settings-field textarea {
165
+ width: 100%; padding: 8px 10px; background: var(--input-bg);
166
+ border: 1px solid var(--border); border-radius: 6px; color: var(--text);
167
+ font-size: 13px; outline: none; transition: border-color var(--transition);
168
+ }
169
+ .settings-field input:focus, .settings-field select:focus, .settings-field textarea:focus { border-color: var(--accent); }
170
+ .settings-field textarea { resize: vertical; min-height: 60px; font-family: monospace; font-size: 12px; }
171
+ .settings-field .hint { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
172
+ .settings-toggle {
173
+ display: flex; align-items: center; justify-content: space-between;
174
+ padding: 8px 0; border-bottom: 1px solid var(--border);
175
+ }
176
+ .settings-toggle-label { font-size: 13px; }
177
+ .toggle-switch {
178
+ width: 40px; height: 22px; background: var(--border); border-radius: 11px;
179
+ cursor: pointer; position: relative; transition: background var(--transition);
180
+ }
181
+ .toggle-switch.on { background: var(--accent); }
182
+ .toggle-switch::after {
183
+ content: ''; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px;
184
+ background: white; border-radius: 50%; transition: transform var(--transition);
185
+ }
186
+ .toggle-switch.on::after { transform: translateX(18px); }
187
+
188
+ /* SIDEBAR FOOTER */
189
+ #sidebar-footer { padding: 10px; border-top: 1px solid var(--border); }
190
+ #status-badge {
191
+ display: flex; align-items: center; gap: 8px; font-size: 12px;
192
+ color: var(--text-muted); padding: 8px; border-radius: var(--radius-sm); background: var(--input-bg);
193
+ }
194
+ #status-dot { width: 8px; height: 8px; border-radius: 50%; background: #888; flex-shrink: 0; }
195
+ #status-dot.online { background: var(--accent); box-shadow: 0 0 6px var(--accent-glow); }
196
+ #status-dot.offline { background: var(--danger); }
197
+
198
+ /* MAIN */
199
+ #main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
200
+ #topbar {
201
+ padding: 12px 20px; border-bottom: 1px solid var(--border);
202
+ display: flex; align-items: center; justify-content: space-between;
203
+ background: var(--bg);
204
+ }
205
+ #topbar-left { display: flex; align-items: center; gap: 10px; }
206
+ #topbar-title { font-size: 15px; font-weight: 600; }
207
+ #model-indicator {
208
+ display: flex; align-items: center; gap: 6px; font-size: 12px;
209
+ padding: 4px 10px; background: var(--input-bg); border-radius: 20px;
210
+ color: var(--text-muted); border: 1px solid var(--border);
211
+ }
212
+ #model-indicator .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
213
+
214
+ /* MESSAGES */
215
+ #messages { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 4px; scroll-behavior: smooth; }
216
+ #messages::-webkit-scrollbar { width: 6px; }
217
+ #messages::-webkit-scrollbar-track { background: transparent; }
218
+ #messages::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
219
+
220
+ .msg-row { display: flex; gap: 12px; padding: 12px 0; max-width: 800px; margin: 0 auto; width: 100%; }
221
+ .msg-row.user { flex-direction: row-reverse; }
222
+ .avatar {
223
+ width: 34px; height: 34px; border-radius: 50%; flex-shrink: 0;
224
+ display: flex; align-items: center; justify-content: center; font-size: 14px;
225
+ font-weight: 700;
226
+ }
227
+ .avatar.ai { background: linear-gradient(135deg, var(--accent), #0ea5e9); color: white; }
228
+ .avatar.user { background: linear-gradient(135deg, #6c6ef5, #8b5cf6); color: white; }
229
+ .bubble {
230
+ max-width: calc(100% - 50px); padding: 12px 16px; border-radius: 16px;
231
+ font-size: 14px; line-height: 1.65; word-break: break-word;
232
+ }
233
+ .bubble.ai { background: var(--ai-bubble); border-radius: 0 16px 16px 16px; }
234
+ .bubble.user { background: var(--user-bubble); border-radius: 16px 16px 0 16px; }
235
+ .bubble pre {
236
+ background: #0d0d1a; border: 1px solid var(--border); border-radius: 8px;
237
+ padding: 12px; overflow-x: auto; margin: 8px 0; font-size: 13px;
238
+ }
239
+ .bubble code { font-family: 'Cascadia Code', 'Fira Code', monospace; }
240
+ .bubble p { margin-bottom: 8px; }
241
+ .bubble p:last-child { margin-bottom: 0; }
242
+ .bubble ul, .bubble ol { padding-left: 20px; margin-bottom: 8px; }
243
+ .bubble strong { color: #fff; }
244
+ .typing-dots span { animation: blink 1.2s infinite; font-size: 20px; color: var(--accent); }
245
+ .typing-dots span:nth-child(2) { animation-delay: 0.2s; }
246
+ .typing-dots span:nth-child(3) { animation-delay: 0.4s; }
247
+ @keyframes blink { 0%,80%,100%{opacity:0} 40%{opacity:1} }
248
+
249
+ #welcome {
250
+ flex: 1; display: flex; flex-direction: column; align-items: center;
251
+ justify-content: center; gap: 16px; color: var(--text-muted); padding: 40px;
252
+ }
253
+ #welcome h2 { font-size: 28px; color: var(--text); background: linear-gradient(135deg, var(--accent), #0ea5e9); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
254
+ #welcome p { font-size: 15px; text-align: center; max-width: 440px; }
255
+ .welcome-chips { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-top: 8px; }
256
+ .chip {
257
+ padding: 8px 14px; background: var(--input-bg); border: 1px solid var(--border);
258
+ border-radius: 20px; font-size: 13px; cursor: pointer; transition: all var(--transition);
259
+ color: var(--text);
260
+ }
261
+ .chip:hover { border-color: var(--accent); color: var(--accent); box-shadow: 0 0 10px var(--accent-glow); }
262
+
263
+ /* INPUT */
264
+ #input-area { padding: 16px 20px 20px; background: var(--bg); }
265
+ #input-box {
266
+ max-width: 800px; margin: 0 auto;
267
+ background: var(--input-bg); border: 1px solid var(--border);
268
+ border-radius: var(--radius); display: flex; align-items: flex-end; gap: 8px;
269
+ padding: 12px 14px; transition: border-color var(--transition);
270
+ }
271
+ #input-box:focus-within { border-color: var(--accent); box-shadow: 0 0 10px var(--accent-glow); }
272
+ #msg-input {
273
+ flex: 1; background: transparent; border: none; outline: none;
274
+ color: var(--text); font-size: 14px; resize: none; max-height: 200px;
275
+ line-height: 1.5; font-family: inherit;
276
+ }
277
+ #msg-input::placeholder { color: var(--text-muted); }
278
+ #send-btn {
279
+ width: 34px; height: 34px; border-radius: 8px; border: none;
280
+ background: var(--accent); color: white; cursor: pointer;
281
+ display: flex; align-items: center; justify-content: center;
282
+ transition: background var(--transition); flex-shrink: 0;
283
+ }
284
+ #send-btn:hover:not(:disabled) { background: var(--accent-hover); }
285
+ #send-btn:disabled { background: var(--border); cursor: not-allowed; }
286
+ #input-hint { text-align: center; font-size: 11px; color: var(--text-muted); margin-top: 8px; }
287
+
288
+ /* MODAL */
289
+ #modal-overlay {
290
+ display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.7);
291
+ z-index: 100; align-items: center; justify-content: center;
292
+ backdrop-filter: blur(4px);
293
+ }
294
+ #modal-overlay.open { display: flex; }
295
+ #modal-box {
296
+ background: var(--sidebar-bg); border: 1px solid var(--border); border-radius: 16px;
297
+ padding: 24px; width: 460px; max-width: 95vw; max-height: 85vh; overflow-y: auto;
298
+ }
299
+ #modal-box h2 { margin-bottom: 20px; font-size: 18px; display: flex; align-items: center; gap: 8px; }
300
+ .field { margin-bottom: 14px; }
301
+ .field label { display: block; font-size: 12px; color: var(--text-muted); margin-bottom: 5px; font-weight: 500; }
302
+ .field input, .field select {
303
+ width: 100%; padding: 9px 12px; background: var(--input-bg);
304
+ border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--text);
305
+ font-size: 13px; outline: none; transition: border-color var(--transition);
306
+ }
307
+ .field input:focus, .field select:focus { border-color: var(--accent); }
308
+ .field .hint { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
309
+ .modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; }
310
+ .btn { padding: 8px 18px; border-radius: var(--radius-sm); font-size: 13px; cursor: pointer; border: none; transition: all var(--transition); }
311
+ .btn-ghost { background: transparent; border: 1px solid var(--border); color: var(--text); }
312
+ .btn-primary { background: var(--accent); color: white; }
313
+ .btn-danger { background: var(--danger); color: white; }
314
+ .btn:hover { opacity: 0.85; }
315
+
316
+ /* TOAST */
317
+ #toast-container { position: fixed; top: 20px; right: 20px; z-index: 200; display: flex; flex-direction: column; gap: 8px; }
318
+ .toast {
319
+ padding: 12px 18px; border-radius: var(--radius-sm); font-size: 13px;
320
+ color: white; animation: slideIn 0.3s ease, fadeOut 0.3s ease 2.7s forwards;
321
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
322
+ }
323
+ .toast.success { background: var(--accent); }
324
+ .toast.error { background: var(--danger); }
325
+ .toast.info { background: var(--info); }
326
+ @keyframes slideIn { from { transform: translateX(100px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
327
+ @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
328
+
329
+ @media (max-width: 640px) {
330
+ #sidebar { position: fixed; left: 0; top: 0; bottom: 0; transform: translateX(-100%); }
331
+ #sidebar.open { transform: translateX(0); }
332
+ #menu-btn { display: flex; }
333
+ }
334
+ #menu-btn {
335
+ display: none; background: transparent; border: none; color: var(--text);
336
+ font-size: 20px; cursor: pointer; padding: 4px;
337
+ }
338
+ </style>
339
+ </head>
340
+ <body>
341
+
342
+ <!-- TOAST CONTAINER -->
343
+ <div id="toast-container"></div>
344
+
345
+ <!-- SIDEBAR -->
346
+ <div id="sidebar">
347
+ <div id="sidebar-header">
348
+ <h1>🦞 PS Claw</h1>
349
+ <button onclick="openModal('about')" title="Sobre" style="background:transparent;border:none;color:var(--text-muted);cursor:pointer;font-size:18px;">ℹ️</button>
350
+ </div>
351
+
352
+ <!-- TAB NAV -->
353
+ <div class="tab-nav">
354
+ <button class="tab-btn active" onclick="switchTab('chat')" id="tab-chat">
355
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
356
+ <span class="tab-label">Chat</span>
357
+ </button>
358
+ <button class="tab-btn" onclick="switchTab('gateways')" id="tab-gateways">
359
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v6m0 6v6m11-7h-6m-6 0H1m16.66-5.66l-4.24 4.24m-2.84 0L6.34 6.34m11.32 11.32l-4.24-4.24m-2.84 0L6.34 17.66"/></svg>
360
+ <span class="tab-label">Gateways</span>
361
+ </button>
362
+ <button class="tab-btn" onclick="switchTab('models')" id="tab-models">
363
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
364
+ <span class="tab-label">Modelos</span>
365
+ </button>
366
+ <button class="tab-btn" onclick="switchTab('settings')" id="tab-settings">
367
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
368
+ <span class="tab-label">Config</span>
369
+ </button>
370
+ </div>
371
+
372
+ <!-- CHAT TAB PANEL -->
373
+ <div class="tab-panel active" id="panel-chat">
374
+ <div style="padding: 8px 8px 4px;">
375
+ <button id="new-chat-btn" onclick="newChat()">
376
+ <span>✏️</span> Novo Chat
377
+ </button>
378
+ </div>
379
+ <div id="chat-list"></div>
380
+ </div>
381
+
382
+ <!-- GATEWAYS TAB PANEL -->
383
+ <div class="tab-panel" id="panel-gateways">
384
+ <div class="gw-section">
385
+ <div class="gw-section-title">
386
+ <span>Conexões de API</span>
387
+ </div>
388
+ </div>
389
+ <div class="gw-list" id="gateway-list"></div>
390
+ <div class="gw-section">
391
+ <button class="gw-add-btn" onclick="openModal('addGateway')">
392
+ <span>+</span> Adicionar Gateway
393
+ </button>
394
+ </div>
395
+ </div>
396
+
397
+ <!-- MODELS TAB PANEL -->
398
+ <div class="tab-panel" id="panel-models">
399
+ <div class="model-section" id="model-list"></div>
400
+ </div>
401
+
402
+ <!-- SETTINGS TAB PANEL -->
403
+ <div class="tab-panel" id="panel-settings">
404
+ <div class="settings-section" id="settings-panel"></div>
405
+ </div>
406
+
407
+ <!-- SIDEBAR FOOTER -->
408
+ <div id="sidebar-footer">
409
+ <div id="status-badge">
410
+ <div id="status-dot"></div>
411
+ <span id="status-text">Verificando gateway...</span>
412
+ </div>
413
+ </div>
414
+ </div>
415
+
416
+ <!-- MAIN -->
417
+ <div id="main">
418
+ <div id="topbar">
419
+ <div id="topbar-left">
420
+ <button id="menu-btn" onclick="toggleSidebar()">☰</button>
421
+ <span id="topbar-title">PS Claw</span>
422
+ </div>
423
+ <div id="model-indicator">
424
+ <div class="dot"></div>
425
+ <span id="current-model-name">Modelo padrao</span>
426
+ </div>
427
+ </div>
428
+
429
+ <div id="messages">
430
+ <div id="welcome">
431
+ <div style="font-size:56px;">🦞</div>
432
+ <h2>PS Claw</h2>
433
+ <p>Agente de IA autonomo leve e poderoso. Configure sua chave de API e comece a conversar.</p>
434
+ <div class="welcome-chips">
435
+ <div class="chip" onclick="sendQuick('Ola! O que voce consegue fazer?')">O que voce consegue fazer?</div>
436
+ <div class="chip" onclick="sendQuick('Me ajude a escrever um script Python')">Escrever codigo</div>
437
+ <div class="chip" onclick="sendQuick('Pesquise na web sobre IA em 2025')">Pesquisar na web</div>
438
+ <div class="chip" onclick="sendQuick('Explique como funciona o PS Claw')">Sobre o PS Claw</div>
439
+ </div>
440
+ </div>
441
+ </div>
442
+
443
+ <div id="input-area">
444
+ <div id="input-box">
445
+ <textarea id="msg-input" placeholder="Escreva uma mensagem..." rows="1" onkeydown="handleKey(event)" oninput="autoResize(this)"></textarea>
446
+ <button id="send-btn" onclick="sendMessage()" title="Enviar (Enter)">
447
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
448
+ <line x1="22" y1="2" x2="11" y2="13"/>
449
+ <polygon points="22 2 15 22 11 13 2 9 22 2"/>
450
+ </svg>
451
+ </button>
452
+ </div>
453
+ <div id="input-hint">Enter para enviar · Shift+Enter para nova linha</div>
454
+ </div>
455
+ </div>
456
+
457
+ <!-- MODAL -->
458
+ <div id="modal-overlay" onclick="closeModal(event)">
459
+ <div id="modal-box">
460
+ <div id="modal-content"></div>
461
+ </div>
462
+ </div>
463
+
464
+ <script>
465
+ // === STATE ===
466
+ let sessions = JSON.parse(localStorage.getItem('ps-claw-sessions') || '[]');
467
+ let currentSession = null;
468
+ let isTyping = false;
469
+ let activeTab = 'chat';
470
+
471
+ let cfg = JSON.parse(localStorage.getItem('ps-claw-cfg') || '{}');
472
+ cfg.gateways = cfg.gateways || [
473
+ { id: 'default', name: 'PS Claw Local', url: 'http://localhost:3000/gateway', token: '', type: 'ps-claw', connected: false }
474
+ ];
475
+ cfg.selectedGateway = cfg.selectedGateway || 'default';
476
+ cfg.selectedModel = cfg.selectedModel || '';
477
+ cfg.displayName = cfg.displayName || 'Voce';
478
+ cfg.temperature = cfg.temperature ?? 0.7;
479
+ cfg.maxTokens = cfg.maxTokens ?? 4096;
480
+ cfg.systemPrompt = cfg.systemPrompt || '';
481
+ cfg.streamResponses = cfg.streamResponses ?? true;
482
+ cfg.darkMode = cfg.darkMode ?? true;
483
+
484
+ const PROVIDERS = [
485
+ {
486
+ id: 'openai', name: 'OpenAI', icon: '🟢',
487
+ models: [
488
+ { id: 'gpt-4o', name: 'GPT-4o', tag: 'Premium' },
489
+ { id: 'gpt-4o-mini', name: 'GPT-4o Mini', tag: 'Rapido' },
490
+ { id: 'gpt-4-turbo', name: 'GPT-4 Turbo', tag: 'Avancado' },
491
+ { id: 'gpt-3.5-turbo', name: 'GPT-3.5 Turbo', tag: 'Economico' },
492
+ { id: 'o1-preview', name: 'o1 Preview', tag: 'Raciocinio' },
493
+ { id: 'o1-mini', name: 'o1 Mini', tag: 'Raciocinio' },
494
+ { id: 'gpt-4-vision-preview', name: 'GPT-4 Vision', tag: 'Visao' },
495
+ ]
496
+ },
497
+ {
498
+ id: 'anthropic', name: 'Anthropic', icon: '🟠',
499
+ models: [
500
+ { id: 'claude-opus-4-5', name: 'Claude Opus 4.5', tag: 'Premium' },
501
+ { id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5', tag: 'Balanceado' },
502
+ { id: 'claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet', tag: 'Rapido' },
503
+ { id: 'claude-3-haiku-20240307', name: 'Claude 3 Haiku', tag: 'Economico' },
504
+ ]
505
+ },
506
+ {
507
+ id: 'google', name: 'Google', icon: '🔵',
508
+ models: [
509
+ { id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro', tag: 'Premium' },
510
+ { id: 'gemini-2.0-flash', name: 'Gemini 2.0 Flash', tag: 'Rapido' },
511
+ { id: 'gemini-1.5-pro', name: 'Gemini 1.5 Pro', tag: 'Avancado' },
512
+ { id: 'gemini-1.5-flash', name: 'Gemini 1.5 Flash', tag: 'Economico' },
513
+ ]
514
+ },
515
+ {
516
+ id: 'deepseek', name: 'DeepSeek', icon: '🟣',
517
+ models: [
518
+ { id: 'deepseek-chat', name: 'DeepSeek Chat', tag: 'Geral' },
519
+ { id: 'deepseek-reasoner', name: 'DeepSeek Reasoner', tag: 'Raciocinio' },
520
+ ]
521
+ },
522
+ {
523
+ id: 'openrouter', name: 'OpenRouter', icon: '🌐',
524
+ models: [
525
+ { id: 'openrouter-auto', name: 'Auto (melhor modelo)', tag: 'Auto' },
526
+ { id: 'meta-llama/llama-3.1-405b-instruct', name: 'Llama 3.1 405B', tag: 'Open Source' },
527
+ { id: 'mistralai/mixtral-8x7b-instruct', name: 'Mixtral 8x7B', tag: 'Open Source' },
528
+ ]
529
+ },
530
+ {
531
+ id: 'local', name: 'Local / LM Studio', icon: '💻',
532
+ models: [
533
+ { id: 'local-default', name: 'Modelo Local Padrao', tag: 'Local' },
534
+ ]
535
+ }
536
+ ];
537
+
538
+ // === INIT ===
539
+ renderChatList();
540
+ renderGatewayList();
541
+ renderModelList();
542
+ renderSettingsPanel();
543
+ checkAllGateways();
544
+ setInterval(checkAllGateways, 15000);
545
+ updateModelIndicator();
546
+
547
+ // === TAB NAVIGATION ===
548
+ function switchTab(tab) {
549
+ activeTab = tab;
550
+ document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
551
+ document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
552
+ document.getElementById('tab-' + tab).classList.add('active');
553
+ document.getElementById('panel-' + tab).classList.add('active');
554
+ }
555
+
556
+ // === GATEWAY ===
557
+ function getActiveGateway() {
558
+ return cfg.gateways.find(g => g.id === cfg.selectedGateway) || cfg.gateways[0];
559
+ }
560
+
561
+ async function checkAllGateways() {
562
+ for (const gw of cfg.gateways) {
563
+ try {
564
+ const r = await fetch(gw.url + '/health', {
565
+ headers: gw.token ? { Authorization: 'Bearer ' + gw.token } : {},
566
+ signal: AbortSignal.timeout(3000)
567
+ });
568
+ gw.connected = r.ok;
569
+ } catch {
570
+ gw.connected = false;
571
+ }
572
+ }
573
+ updateGatewayStatus();
574
+ renderGatewayList();
575
+ }
576
+
577
+ function updateGatewayStatus() {
578
+ const dot = document.getElementById('status-dot');
579
+ const text = document.getElementById('status-text');
580
+ const activeGw = getActiveGateway();
581
+ if (activeGw?.connected) {
582
+ dot.className = 'online'; text.textContent = activeGw.name + ' - Online';
583
+ } else {
584
+ dot.className = 'offline'; text.textContent = activeGw?.name + ' - Offline';
585
+ }
586
+ }
587
+
588
+ function renderGatewayList() {
589
+ const list = document.getElementById('gateway-list');
590
+ list.innerHTML = cfg.gateways.map(gw => `
591
+ <div class="gw-section">
592
+ <div class="gw-card ${gw.connected ? 'connected' : ''} ${gw.id === cfg.selectedGateway ? 'connected' : ''}">
593
+ <div class="gw-card-header">
594
+ <div class="gw-card-name">
595
+ <div class="gw-status-dot ${gw.connected ? 'online' : 'offline'}"></div>
596
+ ${escHtml(gw.name)}
597
+ </div>
598
+ <span style="font-size:11px;color:var(--text-muted);">${gw.type}</span>
599
+ </div>
600
+ <div class="gw-card-url">${escHtml(gw.url)}</div>
601
+ <div class="gw-card-actions">
602
+ <button class="gw-btn primary" onclick="selectGateway('${gw.id}')">Conectar</button>
603
+ <button class="gw-btn" onclick="openModal('editGateway','${gw.id}')">Editar</button>
604
+ ${gw.id !== 'default' ? `<button class="gw-btn danger" onclick="removeGateway('${gw.id}')">Remover</button>` : ''}
605
+ </div>
606
+ </div>
607
+ </div>
608
+ `).join('');
609
+ }
610
+
611
+ function selectGateway(id) {
612
+ cfg.selectedGateway = id;
613
+ saveCfg();
614
+ renderGatewayList();
615
+ updateGatewayStatus();
616
+ showToast('Gateway selecionado: ' + cfg.gateways.find(g => g.id === id)?.name, 'success');
617
+ }
618
+
619
+ function removeGateway(id) {
620
+ if (id === 'default') return;
621
+ cfg.gateways = cfg.gateways.filter(g => g.id !== id);
622
+ if (cfg.selectedGateway === id) cfg.selectedGateway = 'default';
623
+ saveCfg();
624
+ renderGatewayList();
625
+ checkAllGateways();
626
+ showToast('Gateway removido', 'info');
627
+ }
628
+
629
+ // === MODELS ===
630
+ function renderModelList() {
631
+ const container = document.getElementById('model-list');
632
+ container.innerHTML = PROVIDERS.map(p => `
633
+ <div class="provider-group">
634
+ <div class="provider-header" onclick="toggleProvider('${p.id}')">
635
+ <span class="provider-icon">${p.icon}</span>
636
+ <span class="provider-name">${p.name}</span>
637
+ <span class="provider-count">${p.models.length}</span>
638
+ <span class="provider-toggle" id="toggle-${p.id}">▼</span>
639
+ </div>
640
+ <div class="model-list" id="models-${p.id}">
641
+ ${p.models.map(m => `
642
+ <div class="model-item ${cfg.selectedModel === m.id ? 'selected' : ''}" onclick="selectModel('${m.id}', '${m.name}')">
643
+ <div class="model-radio"></div>
644
+ <span class="model-name">${m.name}</span>
645
+ <span class="model-tag">${m.tag}</span>
646
+ </div>
647
+ `).join('')}
648
+ </div>
649
+ </div>
650
+ `).join('');
651
+ }
652
+
653
+ function toggleProvider(id) {
654
+ const list = document.getElementById('models-' + id);
655
+ const toggle = document.getElementById('toggle-' + id);
656
+ list.classList.toggle('open');
657
+ toggle.classList.toggle('open');
658
+ }
659
+
660
+ function selectModel(id, name) {
661
+ cfg.selectedModel = id;
662
+ saveCfg();
663
+ renderModelList();
664
+ updateModelIndicator();
665
+ showToast('Modelo selecionado: ' + name, 'success');
666
+ }
667
+
668
+ function updateModelIndicator() {
669
+ const el = document.getElementById('current-model-name');
670
+ if (cfg.selectedModel) {
671
+ let name = cfg.selectedModel;
672
+ for (const p of PROVIDERS) {
673
+ const m = p.models.find(m => m.id === cfg.selectedModel);
674
+ if (m) { name = m.name; break; }
675
+ }
676
+ el.textContent = name;
677
+ } else {
678
+ el.textContent = 'Modelo padrao';
679
+ }
680
+ }
681
+
682
+ // === SETTINGS ===
683
+ function renderSettingsPanel() {
684
+ const panel = document.getElementById('settings-panel');
685
+ panel.innerHTML = `
686
+ <div class="settings-group">
687
+ <div class="settings-group-title">Perfil</div>
688
+ <div class="settings-field">
689
+ <label>Nome de exibicao</label>
690
+ <input type="text" id="s-name" value="${escAttr(cfg.displayName)}" placeholder="Voce" onchange="updateSetting('displayName', this.value)" />
691
+ </div>
692
+ </div>
693
+
694
+ <div class="settings-group">
695
+ <div class="settings-group-title">Modelo e Comportamento</div>
696
+ <div class="settings-field">
697
+ <label>Temperatura (${cfg.temperature})</label>
698
+ <input type="range" min="0" max="2" step="0.1" value="${cfg.temperature}" oninput="updateSetting('temperature', parseFloat(this.value)); this.previousElementSibling.textContent='Temperatura ('+this.value+')'" />
699
+ <div class="hint">Valores mais baixos = respostas mais deterministas. Valores mais altos = mais criatividade.</div>
700
+ </div>
701
+ <div class="settings-field">
702
+ <label>Max Tokens</label>
703
+ <input type="number" id="s-maxtokens" value="${cfg.maxTokens}" min="100" max="128000" step="100" onchange="updateSetting('maxTokens', parseInt(this.value))" />
704
+ <div class="hint">Limite maximo de tokens na resposta.</div>
705
+ </div>
706
+ <div class="settings-field">
707
+ <label>System Prompt</label>
708
+ <textarea id="s-systemprompt" onchange="updateSetting('systemPrompt', this.value)">${escHtml(cfg.systemPrompt)}</textarea>
709
+ <div class="hint">Instrucoes iniciais para o modelo. Define o comportamento da IA.</div>
710
+ </div>
711
+ </div>
712
+
713
+ <div class="settings-group">
714
+ <div class="settings-group-title">Preferencias</div>
715
+ <div class="settings-toggle">
716
+ <span class="settings-toggle-label">Respostas em stream</span>
717
+ <div class="toggle-switch ${cfg.streamResponses ? 'on' : ''}" onclick="toggleSetting('streamResponses', this)"></div>
718
+ </div>
719
+ <div class="settings-toggle">
720
+ <span class="settings-toggle-label">Modo escuro</span>
721
+ <div class="toggle-switch ${cfg.darkMode ? 'on' : ''}" onclick="toggleSetting('darkMode', this)"></div>
722
+ </div>
723
+ </div>
724
+
725
+ <div class="settings-group">
726
+ <div class="settings-group-title">Chaves de API (salvas localmente)</div>
727
+ <div class="settings-field">
728
+ <label>OpenAI API Key</label>
729
+ <input type="password" id="s-key-openai" value="${escAttr(cfg.apiKeys?.openai || '')}" placeholder="sk-..." onchange="updateApiKey('openai', this.value)" />
730
+ </div>
731
+ <div class="settings-field">
732
+ <label>Anthropic API Key</label>
733
+ <input type="password" id="s-key-anthropic" value="${escAttr(cfg.apiKeys?.anthropic || '')}" placeholder="sk-ant-..." onchange="updateApiKey('anthropic', this.value)" />
734
+ </div>
735
+ <div class="settings-field">
736
+ <label>Google / Gemini API Key</label>
737
+ <input type="password" id="s-key-google" value="${escAttr(cfg.apiKeys?.google || '')}" placeholder="AI..." onchange="updateApiKey('google', this.value)" />
738
+ </div>
739
+ <div class="settings-field">
740
+ <label>DeepSeek API Key</label>
741
+ <input type="password" id="s-key-deepseek" value="${escAttr(cfg.apiKeys?.deepseek || '')}" placeholder="sk-..." onchange="updateApiKey('deepseek', this.value)" />
742
+ </div>
743
+ <div class="settings-field">
744
+ <label>OpenRouter API Key</label>
745
+ <input type="password" id="s-key-openrouter" value="${escAttr(cfg.apiKeys?.openrouter || '')}" placeholder="sk-or-..." onchange="updateApiKey('openrouter', this.value)" />
746
+ </div>
747
+ </div>
748
+
749
+ <div class="settings-group">
750
+ <div class="settings-group-title">Dados</div>
751
+ <button class="btn btn-ghost" style="width:100%;margin-bottom:8px;" onclick="exportConfig()">Exportar Configuracoes</button>
752
+ <button class="btn btn-ghost" style="width:100%;margin-bottom:8px;" onclick="document.getElementById('import-input').click()">Importar Configuracoes</button>
753
+ <input type="file" id="import-input" accept=".json" style="display:none" onchange="importConfig(event)" />
754
+ <button class="btn btn-danger" style="width:100%;" onclick="clearAllData()">Limpar Todos os Dados</button>
755
+ </div>
756
+ `;
757
+ }
758
+
759
+ function updateSetting(key, value) {
760
+ cfg[key] = value;
761
+ saveCfg();
762
+ }
763
+
764
+ function toggleSetting(key, el) {
765
+ cfg[key] = !cfg[key];
766
+ el.classList.toggle('on');
767
+ saveCfg();
768
+ }
769
+
770
+ function updateApiKey(provider, value) {
771
+ if (!cfg.apiKeys) cfg.apiKeys = {};
772
+ cfg.apiKeys[provider] = value;
773
+ saveCfg();
774
+ showToast('Chave ' + provider.toUpperCase() + ' salva localmente', 'success');
775
+ }
776
+
777
+ function exportConfig() {
778
+ const data = JSON.stringify(cfg, null, 2);
779
+ const blob = new Blob([data], { type: 'application/json' });
780
+ const url = URL.createObjectURL(blob);
781
+ const a = document.createElement('a');
782
+ a.href = url; a.download = 'ps-claw-config.json'; a.click();
783
+ URL.revokeObjectURL(url);
784
+ showToast('Configuracoes exportadas', 'success');
785
+ }
786
+
787
+ function importConfig(event) {
788
+ const file = event.target.files[0];
789
+ if (!file) return;
790
+ const reader = new FileReader();
791
+ reader.onload = (e) => {
792
+ try {
793
+ const imported = JSON.parse(e.target.result);
794
+ Object.assign(cfg, imported);
795
+ saveCfg();
796
+ renderSettingsPanel();
797
+ renderGatewayList();
798
+ renderModelList();
799
+ updateModelIndicator();
800
+ showToast('Configuracoes importadas com sucesso', 'success');
801
+ } catch {
802
+ showToast('Erro ao importar: arquivo invalido', 'error');
803
+ }
804
+ };
805
+ reader.readAsText(file);
806
+ }
807
+
808
+ function clearAllData() {
809
+ if (!confirm('Tem certeza? Isso apagara todas as conversas e configuracoes.')) return;
810
+ localStorage.removeItem('ps-claw-sessions');
811
+ localStorage.removeItem('ps-claw-cfg');
812
+ sessions = [];
813
+ currentSession = null;
814
+ location.reload();
815
+ }
816
+
817
+ // === CHAT ===
818
+ function newChat() {
819
+ const id = Date.now().toString();
820
+ const session = { id, title: 'Nova conversa', messages: [], created: Date.now() };
821
+ sessions.unshift(session);
822
+ saveSessions();
823
+ selectChat(id);
824
+ switchTab('chat');
825
+ }
826
+
827
+ function selectChat(id) {
828
+ currentSession = sessions.find(s => s.id === id);
829
+ if (!currentSession) return;
830
+ renderChatList();
831
+ renderMessages();
832
+ }
833
+
834
+ function deleteChat(id, e) {
835
+ e.stopPropagation();
836
+ sessions = sessions.filter(s => s.id !== id);
837
+ saveSessions();
838
+ if (currentSession?.id === id) { currentSession = null; renderMessages(); }
839
+ renderChatList();
840
+ }
841
+
842
+ function saveSessions() {
843
+ localStorage.setItem('ps-claw-sessions', JSON.stringify(sessions));
844
+ }
845
+
846
+ // === RENDER CHAT ===
847
+ function renderChatList() {
848
+ const list = document.getElementById('chat-list');
849
+ list.innerHTML = sessions.map(s => `
850
+ <div class="chat-item ${s.id === currentSession?.id ? 'active' : ''}" onclick="selectChat('${s.id}')">
851
+ <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escHtml(s.title)}</span>
852
+ <button class="chat-item-del" onclick="deleteChat('${s.id}', event)" title="Excluir">x</button>
853
+ </div>
854
+ `).join('');
855
+ }
856
+
857
+ function renderMessages() {
858
+ const box = document.getElementById('messages');
859
+ if (!currentSession || currentSession.messages.length === 0) {
860
+ box.innerHTML = `<div id="welcome">
861
+ <div style="font-size:56px;">🦞</div>
862
+ <h2>PS Claw</h2>
863
+ <p>Agente de IA autonomo. Configure sua chave de API e comece a conversar.</p>
864
+ <div class="welcome-chips">
865
+ <div class="chip" onclick="sendQuick('Ola! O que voce consegue fazer?')">O que voce consegue fazer?</div>
866
+ <div class="chip" onclick="sendQuick('Me ajude a escrever um script Python')">Escrever codigo</div>
867
+ <div class="chip" onclick="sendQuick('Pesquise na web sobre IA em 2025')">Pesquisar na web</div>
868
+ <div class="chip" onclick="sendQuick('Explique como funciona o PS Claw')">Sobre o PS Claw</div>
869
+ </div>
870
+ </div>`;
871
+ return;
872
+ }
873
+ box.innerHTML = currentSession.messages.map(m => renderMsg(m)).join('');
874
+ box.scrollTop = box.scrollHeight;
875
+ }
876
+
877
+ function renderMsg(m) {
878
+ const isUser = m.role === 'user';
879
+ return `
880
+ <div class="msg-row ${isUser ? 'user' : 'ai'}">
881
+ <div class="avatar ${isUser ? 'user' : 'ai'}">${isUser ? (cfg.displayName[0]||'V').toUpperCase() : '🦞'}</div>
882
+ <div class="bubble ${isUser ? 'user' : 'ai'}">${formatText(m.content)}</div>
883
+ </div>`;
884
+ }
885
+
886
+ function addTypingIndicator() {
887
+ const box = document.getElementById('messages');
888
+ const el = document.createElement('div');
889
+ el.id = 'typing-row';
890
+ el.className = 'msg-row ai';
891
+ el.innerHTML = `<div class="avatar ai">🦞</div><div class="bubble ai"><div class="typing-dots"><span>•</span><span>•</span><span>•</span></div></div>`;
892
+ box.appendChild(el);
893
+ box.scrollTop = box.scrollHeight;
894
+ }
895
+ function removeTypingIndicator() {
896
+ document.getElementById('typing-row')?.remove();
897
+ }
898
+
899
+ // === SEND MESSAGE ===
900
+ async function sendMessage() {
901
+ const input = document.getElementById('msg-input');
902
+ const text = input.value.trim();
903
+ if (!text || isTyping) return;
904
+ if (!currentSession) newChat();
905
+
906
+ input.value = '';
907
+ input.style.height = 'auto';
908
+ document.getElementById('send-btn').disabled = true;
909
+ isTyping = true;
910
+
911
+ document.getElementById('welcome')?.remove();
912
+
913
+ currentSession.messages.push({ role: 'user', content: text });
914
+ if (currentSession.messages.length === 1) {
915
+ currentSession.title = text.slice(0, 40) + (text.length > 40 ? '...' : '');
916
+ renderChatList();
917
+ }
918
+ saveSessions();
919
+ renderMessages();
920
+ addTypingIndicator();
921
+
922
+ try {
923
+ const gw = getActiveGateway();
924
+ const body = {
925
+ text,
926
+ ...(cfg.selectedModel && { model: cfg.selectedModel }),
927
+ temperature: cfg.temperature,
928
+ max_tokens: cfg.maxTokens,
929
+ ...(cfg.systemPrompt && { system: cfg.systemPrompt }),
930
+ history: currentSession.messages.slice(0, -1).map(m => ({ role: m.role, content: m.content }))
931
+ };
932
+
933
+ const resp = await fetch(gw.url + '/chat', {
934
+ method: 'POST',
935
+ headers: {
936
+ 'Content-Type': 'application/json',
937
+ ...(gw.token && { Authorization: 'Bearer ' + gw.token })
938
+ },
939
+ body: JSON.stringify(body),
940
+ signal: AbortSignal.timeout(120000)
941
+ });
942
+
943
+ removeTypingIndicator();
944
+
945
+ let replyText;
946
+ if (resp.ok) {
947
+ const data = await resp.json();
948
+ replyText = data.reply || data.text || data.content || data.message || JSON.stringify(data, null, 2);
949
+ } else {
950
+ replyText = await tryFallbackChat(text);
951
+ }
952
+
953
+ currentSession.messages.push({ role: 'assistant', content: replyText });
954
+ saveSessions();
955
+ renderMessages();
956
+
957
+ } catch (err) {
958
+ removeTypingIndicator();
959
+ const fallback = await tryFallbackChat(text);
960
+ currentSession.messages.push({ role: 'assistant', content: fallback });
961
+ saveSessions();
962
+ renderMessages();
963
+ }
964
+
965
+ isTyping = false;
966
+ document.getElementById('send-btn').disabled = false;
967
+ document.getElementById('msg-input').focus();
968
+ }
969
+
970
+ async function tryFallbackChat(text) {
971
+ const gw = getActiveGateway();
972
+ const endpoints = ['/v1/chat', '/api/chat', '/message'];
973
+ for (const ep of endpoints) {
974
+ try {
975
+ const r = await fetch(gw.url + ep, {
976
+ method: 'POST',
977
+ headers: { 'Content-Type': 'application/json', ...(gw.token && { Authorization: 'Bearer ' + gw.token }) },
978
+ body: JSON.stringify({
979
+ message: text,
980
+ model: cfg.selectedModel || undefined,
981
+ temperature: cfg.temperature,
982
+ max_tokens: cfg.maxTokens,
983
+ messages: [{ role: 'user', content: text }]
984
+ }),
985
+ signal: AbortSignal.timeout(10000)
986
+ });
987
+ if (r.ok) {
988
+ const d = await r.json();
989
+ return d.reply || d.text || d.content || d.message || JSON.stringify(d, null, 2);
990
+ }
991
+ } catch {}
992
+ }
993
+ const gwName = gw ? gw.name : 'Desconhecido';
994
+ const gwUrl = gw ? gw.url : 'N/A';
995
+ return `**Gateway offline ou configuracao necessaria**\n\nO PS Claw nao esta respondendo. Verifique:\n\n1. Execute o PS Claw: \`ps-claw gateway run\`\n2. Va na aba **Gateways** e configure a URL\n3. Adicione seu token se necessario\n4. Selecione um modelo na aba **Modelos**\n\nGateway: ${gwName} (${gwUrl})`;
996
+ }
997
+
998
+ function sendQuick(text) {
999
+ document.getElementById('msg-input').value = text;
1000
+ sendMessage();
1001
+ }
1002
+
1003
+ // === MODALS ===
1004
+ function openModal(type, data) {
1005
+ const content = document.getElementById('modal-content');
1006
+
1007
+ if (type === 'addGateway') {
1008
+ content.innerHTML = `
1009
+ <h2>🔗 Adicionar Gateway</h2>
1010
+ <div class="field">
1011
+ <label>Nome</label>
1012
+ <input type="text" id="m-gw-name" placeholder="Meu Gateway" />
1013
+ </div>
1014
+ <div class="field">
1015
+ <label>URL</label>
1016
+ <input type="text" id="m-gw-url" placeholder="http://localhost:3000/gateway" />
1017
+ </div>
1018
+ <div class="field">
1019
+ <label>Tipo</label>
1020
+ <select id="m-gw-type">
1021
+ <option value="ps-claw">PS Claw</option>
1022
+ <option value="openai">OpenAI Compatible</option>
1023
+ <option value="anthropic">Anthropic Compatible</option>
1024
+ <option value="ollama">Ollama</option>
1025
+ <option value="lmstudio">LM Studio</option>
1026
+ <option value="custom">Custom</option>
1027
+ </select>
1028
+ </div>
1029
+ <div class="field">
1030
+ <label>Token / API Key (opcional)</label>
1031
+ <input type="password" id="m-gw-token" placeholder="Seu token de autenticacao" />
1032
+ </div>
1033
+ <div class="modal-actions">
1034
+ <button class="btn btn-ghost" onclick="closeModal()">Cancelar</button>
1035
+ <button class="btn btn-primary" onclick="addGateway()">Adicionar</button>
1036
+ </div>
1037
+ `;
1038
+ } else if (type === 'editGateway') {
1039
+ const gw = cfg.gateways.find(g => g.id === data);
1040
+ if (!gw) return;
1041
+ content.innerHTML = `
1042
+ <h2>✏️ Editar Gateway</h2>
1043
+ <div class="field">
1044
+ <label>Nome</label>
1045
+ <input type="text" id="m-gw-name" value="${escAttr(gw.name)}" />
1046
+ </div>
1047
+ <div class="field">
1048
+ <label>URL</label>
1049
+ <input type="text" id="m-gw-url" value="${escAttr(gw.url)}" />
1050
+ </div>
1051
+ <div class="field">
1052
+ <label>Tipo</label>
1053
+ <select id="m-gw-type">
1054
+ <option value="ps-claw" ${gw.type==='ps-claw'?'selected':''}>PS Claw</option>
1055
+ <option value="openai" ${gw.type==='openai'?'selected':''}>OpenAI Compatible</option>
1056
+ <option value="anthropic" ${gw.type==='anthropic'?'selected':''}>Anthropic Compatible</option>
1057
+ <option value="ollama" ${gw.type==='ollama'?'selected':''}>Ollama</option>
1058
+ <option value="lmstudio" ${gw.type==='lmstudio'?'selected':''}>LM Studio</option>
1059
+ <option value="custom" ${gw.type==='custom'?'selected':''}>Custom</option>
1060
+ </select>
1061
+ </div>
1062
+ <div class="field">
1063
+ <label>Token / API Key</label>
1064
+ <input type="password" id="m-gw-token" value="${escAttr(gw.token)}" />
1065
+ </div>
1066
+ <div class="modal-actions">
1067
+ <button class="btn btn-ghost" onclick="closeModal()">Cancelar</button>
1068
+ <button class="btn btn-primary" onclick="editGateway('${gw.id}')">Salvar</button>
1069
+ </div>
1070
+ `;
1071
+ } else if (type === 'about') {
1072
+ content.innerHTML = `
1073
+ <h2>🦞 Sobre o PS Claw</h2>
1074
+ <p style="margin-bottom:12px;color:var(--text-muted);font-size:14px;line-height:1.7;">
1075
+ PS Claw e um fork leve do OpenClaw — um agente de IA autonomo multi-canal com interface web no estilo ChatGPT.
1076
+ </p>
1077
+ <p style="margin-bottom:12px;color:var(--text-muted);font-size:14px;line-height:1.7;">
1078
+ Use no terminal com <code style="background:var(--input-bg);padding:2px 6px;border-radius:4px;">ps-claw gateway run</code> ou acesse esta interface web para gerenciar gateways, modelos e provedores de API.
1079
+ </p>
1080
+ <div style="background:var(--input-bg);border-radius:8px;padding:12px;font-size:12px;font-family:monospace;color:var(--text-muted);">
1081
+ <div>Comandos uteis:</div>
1082
+ <div style="margin-top:6px;">ps-claw gateway run # Inicia o gateway</div>
1083
+ <div>ps-claw secrets set # Configurar chaves</div>
1084
+ <div>ps-claw doctor # Diagnostico</div>
1085
+ <div>ps-claw configure # Configurar</div>
1086
+ <div>ps-claw models list # Listar modelos</div>
1087
+ </div>
1088
+ <div class="modal-actions">
1089
+ <button class="btn btn-primary" onclick="closeModal()">Fechar</button>
1090
+ </div>
1091
+ `;
1092
+ }
1093
+
1094
+ document.getElementById('modal-overlay').classList.add('open');
1095
+ }
1096
+
1097
+ function closeModal(e) {
1098
+ if (e && e.target !== document.getElementById('modal-overlay')) return;
1099
+ document.getElementById('modal-overlay').classList.remove('open');
1100
+ }
1101
+
1102
+ function addGateway() {
1103
+ const name = document.getElementById('m-gw-name').value.trim();
1104
+ const url = document.getElementById('m-gw-url').value.trim();
1105
+ const type = document.getElementById('m-gw-type').value;
1106
+ const token = document.getElementById('m-gw-token').value.trim();
1107
+ if (!name || !url) { showToast('Preencha nome e URL', 'error'); return; }
1108
+ const id = 'gw-' + Date.now();
1109
+ cfg.gateways.push({ id, name, url, type, token, connected: false });
1110
+ saveCfg();
1111
+ renderGatewayList();
1112
+ closeModal();
1113
+ checkAllGateways();
1114
+ showToast('Gateway adicionado: ' + name, 'success');
1115
+ }
1116
+
1117
+ function editGateway(id) {
1118
+ const gw = cfg.gateways.find(g => g.id === id);
1119
+ if (!gw) return;
1120
+ gw.name = document.getElementById('m-gw-name').value.trim() || gw.name;
1121
+ gw.url = document.getElementById('m-gw-url').value.trim() || gw.url;
1122
+ gw.type = document.getElementById('m-gw-type').value;
1123
+ gw.token = document.getElementById('m-gw-token').value.trim();
1124
+ saveCfg();
1125
+ renderGatewayList();
1126
+ closeModal();
1127
+ checkAllGateways();
1128
+ showToast('Gateway atualizado: ' + gw.name, 'success');
1129
+ }
1130
+
1131
+ // === UTILS ===
1132
+ function handleKey(e) {
1133
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
1134
+ }
1135
+ function autoResize(el) {
1136
+ el.style.height = 'auto';
1137
+ el.style.height = Math.min(el.scrollHeight, 200) + 'px';
1138
+ }
1139
+ function escHtml(s) {
1140
+ return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
1141
+ }
1142
+ function escAttr(s) {
1143
+ return (s||'').replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
1144
+ }
1145
+ function formatText(text) {
1146
+ if (!text) return '';
1147
+ let t = escHtml(text);
1148
+ t = t.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) =>
1149
+ `<pre><code class="language-${lang}">${code.trim()}</code></pre>`);
1150
+ t = t.replace(/`([^`]+)`/g, '<code>$1</code>');
1151
+ t = t.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
1152
+ t = t.replace(/\*(.+?)\*/g, '<em>$1</em>');
1153
+ t = t.replace(/^### (.+)$/gm, '<h3 style="margin:8px 0 4px;font-size:15px;">$1</h3>');
1154
+ t = t.replace(/^## (.+)$/gm, '<h2 style="margin:10px 0 6px;font-size:17px;">$1</h2>');
1155
+ t = t.replace(/^# (.+)$/gm, '<h1 style="margin:12px 0 8px;font-size:20px;">$1</h1>');
1156
+ t = t.replace(/^- (.+)$/gm, '<li>$1</li>');
1157
+ t = t.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>');
1158
+ t = t.split('\n\n').map(p => p.startsWith('<') ? p : `<p>${p.replace(/\n/g, '<br>')}</p>`).join('\n');
1159
+ return t;
1160
+ }
1161
+ function toggleSidebar() {
1162
+ document.getElementById('sidebar').classList.toggle('open');
1163
+ }
1164
+
1165
+ function saveCfg() {
1166
+ localStorage.setItem('ps-claw-cfg', JSON.stringify(cfg));
1167
+ }
1168
+
1169
+ function showToast(message, type = 'info') {
1170
+ const container = document.getElementById('toast-container');
1171
+ const toast = document.createElement('div');
1172
+ toast.className = 'toast ' + type;
1173
+ toast.textContent = message;
1174
+ container.appendChild(toast);
1175
+ setTimeout(() => toast.remove(), 3000);
1176
+ }
1177
+ </script>
1178
+ </body>
1179
+ </html>