ps-claw 1.1.0 → 1.1.2

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.
@@ -1,414 +1,378 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="pt-BR">
3
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; }
4
+ <meta charset="UTF-8"/>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1.0"/>
6
+ <title>PS Claw</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"/>
8
+ <style>
9
+ :root{
10
+ --bg:#0e0e11;
11
+ --surface:#16161a;
12
+ --surface2:#1e1e24;
13
+ --surface3:#26262e;
14
+ --border:#2e2e38;
15
+ --border2:#3a3a48;
16
+ --txt:#f0f0f5;
17
+ --txt2:#9090a8;
18
+ --txt3:#5a5a70;
19
+ --acc:#7c6af7;
20
+ --acc2:#9b8fff;
21
+ --acc3:#5c4fe0;
22
+ --green:#22c98a;
23
+ --red:#f05a5a;
24
+ --orange:#f0a048;
25
+ --yellow:#f0d060;
26
+ --rad:14px;
27
+ --rad-sm:8px;
28
+ }
29
+ *{box-sizing:border-box;margin:0;padding:0}
30
+ html{height:100%}
31
+ body{font-family:'Syne',sans-serif;background:var(--bg);color:var(--txt);display:flex;height:100vh;overflow:hidden;font-size:14px}
32
+
33
+ /* ── SIDEBAR ── */
34
+ #sidebar{
35
+ width:268px;min-width:268px;background:var(--surface);
36
+ display:flex;flex-direction:column;
37
+ border-right:1px solid var(--border);
38
+ transition:transform .3s cubic-bezier(.4,0,.2,1);z-index:20
39
+ }
40
+ #sb-logo{
41
+ padding:20px 18px 16px;
42
+ border-bottom:1px solid var(--border);
43
+ display:flex;align-items:center;gap:10px
44
+ }
45
+ #sb-logo .logo-icon{
46
+ width:34px;height:34px;border-radius:10px;
47
+ background:linear-gradient(135deg,var(--acc3),var(--acc2));
48
+ display:flex;align-items:center;justify-content:center;
49
+ font-size:18px;flex-shrink:0
50
+ }
51
+ #sb-logo h1{font-size:17px;font-weight:800;letter-spacing:-.3px}
52
+ #sb-logo span{font-size:10px;color:var(--acc2);font-weight:600;letter-spacing:.5px;display:block;margin-top:1px}
53
+ #new-btn{
54
+ margin:12px 12px 6px;
55
+ padding:10px 14px;
56
+ background:linear-gradient(135deg,var(--acc3),var(--acc));
57
+ border:none;border-radius:var(--rad-sm);
58
+ color:#fff;cursor:pointer;font-size:13px;font-weight:700;
59
+ font-family:'Syne',sans-serif;
60
+ display:flex;align-items:center;gap:8px;
61
+ transition:opacity .15s;letter-spacing:.2px
62
+ }
63
+ #new-btn:hover{opacity:.85}
64
+ #chat-list{flex:1;overflow-y:auto;padding:4px 8px;scrollbar-width:thin;scrollbar-color:var(--border) transparent}
65
+ .ci{
66
+ padding:9px 10px;border-radius:var(--rad-sm);cursor:pointer;
67
+ font-size:13px;color:var(--txt2);
68
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
69
+ transition:all .15s;display:flex;align-items:center;justify-content:space-between;
70
+ gap:8px;border:1px solid transparent
71
+ }
72
+ .ci:hover{background:var(--surface2);color:var(--txt);border-color:var(--border)}
73
+ .ci.active{background:var(--surface2);color:var(--txt);border-color:var(--border2)}
74
+ .ci-del{opacity:0;border:none;background:transparent;color:var(--txt3);cursor:pointer;font-size:12px;padding:2px 5px;border-radius:4px;flex-shrink:0}
75
+ .ci:hover .ci-del{opacity:1}
76
+ .ci-del:hover{color:var(--red)}
77
+ #sb-footer{padding:12px;border-top:1px solid var(--border)}
78
+ #st-badge{
79
+ display:flex;align-items:center;gap:8px;font-size:12px;
80
+ color:var(--txt2);padding:9px 12px;border-radius:var(--rad-sm);
81
+ background:var(--surface2);border:1px solid var(--border)
82
+ }
83
+ #st-dot{width:7px;height:7px;border-radius:50%;background:var(--border2);flex-shrink:0;transition:all .4s}
84
+ #st-dot.on{background:var(--green);box-shadow:0 0 8px var(--green)}
85
+ #st-dot.off{background:var(--red)}
86
+ #st-dot.warn{background:var(--orange)}
87
+
88
+ /* ── MAIN ── */
89
+ #main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}
90
+
91
+ /* ── TOPBAR ── */
92
+ #topbar{
93
+ padding:0 20px;border-bottom:1px solid var(--border);
94
+ display:flex;align-items:center;justify-content:space-between;
95
+ height:54px;background:var(--surface);flex-shrink:0
96
+ }
97
+ #tb-left{display:flex;align-items:center;gap:12px}
98
+ #menu-btn{display:none;background:transparent;border:none;color:var(--txt2);font-size:20px;cursor:pointer;padding:4px}
99
+ #tb-title{font-size:15px;font-weight:700;letter-spacing:-.2px}
100
+ #tb-right{display:flex;align-items:center;gap:8px}
101
+ #model-pill{
102
+ display:flex;align-items:center;gap:6px;
103
+ background:var(--surface2);border:1px solid var(--border);
104
+ padding:6px 12px;border-radius:20px;cursor:pointer;
105
+ font-size:12px;font-weight:600;color:var(--txt2);
106
+ transition:all .15s
107
+ }
108
+ #model-pill:hover{border-color:var(--acc);color:var(--acc)}
109
+ #model-pill .dot{width:6px;height:6px;border-radius:50%;background:var(--green)}
315
110
 
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; } }
111
+ /* ── TABS ── */
112
+ #tabs{
113
+ display:flex;gap:0;border-bottom:1px solid var(--border);
114
+ background:var(--surface);flex-shrink:0;padding:0 20px
115
+ }
116
+ .tab{
117
+ padding:11px 16px;font-size:13px;font-weight:600;
118
+ color:var(--txt3);cursor:pointer;
119
+ border-bottom:2px solid transparent;
120
+ transition:all .15s;white-space:nowrap;
121
+ display:flex;align-items:center;gap:7px;
122
+ letter-spacing:.1px
123
+ }
124
+ .tab:hover{color:var(--txt2)}
125
+ .tab.active{color:var(--acc2);border-bottom-color:var(--acc)}
126
+
127
+ /* ── PAGES ── */
128
+ .page{flex:1;overflow-y:auto;display:none;scrollbar-width:thin;scrollbar-color:var(--border) transparent}
129
+ .page.active{display:flex;flex-direction:column}
130
+ .page-inner{padding:24px;max-width:780px;width:100%}
131
+
132
+ /* ── CHAT ── */
133
+ #chat-page{position:relative}
134
+ #messages{
135
+ flex:1;overflow-y:auto;
136
+ padding:24px 20px;
137
+ display:flex;flex-direction:column;gap:2px;
138
+ scroll-behavior:smooth;
139
+ scrollbar-width:thin;scrollbar-color:var(--border) transparent
140
+ }
141
+ #welcome{
142
+ flex:1;display:flex;flex-direction:column;
143
+ align-items:center;justify-content:center;
144
+ gap:20px;padding:40px 20px;text-align:center
145
+ }
146
+ #welcome .w-icon{
147
+ width:72px;height:72px;border-radius:20px;
148
+ background:linear-gradient(135deg,var(--acc3),var(--acc2));
149
+ display:flex;align-items:center;justify-content:center;font-size:36px;
150
+ box-shadow:0 0 40px rgba(124,106,247,.3)
151
+ }
152
+ #welcome h2{font-size:28px;font-weight:800;letter-spacing:-.5px}
153
+ #welcome p{font-size:14px;color:var(--txt2);max-width:400px;line-height:1.7}
154
+ .chips{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin-top:4px}
155
+ .chip{
156
+ padding:8px 16px;background:var(--surface2);
157
+ border:1px solid var(--border);border-radius:20px;
158
+ font-size:13px;cursor:pointer;transition:all .15s;color:var(--txt2);
159
+ font-family:'Syne',sans-serif;font-weight:600
160
+ }
161
+ .chip:hover{border-color:var(--acc);color:var(--acc);background:rgba(124,106,247,.08)}
162
+
163
+ .msg-row{display:flex;gap:12px;padding:10px 0;max-width:820px;margin:0 auto;width:100%;animation:fadeUp .2s ease}
164
+ @keyframes fadeUp{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
165
+ .msg-row.user{flex-direction:row-reverse}
166
+ .av{
167
+ width:32px;height:32px;border-radius:10px;flex-shrink:0;
168
+ display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:800
169
+ }
170
+ .av.ai{background:linear-gradient(135deg,var(--acc3),var(--acc2));color:#fff}
171
+ .av.user{background:var(--surface3);color:var(--txt);border:1px solid var(--border2)}
172
+ .bbl{
173
+ max-width:calc(100% - 48px);padding:12px 16px;
174
+ font-size:14px;line-height:1.75;word-break:break-word
175
+ }
176
+ .bbl.ai{color:var(--txt)}
177
+ .bbl.user{
178
+ background:var(--surface2);border:1px solid var(--border);
179
+ border-radius:14px 14px 4px 14px;color:var(--txt)
180
+ }
181
+ .bbl pre{
182
+ background:var(--surface3);border:1px solid var(--border2);
183
+ border-radius:var(--rad-sm);padding:14px;overflow-x:auto;
184
+ margin:10px 0;font-size:13px;font-family:'JetBrains Mono',monospace
185
+ }
186
+ .bbl code{font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--acc2)}
187
+ .bbl p{margin-bottom:8px}.bbl p:last-child{margin:0}
188
+ .bbl ul,.bbl ol{padding-left:20px;margin-bottom:8px}
189
+ .bbl strong{color:var(--txt);font-weight:700}
190
+ .bbl h1,.bbl h2,.bbl h3{margin:10px 0 6px;font-weight:700}
191
+ .bbl table{border-collapse:collapse;width:100%;margin:10px 0}
192
+ .bbl th,.bbl td{border:1px solid var(--border2);padding:8px 12px;text-align:left}
193
+ .bbl th{background:var(--surface3);font-weight:700}
194
+
195
+ /* action tags */
196
+ .act{padding:4px 5px 4px 10px;margin:6px 0 2px;border-radius:8px;font-size:12px;font-weight:700;font-family:'JetBrains Mono',monospace;display:flex;align-items:center;gap:7px;line-height:1.3;word-break:break-word}
197
+ .act-cmd{background:rgba(59,130,246,.12);color:#3b82f6;border-left:3px solid #3b82f6}
198
+ .act-click{background:rgba(139,92,246,.12);color:#8b5cf6;border-left:3px solid #8b5cf6}
199
+ .act-app{background:rgba(16,185,129,.12);color:#10b981;border-left:3px solid #10b981}
200
+ .act-file{background:rgba(245,158,11,.12);color:#f59e0b;border-left:3px solid #f59e0b}
201
+ .act-err{background:rgba(239,68,68,.1);color:#ef4444;border-left:3px solid #ef4444}
202
+ .act-ok{background:rgba(16,185,129,.1);color:#10b981;border-left:3px solid #10b981}
203
+ .act::before{font-size:13px}
204
+ .act-cmd::before{content:'💻'}
205
+ .act-click::before{content:'🖱️'}
206
+ .act-app::before{content:'📂'}
207
+ .act-file::before{content:'📄'}
208
+ .act-err::before{content:'❌'}
209
+ .act-ok::before{content:'✅'}
210
+
211
+ /* typing */
212
+ .typing{display:flex;gap:4px;align-items:center;padding:14px 16px}
213
+ .typing span{width:6px;height:6px;border-radius:50%;background:var(--acc);animation:pulse 1.2s infinite}
214
+ .typing span:nth-child(2){animation-delay:.2s}
215
+ .typing span:nth-child(3){animation-delay:.4s}
216
+ @keyframes pulse{0%,60%,100%{opacity:.2;transform:scale(.8)}30%{opacity:1;transform:scale(1)}}
217
+
218
+ /* ── INPUT ── */
219
+ #input-wrap{padding:16px 20px 20px;background:var(--surface);border-top:1px solid var(--border);flex-shrink:0}
220
+ #input-box{
221
+ max-width:820px;margin:0 auto;
222
+ background:var(--surface2);border:1px solid var(--border);
223
+ border-radius:var(--rad);display:flex;align-items:flex-end;gap:8px;
224
+ padding:12px 14px;transition:border-color .2s
225
+ }
226
+ #input-box:focus-within{border-color:var(--acc)}
227
+ #msg-in{
228
+ flex:1;background:transparent;border:none;outline:none;
229
+ color:var(--txt);font-size:14px;resize:none;max-height:180px;
230
+ line-height:1.6;font-family:'Syne',sans-serif
231
+ }
232
+ #msg-in::placeholder{color:var(--txt3)}
233
+ #send-btn{
234
+ width:36px;height:36px;border-radius:10px;border:none;
235
+ background:linear-gradient(135deg,var(--acc3),var(--acc));
236
+ color:#fff;cursor:pointer;
237
+ display:flex;align-items:center;justify-content:center;
238
+ transition:opacity .15s;flex-shrink:0
239
+ }
240
+ #send-btn:hover:not(:disabled){opacity:.85}
241
+ #send-btn:disabled{background:var(--surface3);cursor:not-allowed}
242
+ #input-hint{
243
+ text-align:center;font-size:11px;color:var(--txt3);
244
+ margin-top:8px;max-width:820px;margin-left:auto;margin-right:auto
245
+ }
328
246
 
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>
247
+ /* ── SETTINGS PAGES ── */
248
+ .sp-header{margin-bottom:24px}
249
+ .sp-title{font-size:22px;font-weight:800;letter-spacing:-.4px;margin-bottom:4px}
250
+ .sp-sub{font-size:13px;color:var(--txt2);line-height:1.6}
251
+ .card{
252
+ background:var(--surface2);border:1px solid var(--border);
253
+ border-radius:var(--rad);padding:20px;margin-bottom:14px
254
+ }
255
+ .card h3{font-size:13px;font-weight:700;margin-bottom:16px;color:var(--txt2);text-transform:uppercase;letter-spacing:.8px;display:flex;align-items:center;gap:8px}
256
+ .field{margin-bottom:14px}.field:last-child{margin:0}
257
+ .field label{display:block;font-size:12px;color:var(--txt3);margin-bottom:6px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}
258
+ .field input,.field select,.field textarea{
259
+ width:100%;padding:10px 13px;
260
+ background:var(--surface);border:1px solid var(--border);
261
+ border-radius:var(--rad-sm);color:var(--txt);
262
+ font-size:14px;outline:none;font-family:'Syne',sans-serif;
263
+ transition:border-color .15s
264
+ }
265
+ .field input:focus,.field select:focus,.field textarea:focus{border-color:var(--acc)}
266
+ .field input[type=password]{font-family:'JetBrains Mono',monospace;letter-spacing:.1em}
267
+ .field select option{background:var(--surface)}
268
+ .field textarea{resize:vertical;min-height:80px;line-height:1.6}
269
+ .row{display:flex;gap:12px}.row .field{flex:1}
270
+ .btn{padding:10px 20px;border-radius:var(--rad-sm);font-size:13px;cursor:pointer;border:none;font-weight:700;transition:all .15s;font-family:'Syne',sans-serif;letter-spacing:.2px}
271
+ .btn-primary{background:linear-gradient(135deg,var(--acc3),var(--acc));color:#fff}
272
+ .btn-primary:hover{opacity:.85}
273
+ .btn-ghost{background:transparent;border:1px solid var(--border);color:var(--txt2)}
274
+ .btn-ghost:hover{border-color:var(--border2);color:var(--txt)}
275
+ .btn-danger{background:transparent;border:1px solid var(--red);color:var(--red)}
276
+ .btn-danger:hover{background:var(--red);color:#fff}
277
+ .btn-sm{padding:6px 14px;font-size:12px}
278
+ .btn-xs{padding:4px 10px;font-size:11px}
279
+
280
+ /* ── MODEL GRID ── */
281
+ .model-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px;margin-top:4px}
282
+ .mc{
283
+ background:var(--surface);border:1.5px solid var(--border);
284
+ border-radius:var(--rad);padding:14px;cursor:pointer;
285
+ transition:all .2s;position:relative;overflow:hidden
286
+ }
287
+ .mc::before{content:'';position:absolute;inset:0;background:linear-gradient(135deg,var(--acc3),var(--acc));opacity:0;transition:opacity .2s}
288
+ .mc:hover{border-color:var(--border2);transform:translateY(-2px)}
289
+ .mc.sel{border-color:var(--acc);box-shadow:0 0 0 1px var(--acc)}
290
+ .mc.sel::before{opacity:.06}
291
+ .mc-check{position:absolute;top:10px;right:10px;width:18px;height:18px;border-radius:50%;background:var(--acc);color:#fff;display:none;align-items:center;justify-content:center;font-size:10px;font-weight:700}
292
+ .mc.sel .mc-check{display:flex}
293
+ .mc-prov{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;opacity:.7}
294
+ .mc-name{font-size:14px;font-weight:700;margin-bottom:4px;position:relative}
295
+ .mc-desc{font-size:11px;color:var(--txt2);position:relative;line-height:1.5}
296
+ .prov-anthropic .mc-prov{color:var(--orange)}
297
+ .prov-openai .mc-prov{color:var(--green)}
298
+ .prov-google .mc-prov{color:#4da6ff}
299
+ .prov-mistral .mc-prov{color:var(--acc2)}
300
+ .prov-local .mc-prov{color:var(--txt3)}
301
+ .prov-filter-bar{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:16px}
302
+ .pf{padding:6px 14px;border-radius:20px;border:1px solid var(--border);background:transparent;color:var(--txt3);cursor:pointer;font-size:12px;font-weight:700;transition:all .15s;font-family:'Syne',sans-serif}
303
+ .pf:hover{color:var(--txt2);border-color:var(--border2)}
304
+ .pf.on{background:var(--acc);border-color:var(--acc);color:#fff}
305
+
306
+ /* ── GATEWAY CARDS ── */
307
+ .gw-card{
308
+ background:var(--surface);border:1.5px solid var(--border);
309
+ border-radius:var(--rad);padding:16px;margin-bottom:10px;
310
+ display:flex;align-items:center;gap:14px;transition:border-color .15s
311
+ }
312
+ .gw-card.active-gw{border-color:var(--acc)}
313
+ .gw-left{flex:1;min-width:0}
314
+ .gw-name{font-size:14px;font-weight:700;margin-bottom:4px;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
315
+ .gw-url{font-size:11px;color:var(--txt3);font-family:'JetBrains Mono',monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
316
+ .badge{display:inline-flex;align-items:center;font-size:10px;font-weight:700;padding:2px 8px;border-radius:10px}
317
+ .badge-green{background:rgba(34,201,138,.15);color:var(--green)}
318
+ .badge-red{background:rgba(240,90,90,.15);color:var(--red)}
319
+ .badge-acc{background:rgba(124,106,247,.15);color:var(--acc2)}
320
+ .badge-gray{background:var(--surface3);color:var(--txt3)}
321
+ .gw-acts{display:flex;gap:6px;flex-shrink:0}
322
+
323
+ /* ── TOAST ── */
324
+ #toast{
325
+ position:fixed;bottom:24px;right:24px;
326
+ background:var(--surface2);border:1px solid var(--border2);
327
+ color:var(--txt);padding:12px 20px;border-radius:var(--rad-sm);
328
+ font-size:13px;z-index:999;opacity:0;
329
+ transform:translateY(10px);transition:all .25s;pointer-events:none;
330
+ box-shadow:0 8px 32px rgba(0,0,0,.4);font-weight:600
331
+ }
332
+ #toast.show{opacity:1;transform:none}
333
+
334
+ /* ── MODAL ── */
335
+ #modal-bg{display:none;position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:200;align-items:center;justify-content:center;backdrop-filter:blur(4px)}
336
+ #modal-bg.open{display:flex}
337
+ #modal{background:var(--surface);border:1px solid var(--border2);border-radius:var(--rad);padding:28px;width:480px;max-width:95vw;box-shadow:0 24px 80px rgba(0,0,0,.5)}
338
+ #modal h2{font-size:18px;font-weight:800;margin-bottom:20px;letter-spacing:-.3px}
339
+ .modal-acts{display:flex;justify-content:flex-end;gap:10px;margin-top:20px}
340
+
341
+ /* ── KEY STATUS ── */
342
+ .key-row{display:flex;align-items:center;gap:10px}
343
+ .key-row .field{flex:1;margin:0}
344
+ .key-status{width:8px;height:8px;border-radius:50%;background:var(--border2);flex-shrink:0}
345
+ .key-status.ok{background:var(--green);box-shadow:0 0 6px var(--green)}
346
+
347
+ /* ── RESPONSIVE ── */
348
+ @media(max-width:700px){
349
+ #sidebar{position:fixed;left:0;top:0;bottom:0;transform:translateX(-100%);box-shadow:4px 0 24px rgba(0,0,0,.5)}
350
+ #sidebar.open{transform:none}
351
+ #menu-btn{display:flex}
352
+ .model-grid{grid-template-columns:1fr 1fr}
353
+ }
354
+ </style>
339
355
  </head>
340
356
  <body>
341
357
 
342
- <!-- TOAST CONTAINER -->
343
- <div id="toast-container"></div>
344
-
345
358
  <!-- SIDEBAR -->
346
359
  <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>
360
+ <div id="sb-logo">
361
+ <div class="logo-icon">🦞</div>
362
+ <div>
363
+ <h1>PS Claw</h1>
364
+ <span>AI AGENT v1.1</span>
378
365
  </div>
379
- <div id="chat-list"></div>
380
366
  </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>
367
+ <button id="new-btn" onclick="newChat()">
368
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
369
+ Novo Chat
370
+ </button>
371
+ <div id="chat-list"></div>
372
+ <div id="sb-footer">
373
+ <div id="st-badge">
374
+ <div id="st-dot" class="warn"></div>
375
+ <span id="st-txt">Configure uma API key</span>
412
376
  </div>
413
377
  </div>
414
378
  </div>
@@ -416,764 +380,679 @@
416
380
  <!-- MAIN -->
417
381
  <div id="main">
418
382
  <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>
383
+ <div id="tb-left">
384
+ <button id="menu-btn" onclick="toggleSb()">☰</button>
385
+ <span id="tb-title">Chat</span>
426
386
  </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>
387
+ <div id="tb-right">
388
+ <div id="model-pill" onclick="goTab('models')">
389
+ <span class="dot"></span>
390
+ <span id="model-name-badge">Selecionar modelo</span>
439
391
  </div>
440
392
  </div>
441
393
  </div>
442
394
 
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>
395
+ <div id="tabs">
396
+ <div class="tab active" data-t="chat" onclick="goTab('chat')">💬 Chat</div>
397
+ <div class="tab" data-t="keys" onclick="goTab('keys')">🔑 API Keys</div>
398
+ <div class="tab" data-t="models" onclick="goTab('models')">🤖 Modelos</div>
399
+ <div class="tab" data-t="gateways" onclick="goTab('gateways')">🔌 Gateways</div>
400
+ <div class="tab" data-t="settings" onclick="goTab('settings')">⚙️ Config</div>
454
401
  </div>
455
- </div>
456
402
 
457
- <!-- MODAL -->
458
- <div id="modal-overlay" onclick="closeModal(event)">
459
- <div id="modal-box">
460
- <div id="modal-content"></div>
403
+ <!-- CHAT -->
404
+ <div class="page active" id="chat-page">
405
+ <div id="messages">
406
+ <div id="welcome">
407
+ <div class="w-icon">🦞</div>
408
+ <h2>Bem-vindo ao PS Claw</h2>
409
+ <p>Configure uma chave de API na aba <strong>🔑 API Keys</strong> e selecione um modelo em <strong>🤖 Modelos</strong> para começar.</p>
410
+ <div class="chips">
411
+ <div class="chip" onclick="sendQ('O que você consegue fazer?')">O que você faz?</div>
412
+ <div class="chip" onclick="sendQ('Me ajude a escrever um script Python para automação')">Escrever código</div>
413
+ <div class="chip" onclick="sendQ('Quais são as principais tendências de IA em 2025?')">Tendências de IA</div>
414
+ <div class="chip" onclick="sendQ('Explique o que é o PS Claw e como funciona')">Sobre o PS Claw</div>
415
+ </div>
416
+ </div>
417
+ </div>
418
+ <div id="input-wrap">
419
+ <div id="input-box">
420
+ <textarea id="msg-in" placeholder="Mensagem..." rows="1"
421
+ onkeydown="handleKey(event)" oninput="autoResize(this)"></textarea>
422
+ <button id="send-btn" onclick="sendMsg()">
423
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
424
+ <line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
425
+ </svg>
426
+ </button>
427
+ </div>
428
+ <div id="input-hint">Enter para enviar · Shift+Enter para nova linha</div>
429
+ </div>
461
430
  </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
431
 
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
- }
432
+ <!-- API KEYS -->
433
+ <div class="page" id="keys-page">
434
+ <div class="page-inner">
435
+ <div class="sp-header">
436
+ <div class="sp-title">🔑 API Keys</div>
437
+ <div class="sp-sub">Configure suas chaves de API. São salvas localmente no navegador e nunca enviadas a terceiros.</div>
438
+ </div>
587
439
 
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)}
440
+ <div class="card">
441
+ <h3>🟠 Anthropic Claude</h3>
442
+ <div class="field">
443
+ <label>API Key</label>
444
+ <div class="key-row">
445
+ <div class="field">
446
+ <input type="password" id="k-anthropic" placeholder="sk-ant-api03-..." oninput="saveKeys()"/>
447
+ </div>
448
+ <div class="key-status" id="ks-anthropic"></div>
597
449
  </div>
598
- <span style="font-size:11px;color:var(--text-muted);">${gw.type}</span>
599
450
  </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>` : ''}
451
+ <div style="font-size:12px;color:var(--txt3);margin-top:8px">
452
+ Obtenha em: <a href="https://console.anthropic.com/api-keys" target="_blank" style="color:var(--acc2)">console.anthropic.com</a> · Grátis $5 de crédito
605
453
  </div>
606
454
  </div>
607
- </div>
608
- `).join('');
609
- }
610
455
 
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>
456
+ <div class="card">
457
+ <h3>🟢 OpenAI — GPT-4</h3>
458
+ <div class="field">
459
+ <label>API Key</label>
460
+ <div class="key-row">
461
+ <div class="field">
462
+ <input type="password" id="k-openai" placeholder="sk-proj-..." oninput="saveKeys()"/>
463
+ </div>
464
+ <div class="key-status" id="ks-openai"></div>
646
465
  </div>
647
- `).join('')}
466
+ </div>
467
+ <div style="font-size:12px;color:var(--txt3);margin-top:8px">
468
+ Obtenha em: <a href="https://platform.openai.com/api-keys" target="_blank" style="color:var(--acc2)">platform.openai.com</a> · Grátis $5 de crédito
469
+ </div>
648
470
  </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
471
 
660
- function selectModel(id, name) {
661
- cfg.selectedModel = id;
662
- saveCfg();
663
- renderModelList();
664
- updateModelIndicator();
665
- showToast('Modelo selecionado: ' + name, 'success');
666
- }
472
+ <div class="card">
473
+ <h3>🔵 Google — Gemini</h3>
474
+ <div class="field">
475
+ <label>API Key</label>
476
+ <div class="key-row">
477
+ <div class="field">
478
+ <input type="password" id="k-google" placeholder="AIzaSy..." oninput="saveKeys()"/>
479
+ </div>
480
+ <div class="key-status" id="ks-google"></div>
481
+ </div>
482
+ </div>
483
+ <div style="font-size:12px;color:var(--txt3);margin-top:8px">
484
+ Obtenha em: <a href="https://aistudio.google.com/apikey" target="_blank" style="color:var(--acc2)">aistudio.google.com</a> · <strong style="color:var(--green)">Grátis para sempre</strong>
485
+ </div>
486
+ </div>
667
487
 
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
- }
488
+ <div class="card">
489
+ <h3>🟣 Mistral AI</h3>
490
+ <div class="field">
491
+ <label>API Key</label>
492
+ <div class="key-row">
493
+ <div class="field">
494
+ <input type="password" id="k-mistral" placeholder="..." oninput="saveKeys()"/>
495
+ </div>
496
+ <div class="key-status" id="ks-mistral"></div>
497
+ </div>
498
+ </div>
499
+ <div style="font-size:12px;color:var(--txt3);margin-top:8px">
500
+ Obtenha em: <a href="https://console.mistral.ai/api-keys" target="_blank" style="color:var(--acc2)">console.mistral.ai</a>
501
+ </div>
502
+ </div>
681
503
 
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)" />
504
+ <div style="display:flex;gap:10px;margin-top:8px">
505
+ <button class="btn btn-primary" onclick="testAllKeys()">🔍 Testar todas as chaves</button>
506
+ <button class="btn btn-ghost" onclick="goTab('models')">→ Selecionar modelo</button>
691
507
  </div>
692
508
  </div>
509
+ </div>
693
510
 
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>
511
+ <!-- MODELS -->
512
+ <div class="page" id="models-page">
513
+ <div class="page-inner">
514
+ <div class="sp-header">
515
+ <div class="sp-title">🤖 Modelos</div>
516
+ <div class="sp-sub">Selecione o modelo a ser usado no chat. Certifique-se de ter a API Key correspondente configurada.</div>
700
517
  </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>
518
+ <div class="prov-filter-bar">
519
+ <button class="pf on" data-p="all" onclick="filterM('all',this)">Todos</button>
520
+ <button class="pf" data-p="anthropic" onclick="filterM('anthropic',this)">🟠 Anthropic</button>
521
+ <button class="pf" data-p="openai" onclick="filterM('openai',this)">🟢 OpenAI</button>
522
+ <button class="pf" data-p="google" onclick="filterM('google',this)">🔵 Google</button>
523
+ <button class="pf" data-p="mistral" onclick="filterM('mistral',this)">🟣 Mistral</button>
524
+ <button class="pf" data-p="local" onclick="filterM('local',this)">⚫ Local</button>
710
525
  </div>
526
+ <div id="model-grid" class="model-grid"></div>
711
527
  </div>
528
+ </div>
712
529
 
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>
530
+ <!-- GATEWAYS -->
531
+ <div class="page" id="gateways-page">
532
+ <div class="page-inner">
533
+ <div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:20px;flex-wrap:wrap;gap:12px">
534
+ <div class="sp-header" style="margin:0">
535
+ <div class="sp-title">🔌 Gateways</div>
536
+ <div class="sp-sub">Opcional. Use se tiver um servidor PS Claw/OpenClaw rodando.</div>
537
+ </div>
538
+ <button class="btn btn-primary btn-sm" onclick="openAddGw()">+ Adicionar</button>
718
539
  </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>
540
+ <div class="card" style="margin-bottom:16px">
541
+ <h3>💡 O que é um Gateway?</h3>
542
+ <p style="font-size:13px;color:var(--txt2);line-height:1.75">
543
+ Gateways são <strong style="color:var(--txt)">opcionais</strong>. Sem eles, o PS Claw chama as APIs diretamente usando suas API Keys.<br><br>
544
+ Se você tem um servidor PS Claw/OpenClaw rodando (ex: <code style="background:var(--surface3);padding:1px 6px;border-radius:4px;font-family:'JetBrains Mono',monospace">localhost:18789</code>),
545
+ adicione aqui para rotear as mensagens através dele.
546
+ </p>
722
547
  </div>
548
+ <div id="gw-list"></div>
723
549
  </div>
550
+ </div>
724
551
 
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)" />
552
+ <!-- SETTINGS -->
553
+ <div class="page" id="settings-page">
554
+ <div class="page-inner">
555
+ <div class="sp-header">
556
+ <div class="sp-title">⚙️ Configurações</div>
734
557
  </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)" />
558
+ <div class="card">
559
+ <h3>👤 Perfil</h3>
560
+ <div class="row">
561
+ <div class="field">
562
+ <label>Seu nome</label>
563
+ <input type="text" id="cfg-name" placeholder="Você" oninput="saveCfg()"/>
564
+ </div>
565
+ <div class="field">
566
+ <label>Nome do agente</label>
567
+ <input type="text" id="cfg-agent" placeholder="PS Claw" oninput="saveCfg()"/>
568
+ </div>
569
+ </div>
738
570
  </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)" />
571
+ <div class="card">
572
+ <h3>💬 Comportamento</h3>
573
+ <div class="field">
574
+ <label>System prompt</label>
575
+ <textarea id="cfg-sys" placeholder="Você é a PS Claw, assistente de IA com acesso a ferramentas e ações executáveis no computador do usuário. Você pode: executar comandos no terminal/shell do sistema, dar cliques na tela e controlar o mouse, abrir aplicativos e programas, manipular arquivos e pastas. Sempre informe o que está executando e o resultado obtido. Seja direta, útil e proativa." oninput="saveCfg()"></textarea>
576
+ </div>
577
+ <div class="row">
578
+ <div class="field">
579
+ <label>Temperatura (0–2)</label>
580
+ <input type="number" id="cfg-temp" min="0" max="2" step="0.1" placeholder="0.7" oninput="saveCfg()"/>
581
+ </div>
582
+ <div class="field">
583
+ <label>Max tokens</label>
584
+ <input type="number" id="cfg-tok" min="256" max="128000" step="256" placeholder="4096" oninput="saveCfg()"/>
585
+ </div>
586
+ </div>
742
587
  </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)" />
588
+ <div class="card">
589
+ <h3>🗑️ Dados</h3>
590
+ <p style="font-size:13px;color:var(--txt2);margin-bottom:14px">Todos os dados são salvos localmente no seu navegador.</p>
591
+ <div style="display:flex;gap:10px;flex-wrap:wrap">
592
+ <button class="btn btn-danger btn-sm" onclick="clearChats()">Apagar histórico</button>
593
+ <button class="btn btn-danger btn-sm" onclick="clearAll()">Resetar tudo</button>
594
+ </div>
746
595
  </div>
747
596
  </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>
597
+ </div>
598
+ </div><!-- /main -->
599
+
600
+ <!-- MODAL ADD GATEWAY -->
601
+ <div id="modal-bg" onclick="closeMod(event)">
602
+ <div id="modal">
603
+ <h2>🔌 Adicionar Gateway</h2>
604
+ <div class="field"><label>Nome</label><input type="text" id="gw-n" placeholder="PS Claw Local"/></div>
605
+ <div class="field"><label>URL</label><input type="text" id="gw-u" placeholder="http://localhost:18789"/></div>
606
+ <div class="field"><label>Token (opcional)</label><input type="password" id="gw-t" placeholder="Bearer token"/></div>
607
+ <div class="modal-acts">
608
+ <button class="btn btn-ghost" onclick="closeMod()">Cancelar</button>
609
+ <button class="btn btn-primary" onclick="addGw()">Adicionar</button>
755
610
  </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
- }
611
+ </div>
612
+ </div>
769
613
 
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
- }
614
+ <div id="toast"></div>
776
615
 
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
- }
616
+ <script>
617
+ // ─── MODELOS ──────────────────────────────────────────────────────────────
618
+ const MODELS = [
619
+ // Anthropic
620
+ {id:'claude-opus-4-5', name:'Claude Opus 4.5', p:'anthropic', desc:'Mais poderoso · raciocínio complexo',
621
+ api:'https://api.anthropic.com/v1/messages'},
622
+ {id:'claude-sonnet-4-5', name:'Claude Sonnet 4.5', p:'anthropic', desc:'Rápido e inteligente · uso geral',
623
+ api:'https://api.anthropic.com/v1/messages'},
624
+ {id:'claude-haiku-4-5', name:'Claude Haiku 4.5', p:'anthropic', desc:'Ultra rápido · econômico',
625
+ api:'https://api.anthropic.com/v1/messages'},
626
+ // OpenAI
627
+ {id:'gpt-4o', name:'GPT-4o', p:'openai', desc:'Flagship multimodal da OpenAI',
628
+ api:'https://api.openai.com/v1/chat/completions'},
629
+ {id:'gpt-4o-mini', name:'GPT-4o mini', p:'openai', desc:'Rápido e barato · uso geral',
630
+ api:'https://api.openai.com/v1/chat/completions'},
631
+ {id:'gpt-4-turbo', name:'GPT-4 Turbo', p:'openai', desc:'Alta capacidade · longo contexto',
632
+ api:'https://api.openai.com/v1/chat/completions'},
633
+ {id:'o3-mini', name:'o3-mini', p:'openai', desc:'Raciocínio avançado · lógica',
634
+ api:'https://api.openai.com/v1/chat/completions'},
635
+ // Google
636
+ {id:'gemini-2.5-pro', name:'Gemini 2.5 Pro', p:'google', desc:'Mais poderoso do Google',
637
+ api:'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent'},
638
+ {id:'gemini-2.5-flash', name:'Gemini 2.5 Flash', p:'google', desc:'Rápido · contexto longo · gratuito',
639
+ api:'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent'},
640
+ {id:'gemini-1.5-flash', name:'Gemini 1.5 Flash', p:'google', desc:'Estável e confiável · gratuito',
641
+ api:'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent'},
642
+ // Mistral
643
+ {id:'mistral-large-latest', name:'Mistral Large', p:'mistral', desc:'Poderoso · multilingual',
644
+ api:'https://api.mistral.ai/v1/chat/completions'},
645
+ {id:'mistral-small-latest', name:'Mistral Small', p:'mistral', desc:'Rápido · barato · europeu',
646
+ api:'https://api.mistral.ai/v1/chat/completions'},
647
+ // Local
648
+ {id:'llama3.2', name:'Llama 3.2 (Ollama)', p:'local', desc:'Roda localmente · configure URL do gateway',
649
+ api:''},
650
+ ];
786
651
 
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
- }
652
+ // ─── ESTADO ───────────────────────────────────────────────────────────────
653
+ let S = JSON.parse(localStorage.getItem('psc2') || '{}');
654
+ S.chats = S.chats || [];
655
+ S.model = S.model || null;
656
+ S.gws = S.gws || [];
657
+ S.activeGw = S.activeGw || null;
658
+ S.keys = S.keys || {anthropic:'',openai:'',google:'',mistral:''};
659
+ S.cfg = S.cfg || {name:'Você',agent:'PS Claw',sys:'',temp:'0.7',tok:'4096'};
807
660
 
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();
661
+ let curChat = null;
662
+ let isTyping = false;
663
+ let mFilter = 'all';
664
+
665
+ function save(){ localStorage.setItem('psc2', JSON.stringify(S)) }
666
+
667
+ // ─── TABS ─────────────────────────────────────────────────────────────────
668
+ function goTab(t){
669
+ document.querySelectorAll('.tab').forEach(e=>e.classList.toggle('active',e.dataset.t===t));
670
+ document.querySelectorAll('.page').forEach(e=>e.classList.remove('active'));
671
+ document.getElementById(t+'-page').classList.add('active');
672
+ document.getElementById('tb-title').textContent={chat:'Chat',keys:'API Keys',models:'Modelos',gateways:'Gateways',settings:'Configurações'}[t]||t;
673
+ if(t==='models') renderModels();
674
+ if(t==='gateways') renderGws();
675
+ if(t==='keys') loadKeys();
676
+ if(t==='settings') loadCfg();
815
677
  }
816
678
 
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');
679
+ // ─── CHAT ─────────────────────────────────────────────────────────────────
680
+ function newChat(){
681
+ const id='c'+Date.now();
682
+ S.chats.unshift({id,title:'Nova conversa',msgs:[],ts:Date.now()});
683
+ save(); selectChat(id); goTab('chat');
825
684
  }
826
-
827
- function selectChat(id) {
828
- currentSession = sessions.find(s => s.id === id);
829
- if (!currentSession) return;
830
- renderChatList();
831
- renderMessages();
685
+ function selectChat(id){
686
+ curChat=S.chats.find(c=>c.id===id);
687
+ renderChatList(); renderMsgs();
832
688
  }
833
-
834
- function deleteChat(id, e) {
689
+ function deleteChat(id,e){
835
690
  e.stopPropagation();
836
- sessions = sessions.filter(s => s.id !== id);
837
- saveSessions();
838
- if (currentSession?.id === id) { currentSession = null; renderMessages(); }
839
- renderChatList();
691
+ S.chats=S.chats.filter(c=>c.id!==id);
692
+ if(curChat?.id===id){curChat=null;renderMsgs();}
693
+ save(); renderChatList();
840
694
  }
841
-
842
- function saveSessions() {
843
- localStorage.setItem('ps-claw-sessions', JSON.stringify(sessions));
695
+ function renderChatList(){
696
+ document.getElementById('chat-list').innerHTML=S.chats.map(c=>`
697
+ <div class="ci ${c.id===curChat?.id?'active':''}" onclick="selectChat('${c.id}')">
698
+ <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1">${h(c.title)}</span>
699
+ <button class="ci-del" onclick="deleteChat('${c.id}',event)">✕</button>
700
+ </div>`).join('');
844
701
  }
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('');
702
+ function parseMsg(text){
703
+ var lines=text.split('\n');var parts=[];var buf=[];
704
+ var flush=function(){if(!buf.length)return;var raw=buf.join('\n').trim();buf.length=0;if(!raw)return;var cls='act-cmd';var lo=raw.toLowerCase();if(/\b(click|clique|mouse|botao|botão|right.?click|duplo|dblclick|scroll|arrastar|drag)\b/.test(lo))cls='act-click';else if(/\b(abrir|open|launch|start|iniciar|executar app|\.exe|notepad|calc|chrome|firefox|edge|explorer)\b/.test(lo))cls='act-app';else if(/\b(arquivo|file|criar|deletar|remover|delete|salvar|save|mover|move|copiar|copy|mkdir)\b/.test(lo))cls='act-file';else if(/^(erro|error|falha|failed|exception|traceback|fatal)/i.test(lo))cls='act-err';else if(/^(ok|sucesso|success|concluído|concluido|feito|done|pronto)/i.test(lo))cls='act-ok';parts.push({type:'action',text:raw,cls});};
705
+ for(var i=0;i<lines.length;i++){var trimmed=lines[i].trim();if(!trimmed||trimmed.charAt(0)=='-'||trimmed.charAt(0)=='*'||trimmed.charAt(0)=='#'||trimmed.indexOf('\`\`\`')===0){buf.push(lines[i]);continue;}var isAct=/^(> |\$ |❯|▶|\$|PS |C:|D:)/.test(trimmed)||/^(Executando|Running |Abrindo|Opening |Exec)/.test(trimmed)||/^(npm |pip |python |node |git |ls |cd |mkdir |rm |cat |open |start |launch |echo |mv |cp |touch |chmod |docker |kubectl |make )/i.test(trimmed);if(isAct){flush();buf.push(lines[i]);}else buf.push(lines[i]);}flush();if(!parts.length&&text.trim())parts.push({type:'text',text});return parts;
855
706
  }
856
707
 
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>
708
+ function renderMsgs(){
709
+ const box=document.getElementById('messages');
710
+ if(!curChat||!curChat.msgs.length){
711
+ box.innerHTML=`<div id="welcome">
712
+ <div class="w-icon">🦞</div>
713
+ <h2>${h(S.cfg.agent||'PS Claw')}</h2>
714
+ <p>Configure uma chave de API na aba <strong>🔑 API Keys</strong> e selecione um modelo em <strong>🤖 Modelos</strong> para começar.</p>
715
+ <div class="chips">
716
+ <div class="chip" onclick="sendQ('O que você consegue fazer?')">O que você faz?</div>
717
+ <div class="chip" onclick="sendQ('Me ajude com um script Python')">Escrever código</div>
718
+ <div class="chip" onclick="sendQ('Tendências de IA em 2025')">Tendências de IA</div>
719
+ <div class="chip" onclick="sendQ('Explique o PS Claw')">Sobre o PS Claw</div>
869
720
  </div>
870
721
  </div>`;
871
722
  return;
872
723
  }
873
- box.innerHTML = currentSession.messages.map(m => renderMsg(m)).join('');
874
- box.scrollTop = box.scrollHeight;
724
+ box.innerHTML=curChat.msgs.map(function(m){var body="";if(m.role==="user"){body="<div class=\"bbl user\">"+fmt(m.content)+"</div>";}else{var parts=parseMsg(m.content||"");body=parts.map(function(p){if(p.type==="text")return fmt(p.text);return "<div class=\"act "+p.cls+"\">"+fmt(p.text)+"</div>";}).join("");body="<div class=\"bbl ai\">"+body+"</div>";}return "<div class=\"msg-row "+(m.role==="user"?"user":"")+"\">\n <div class=\\"av \"+(m.role==="user"?"user":"ai")+"\">"+(m.role==="user"?(S.cfg.name||"V")[0].toUpperCase():"🦞")+"</div>\n "+body+"</div>";}).join("");box.scrollTop=box.scrollHeight;}
725
+ function addTyping(){
726
+ const box=document.getElementById('messages');
727
+ document.getElementById('welcome')?.remove();
728
+ const el=document.createElement('div');
729
+ el.id='typdiv';el.className='msg-row';
730
+ el.innerHTML=`<div class="av ai">🦞</div><div class="bbl ai"><div class="typing"><span></span><span></span><span></span></div></div>`;
731
+ box.appendChild(el);box.scrollTop=box.scrollHeight;
875
732
  }
733
+ function rmTyping(){document.getElementById('typdiv')?.remove()}
734
+
735
+ // ─── ENVIO ────────────────────────────────────────────────────────────────
736
+ async function sendMsg(){
737
+ const inp=document.getElementById('msg-in');
738
+ const text=inp.value.trim();
739
+ if(!text||isTyping)return;
740
+ if(!curChat)newChat();
741
+
742
+ // Verificar se tem modelo e chave
743
+ const model=MODELS.find(m=>m.id===S.model);
744
+ if(!model){toast('⚠️ Selecione um modelo na aba 🤖 Modelos');goTab('models');return;}
745
+ const key=S.keys[model.p];
746
+ if(!key&&model.p!=='local'){toast('⚠️ Configure a API Key na aba 🔑 API Keys');goTab('keys');return;}
747
+
748
+ inp.value='';inp.style.height='auto';
749
+ document.getElementById('send-btn').disabled=true;
750
+ isTyping=true;
751
+ document.getElementById('welcome')?.remove();
876
752
 
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
- }
753
+ curChat.msgs.push({role:'user',content:text});
754
+ if(curChat.msgs.length===1)curChat.title=text.slice(0,44)+(text.length>44?'':'');
755
+ save();renderChatList();renderMsgs();addTyping();
885
756
 
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();
757
+ let reply;
758
+ try{
759
+ reply = await callApi(model, text, key);
760
+ }catch(e){
761
+ reply=`❌ **Erro:** ${e.message}`;
762
+ }
763
+ rmTyping();
764
+ curChat.msgs.push({role:'assistant',content:reply});
765
+ save();renderMsgs();
766
+ isTyping=false;
767
+ document.getElementById('send-btn').disabled=false;
768
+ inp.focus();
897
769
  }
898
770
 
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();
771
+ // ─── CHAMADAS DE API ──────────────────────────────────────────────────────
772
+ async function callApi(model, text, key){
773
+ const history = curChat.msgs.slice(0,-1).map(m=>({role:m.role,content:m.content}));
774
+ const messages = [...history, {role:'user',content:text}];
775
+ const sys = S.cfg.sys || 'Você é a PS Claw, uma assistente de IA inteligente e prestativa. Você tem acesso a ferramentas poderosas e pode executar ações no computador do usuário, incluindo: executar comandos no terminal/shell do sistema, dar cliques na tela e controlar o mouse, abrir aplicativos e programas, e manipular arquivos. Sempre que executar uma dessas ações, explique o que está fazendo e informe o resultado. Seja direta, útil e proativa.';
776
+ const temp = parseFloat(S.cfg.temp)||0.7;
777
+ const maxTok = parseInt(S.cfg.tok)||4096;
778
+
779
+ // Verificar se tem gateway ativo (prioridade sobre chamada direta)
780
+ const gw = S.gws.find(g=>g.id===S.activeGw);
781
+ if(gw){
782
+ return await callGateway(gw, model, messages, sys, temp, maxTok);
917
783
  }
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
784
 
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
- });
785
+ // Chamada direta por provedor
786
+ if(model.p==='anthropic') return await callAnthropic(model, messages, sys, key, maxTok, temp);
787
+ if(model.p==='openai') return await callOpenAI(model, messages, sys, key, maxTok, temp);
788
+ if(model.p==='google') return await callGoogle(model, messages, sys, key, maxTok, temp);
789
+ if(model.p==='mistral') return await callOpenAI(model, messages, sys, key, maxTok, temp, 'https://api.mistral.ai/v1/chat/completions');
790
+ throw new Error('Provedor desconhecido. Configure um gateway ou selecione outro modelo.');
791
+ }
942
792
 
943
- removeTypingIndicator();
793
+ // Proxy URL para evitar CORS (usa o servidor local)
794
+ function px(url){ return `/proxy?url=${encodeURIComponent(url)}`; }
795
+
796
+ async function callAnthropic(model, messages, sys, key, maxTok, temp){
797
+ // Separar mensagens de sistema
798
+ const msgs = messages.filter(m=>m.role!=='system').map(m=>({role:m.role,content:m.content}));
799
+ const r = await fetch(px('https://api.anthropic.com/v1/messages'),{
800
+ method:'POST',
801
+ headers:{'Content-Type':'application/json','x-api-key':key,'anthropic-version':'2023-06-01'},
802
+ body:JSON.stringify({model:model.id,max_tokens:maxTok,temperature:temp,system:sys,messages:msgs})
803
+ });
804
+ const d=await r.json();
805
+ if(!r.ok)throw new Error(d.error?.message||JSON.stringify(d));
806
+ return d.content?.[0]?.text||JSON.stringify(d);
807
+ }
944
808
 
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
- }
809
+ async function callOpenAI(model, messages, sys, key, maxTok, temp, overrideUrl){
810
+ const url = overrideUrl || 'https://api.openai.com/v1/chat/completions';
811
+ const msgs = [{role:'system',content:sys},...messages];
812
+ const r = await fetch(px(url),{
813
+ method:'POST',
814
+ headers:{'Content-Type':'application/json','Authorization':'Bearer '+key},
815
+ body:JSON.stringify({model:model.id,max_tokens:maxTok,temperature:temp,messages:msgs})
816
+ });
817
+ const d=await r.json();
818
+ if(!r.ok)throw new Error(d.error?.message||JSON.stringify(d));
819
+ return d.choices?.[0]?.message?.content||JSON.stringify(d);
820
+ }
952
821
 
953
- currentSession.messages.push({ role: 'assistant', content: replyText });
954
- saveSessions();
955
- renderMessages();
822
+ async function callGoogle(model, messages, sys, key, maxTok, temp){
823
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${model.id}:generateContent?key=${key}`;
824
+ const contents = messages.map(m=>({
825
+ role: m.role==='assistant'?'model':'user',
826
+ parts:[{text:m.content}]
827
+ }));
828
+ const r = await fetch(px(url),{
829
+ method:'POST',
830
+ headers:{'Content-Type':'application/json'},
831
+ body:JSON.stringify({
832
+ contents,
833
+ systemInstruction:{parts:[{text:sys}]},
834
+ generationConfig:{maxOutputTokens:maxTok,temperature:temp}
835
+ })
836
+ });
837
+ const d=await r.json();
838
+ if(!r.ok)throw new Error(d.error?.message||JSON.stringify(d));
839
+ return d.candidates?.[0]?.content?.parts?.[0]?.text||JSON.stringify(d);
840
+ }
956
841
 
957
- } catch (err) {
958
- removeTypingIndicator();
959
- const fallback = await tryFallbackChat(text);
960
- currentSession.messages.push({ role: 'assistant', content: fallback });
961
- saveSessions();
962
- renderMessages();
842
+ async function callGateway(gw, model, messages, sys, temp, maxTok){
843
+ const endpoints=['/v1/chat/completions','/api/chat','/chat'];
844
+ const headers={'Content-Type':'application/json',...(gw.token&&{Authorization:'Bearer '+gw.token})};
845
+ const body={model:model.id,messages:[{role:'system',content:sys},...messages],temperature:temp,max_tokens:maxTok};
846
+ for(const ep of endpoints){
847
+ try{
848
+ const r=await fetch(px(gw.url+ep),{method:'POST',headers,body:JSON.stringify(body),signal:AbortSignal.timeout(60000)});
849
+ if(!r.ok)continue;
850
+ const d=await r.json();
851
+ return d.choices?.[0]?.message?.content||d.reply||d.text||d.content||JSON.stringify(d);
852
+ }catch{}
963
853
  }
964
-
965
- isTyping = false;
966
- document.getElementById('send-btn').disabled = false;
967
- document.getElementById('msg-input').focus();
854
+ throw new Error('Gateway não respondeu. Verifique a URL.');
968
855
  }
969
856
 
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 {}
857
+ // ─── API KEY TEST ─────────────────────────────────────────────────────────
858
+ async function testAllKeys(){
859
+ toast('🔍 Testando chaves...',3000);
860
+ const tests=[
861
+ {id:'anthropic',fn:async()=>{
862
+ if(!S.keys.anthropic)return false;
863
+ const r=await fetch(px('https://api.anthropic.com/v1/models'),{headers:{'x-api-key':S.keys.anthropic,'anthropic-version':'2023-06-01'}});
864
+ return r.ok;
865
+ }},
866
+ {id:'openai',fn:async()=>{
867
+ if(!S.keys.openai)return false;
868
+ const r=await fetch(px('https://api.openai.com/v1/models'),{headers:{Authorization:'Bearer '+S.keys.openai}});
869
+ return r.ok;
870
+ }},
871
+ {id:'google',fn:async()=>{
872
+ if(!S.keys.google)return false;
873
+ const r=await fetch(px(`https://generativelanguage.googleapis.com/v1beta/models?key=${S.keys.google}`));
874
+ return r.ok;
875
+ }},
876
+ {id:'mistral',fn:async()=>{
877
+ if(!S.keys.mistral)return false;
878
+ const r=await fetch(px('https://api.mistral.ai/v1/models'),{headers:{Authorization:'Bearer '+S.keys.mistral}});
879
+ return r.ok;
880
+ }},
881
+ ];
882
+ for(const t of tests){
883
+ try{
884
+ const ok=await t.fn();
885
+ const el=document.getElementById('ks-'+t.id);
886
+ if(el)el.className='key-status '+(ok?'ok':'');
887
+ }catch{}
992
888
  }
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})`;
889
+ updateStatus();
890
+ toast('✅ Teste concluído!');
996
891
  }
997
892
 
998
- function sendQuick(text) {
999
- document.getElementById('msg-input').value = text;
1000
- sendMessage();
893
+ function updateStatus(){
894
+ const dot=document.getElementById('st-dot');
895
+ const txt=document.getElementById('st-txt');
896
+ const model=MODELS.find(m=>m.id===S.model);
897
+ const key=model?S.keys[model.p]:'';
898
+ if(!S.model){dot.className='warn';txt.textContent='Selecione um modelo';return;}
899
+ if(!key&&model?.p!=='local'){dot.className='off';txt.textContent='Sem API Key para '+model.p;return;}
900
+ dot.className='on';txt.textContent=(model?.name||S.model)+' · Pronto';
1001
901
  }
1002
902
 
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');
903
+ // ─── MODELOS ──────────────────────────────────────────────────────────────
904
+ function renderModels(){
905
+ const list=mFilter==='all'?MODELS:MODELS.filter(m=>m.p===mFilter);
906
+ document.getElementById('model-grid').innerHTML=list.map(m=>`
907
+ <div class="mc prov-${m.p} ${S.model===m.id?'sel':''}" onclick="selModel('${m.id}')">
908
+ <div class="mc-check">✓</div>
909
+ <div class="mc-prov">${{anthropic:'🟠 Anthropic',openai:'🟢 OpenAI',google:'🔵 Google',mistral:'🟣 Mistral',local:'⚫ Local'}[m.p]}</div>
910
+ <div class="mc-name">${h(m.name)}</div>
911
+ <div class="mc-desc">${h(m.desc)}</div>
912
+ </div>`).join('');
1095
913
  }
1096
-
1097
- function closeModal(e) {
1098
- if (e && e.target !== document.getElementById('modal-overlay')) return;
1099
- document.getElementById('modal-overlay').classList.remove('open');
914
+ function filterM(p,btn){
915
+ mFilter=p;
916
+ document.querySelectorAll('.pf').forEach(b=>b.classList.remove('on'));
917
+ btn.classList.add('on');
918
+ renderModels();
1100
919
  }
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');
920
+ function selModel(id){
921
+ S.model=id;save();
922
+ renderModels();
923
+ const m=MODELS.find(x=>x.id===id);
924
+ document.getElementById('model-name-badge').textContent=m?.name||id;
925
+ updateStatus();
926
+ toast(' Modelo: '+m?.name);
1115
927
  }
1116
928
 
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');
929
+ // ─── KEYS ─────────────────────────────────────────────────────────────────
930
+ function loadKeys(){
931
+ document.getElementById('k-anthropic').value=S.keys.anthropic||'';
932
+ document.getElementById('k-openai').value=S.keys.openai||'';
933
+ document.getElementById('k-google').value=S.keys.google||'';
934
+ document.getElementById('k-mistral').value=S.keys.mistral||'';
935
+ }
936
+ function saveKeys(){
937
+ S.keys.anthropic=document.getElementById('k-anthropic').value.trim();
938
+ S.keys.openai=document.getElementById('k-openai').value.trim();
939
+ S.keys.google=document.getElementById('k-google').value.trim();
940
+ S.keys.mistral=document.getElementById('k-mistral').value.trim();
941
+ save();updateStatus();
1129
942
  }
1130
943
 
1131
- // === UTILS ===
1132
- function handleKey(e) {
1133
- if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
944
+ // ─── GATEWAYS ─────────────────────────────────────────────────────────────
945
+ function renderGws(){
946
+ const el=document.getElementById('gw-list');
947
+ if(!S.gws.length){
948
+ el.innerHTML=`<div style="text-align:center;padding:32px;color:var(--txt3)">
949
+ <div style="font-size:32px;margin-bottom:10px">🔌</div>
950
+ Nenhum gateway. O PS Claw chamará as APIs diretamente com suas API Keys.
951
+ </div>`;return;
952
+ }
953
+ el.innerHTML=S.gws.map(g=>`
954
+ <div class="gw-card ${S.activeGw===g.id?'active-gw':''}">
955
+ <div class="gw-left">
956
+ <div class="gw-name">
957
+ ${h(g.name)}
958
+ ${S.activeGw===g.id?'<span class="badge badge-acc">Ativo</span>':''}
959
+ <span class="badge badge-gray" id="gst-${g.id}">...</span>
960
+ </div>
961
+ <div class="gw-url">${h(g.url)}</div>
962
+ </div>
963
+ <div class="gw-acts">
964
+ <button class="btn btn-ghost btn-xs" onclick="testGw('${g.id}')">Testar</button>
965
+ ${S.activeGw!==g.id?`<button class="btn btn-primary btn-xs" onclick="setGw('${g.id}')">Usar</button>`:''}
966
+ <button class="btn btn-danger btn-xs" onclick="delGw('${g.id}')">✕</button>
967
+ </div>
968
+ </div>`).join('');
969
+ S.gws.forEach(g=>testGw(g.id,true));
1134
970
  }
1135
- function autoResize(el) {
1136
- el.style.height = 'auto';
1137
- el.style.height = Math.min(el.scrollHeight, 200) + 'px';
971
+ function openAddGw(){
972
+ document.getElementById('gw-n').value='';
973
+ document.getElementById('gw-u').value='http://localhost:18789';
974
+ document.getElementById('gw-t').value='';
975
+ document.getElementById('modal-bg').classList.add('open');
1138
976
  }
1139
- function escHtml(s) {
1140
- return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
977
+ function closeMod(e){if(!e||e.target===document.getElementById('modal-bg'))document.getElementById('modal-bg').classList.remove('open')}
978
+ function addGw(){
979
+ const id='gw'+Date.now();
980
+ const name=document.getElementById('gw-n').value.trim()||'Gateway';
981
+ const url=document.getElementById('gw-u').value.trim()||'http://localhost:18789';
982
+ const token=document.getElementById('gw-t').value.trim();
983
+ S.gws.push({id,name,url,token});
984
+ if(!S.activeGw)S.activeGw=id;
985
+ save();closeMod();renderGws();toast('✅ Gateway adicionado!');
1141
986
  }
1142
- function escAttr(s) {
1143
- return (s||'').replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
987
+ function delGw(id){S.gws=S.gws.filter(g=>g.id!==id);if(S.activeGw===id)S.activeGw=S.gws[0]?.id||null;save();renderGws();}
988
+ function setGw(id){S.activeGw=id;save();renderGws();toast('✅ Gateway ativo!');}
989
+ async function testGw(id,silent){
990
+ const gw=S.gws.find(g=>g.id===id);const el=document.getElementById('gst-'+id);
991
+ if(!el)return;el.textContent='...';el.className='badge badge-gray';
992
+ try{
993
+ const r=await fetch(px(gw.url+'/health'),{headers:gw.token?{Authorization:'Bearer '+gw.token}:{},signal:AbortSignal.timeout(4000)});
994
+ el.textContent=r.ok?'Online':'Erro';
995
+ el.className='badge '+(r.ok?'badge-green':'badge-red');
996
+ if(!silent&&r.ok)toast('✅ Gateway online!');
997
+ }catch{el.textContent='Offline';el.className='badge badge-red';if(!silent)toast('❌ Inacessível');}
1144
998
  }
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;
999
+
1000
+ // ─── CFG ──────────────────────────────────────────────────────────────────
1001
+ function loadCfg(){
1002
+ document.getElementById('cfg-name').value=S.cfg.name||'Você';
1003
+ document.getElementById('cfg-agent').value=S.cfg.agent||'PS Claw';
1004
+ document.getElementById('cfg-sys').value=S.cfg.sys||'';
1005
+ document.getElementById('cfg-temp').value=S.cfg.temp||'0.7';
1006
+ document.getElementById('cfg-tok').value=S.cfg.tok||'4096';
1160
1007
  }
1161
- function toggleSidebar() {
1162
- document.getElementById('sidebar').classList.toggle('open');
1008
+ function saveCfg(){
1009
+ S.cfg.name=document.getElementById('cfg-name').value;
1010
+ S.cfg.agent=document.getElementById('cfg-agent').value;
1011
+ S.cfg.sys=document.getElementById('cfg-sys').value;
1012
+ S.cfg.temp=document.getElementById('cfg-temp').value;
1013
+ S.cfg.tok=document.getElementById('cfg-tok').value;
1014
+ save();
1163
1015
  }
1164
-
1165
- function saveCfg() {
1166
- localStorage.setItem('ps-claw-cfg', JSON.stringify(cfg));
1016
+ function clearChats(){if(!confirm('Apagar histórico?'))return;S.chats=[];curChat=null;save();renderChatList();renderMsgs();toast('🗑️ Histórico apagado');}
1017
+ function clearAll(){if(!confirm('Resetar tudo?'))return;localStorage.removeItem('psc2');location.reload();}
1018
+
1019
+ // ─── UTILS ────────────────────────────────────────────────────────────────
1020
+ function h(s=''){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
1021
+ function fmt(text=''){
1022
+ let t=h(text);
1023
+ t=t.replace(/```(\w*)\n?([\s\S]*?)```/g,(_,l,c)=>`<pre><code>${c.trim()}</code></pre>`);
1024
+ t=t.replace(/`([^`]+)`/g,'<code>$1</code>');
1025
+ t=t.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>');
1026
+ t=t.replace(/\*(.+?)\*/g,'<em>$1</em>');
1027
+ t=t.replace(/^### (.+)$/gm,'<h3>$1</h3>');
1028
+ t=t.replace(/^## (.+)$/gm,'<h2>$1</h2>');
1029
+ t=t.replace(/^# (.+)$/gm,'<h1>$1</h1>');
1030
+ t=t.replace(/^- (.+)$/gm,'<li>$1</li>');
1031
+ t=t.replace(/(\|.+\|\n?)+/g,tbl=>{
1032
+ const rows=tbl.trim().split('\n').filter(r=>!r.match(/^\|[-\s|]+\|$/));
1033
+ return '<table>'+rows.map((r,i)=>{
1034
+ const cells=r.split('|').filter((_,j)=>j>0&&j<r.split('|').length-1);
1035
+ return i===0?`<tr>${cells.map(c=>`<th>${c.trim()}</th>`).join('')}</tr>`:`<tr>${cells.map(c=>`<td>${c.trim()}</td>`).join('')}</tr>`;
1036
+ }).join('')+'</table>';
1037
+ });
1038
+ t=t.split('\n\n').map(p=>p.startsWith('<')?p:`<p>${p.replace(/\n/g,'<br>')}</p>`).join('');
1039
+ return t;
1167
1040
  }
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);
1041
+ function toast(msg,dur=2600){
1042
+ const el=document.getElementById('toast');
1043
+ el.textContent=msg;el.classList.add('show');
1044
+ setTimeout(()=>el.classList.remove('show'),dur);
1176
1045
  }
1046
+ function handleKey(e){if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendMsg();}}
1047
+ function autoResize(el){el.style.height='auto';el.style.height=Math.min(el.scrollHeight,180)+'px';}
1048
+ function sendQ(t){document.getElementById('msg-in').value=t;goTab('chat');sendMsg();}
1049
+ function toggleSb(){document.getElementById('sidebar').classList.toggle('open');}
1050
+
1051
+ // ─── BOOT ─────────────────────────────────────────────────────────────────
1052
+ renderChatList();renderMsgs();updateStatus();
1053
+ const bm=MODELS.find(m=>m.id===S.model);
1054
+ if(bm)document.getElementById('model-name-badge').textContent=bm.name;
1055
+ loadKeys();
1177
1056
  </script>
1178
1057
  </body>
1179
1058
  </html>