ps-claw 1.1.0 β†’ 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,414 +1,362 @@
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
+ /* typing */
196
+ .typing{display:flex;gap:4px;align-items:center;padding:14px 16px}
197
+ .typing span{width:6px;height:6px;border-radius:50%;background:var(--acc);animation:pulse 1.2s infinite}
198
+ .typing span:nth-child(2){animation-delay:.2s}
199
+ .typing span:nth-child(3){animation-delay:.4s}
200
+ @keyframes pulse{0%,60%,100%{opacity:.2;transform:scale(.8)}30%{opacity:1;transform:scale(1)}}
201
+
202
+ /* ── INPUT ── */
203
+ #input-wrap{padding:16px 20px 20px;background:var(--surface);border-top:1px solid var(--border);flex-shrink:0}
204
+ #input-box{
205
+ max-width:820px;margin:0 auto;
206
+ background:var(--surface2);border:1px solid var(--border);
207
+ border-radius:var(--rad);display:flex;align-items:flex-end;gap:8px;
208
+ padding:12px 14px;transition:border-color .2s
209
+ }
210
+ #input-box:focus-within{border-color:var(--acc)}
211
+ #msg-in{
212
+ flex:1;background:transparent;border:none;outline:none;
213
+ color:var(--txt);font-size:14px;resize:none;max-height:180px;
214
+ line-height:1.6;font-family:'Syne',sans-serif
215
+ }
216
+ #msg-in::placeholder{color:var(--txt3)}
217
+ #send-btn{
218
+ width:36px;height:36px;border-radius:10px;border:none;
219
+ background:linear-gradient(135deg,var(--acc3),var(--acc));
220
+ color:#fff;cursor:pointer;
221
+ display:flex;align-items:center;justify-content:center;
222
+ transition:opacity .15s;flex-shrink:0
223
+ }
224
+ #send-btn:hover:not(:disabled){opacity:.85}
225
+ #send-btn:disabled{background:var(--surface3);cursor:not-allowed}
226
+ #input-hint{
227
+ text-align:center;font-size:11px;color:var(--txt3);
228
+ margin-top:8px;max-width:820px;margin-left:auto;margin-right:auto
229
+ }
328
230
 
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>
231
+ /* ── SETTINGS PAGES ── */
232
+ .sp-header{margin-bottom:24px}
233
+ .sp-title{font-size:22px;font-weight:800;letter-spacing:-.4px;margin-bottom:4px}
234
+ .sp-sub{font-size:13px;color:var(--txt2);line-height:1.6}
235
+ .card{
236
+ background:var(--surface2);border:1px solid var(--border);
237
+ border-radius:var(--rad);padding:20px;margin-bottom:14px
238
+ }
239
+ .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}
240
+ .field{margin-bottom:14px}.field:last-child{margin:0}
241
+ .field label{display:block;font-size:12px;color:var(--txt3);margin-bottom:6px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}
242
+ .field input,.field select,.field textarea{
243
+ width:100%;padding:10px 13px;
244
+ background:var(--surface);border:1px solid var(--border);
245
+ border-radius:var(--rad-sm);color:var(--txt);
246
+ font-size:14px;outline:none;font-family:'Syne',sans-serif;
247
+ transition:border-color .15s
248
+ }
249
+ .field input:focus,.field select:focus,.field textarea:focus{border-color:var(--acc)}
250
+ .field input[type=password]{font-family:'JetBrains Mono',monospace;letter-spacing:.1em}
251
+ .field select option{background:var(--surface)}
252
+ .field textarea{resize:vertical;min-height:80px;line-height:1.6}
253
+ .row{display:flex;gap:12px}.row .field{flex:1}
254
+ .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}
255
+ .btn-primary{background:linear-gradient(135deg,var(--acc3),var(--acc));color:#fff}
256
+ .btn-primary:hover{opacity:.85}
257
+ .btn-ghost{background:transparent;border:1px solid var(--border);color:var(--txt2)}
258
+ .btn-ghost:hover{border-color:var(--border2);color:var(--txt)}
259
+ .btn-danger{background:transparent;border:1px solid var(--red);color:var(--red)}
260
+ .btn-danger:hover{background:var(--red);color:#fff}
261
+ .btn-sm{padding:6px 14px;font-size:12px}
262
+ .btn-xs{padding:4px 10px;font-size:11px}
263
+
264
+ /* ── MODEL GRID ── */
265
+ .model-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px;margin-top:4px}
266
+ .mc{
267
+ background:var(--surface);border:1.5px solid var(--border);
268
+ border-radius:var(--rad);padding:14px;cursor:pointer;
269
+ transition:all .2s;position:relative;overflow:hidden
270
+ }
271
+ .mc::before{content:'';position:absolute;inset:0;background:linear-gradient(135deg,var(--acc3),var(--acc));opacity:0;transition:opacity .2s}
272
+ .mc:hover{border-color:var(--border2);transform:translateY(-2px)}
273
+ .mc.sel{border-color:var(--acc);box-shadow:0 0 0 1px var(--acc)}
274
+ .mc.sel::before{opacity:.06}
275
+ .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}
276
+ .mc.sel .mc-check{display:flex}
277
+ .mc-prov{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;opacity:.7}
278
+ .mc-name{font-size:14px;font-weight:700;margin-bottom:4px;position:relative}
279
+ .mc-desc{font-size:11px;color:var(--txt2);position:relative;line-height:1.5}
280
+ .prov-anthropic .mc-prov{color:var(--orange)}
281
+ .prov-openai .mc-prov{color:var(--green)}
282
+ .prov-google .mc-prov{color:#4da6ff}
283
+ .prov-mistral .mc-prov{color:var(--acc2)}
284
+ .prov-local .mc-prov{color:var(--txt3)}
285
+ .prov-filter-bar{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:16px}
286
+ .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}
287
+ .pf:hover{color:var(--txt2);border-color:var(--border2)}
288
+ .pf.on{background:var(--acc);border-color:var(--acc);color:#fff}
289
+
290
+ /* ── GATEWAY CARDS ── */
291
+ .gw-card{
292
+ background:var(--surface);border:1.5px solid var(--border);
293
+ border-radius:var(--rad);padding:16px;margin-bottom:10px;
294
+ display:flex;align-items:center;gap:14px;transition:border-color .15s
295
+ }
296
+ .gw-card.active-gw{border-color:var(--acc)}
297
+ .gw-left{flex:1;min-width:0}
298
+ .gw-name{font-size:14px;font-weight:700;margin-bottom:4px;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
299
+ .gw-url{font-size:11px;color:var(--txt3);font-family:'JetBrains Mono',monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
300
+ .badge{display:inline-flex;align-items:center;font-size:10px;font-weight:700;padding:2px 8px;border-radius:10px}
301
+ .badge-green{background:rgba(34,201,138,.15);color:var(--green)}
302
+ .badge-red{background:rgba(240,90,90,.15);color:var(--red)}
303
+ .badge-acc{background:rgba(124,106,247,.15);color:var(--acc2)}
304
+ .badge-gray{background:var(--surface3);color:var(--txt3)}
305
+ .gw-acts{display:flex;gap:6px;flex-shrink:0}
306
+
307
+ /* ── TOAST ── */
308
+ #toast{
309
+ position:fixed;bottom:24px;right:24px;
310
+ background:var(--surface2);border:1px solid var(--border2);
311
+ color:var(--txt);padding:12px 20px;border-radius:var(--rad-sm);
312
+ font-size:13px;z-index:999;opacity:0;
313
+ transform:translateY(10px);transition:all .25s;pointer-events:none;
314
+ box-shadow:0 8px 32px rgba(0,0,0,.4);font-weight:600
315
+ }
316
+ #toast.show{opacity:1;transform:none}
317
+
318
+ /* ── MODAL ── */
319
+ #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)}
320
+ #modal-bg.open{display:flex}
321
+ #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)}
322
+ #modal h2{font-size:18px;font-weight:800;margin-bottom:20px;letter-spacing:-.3px}
323
+ .modal-acts{display:flex;justify-content:flex-end;gap:10px;margin-top:20px}
324
+
325
+ /* ── KEY STATUS ── */
326
+ .key-row{display:flex;align-items:center;gap:10px}
327
+ .key-row .field{flex:1;margin:0}
328
+ .key-status{width:8px;height:8px;border-radius:50%;background:var(--border2);flex-shrink:0}
329
+ .key-status.ok{background:var(--green);box-shadow:0 0 6px var(--green)}
330
+
331
+ /* ── RESPONSIVE ── */
332
+ @media(max-width:700px){
333
+ #sidebar{position:fixed;left:0;top:0;bottom:0;transform:translateX(-100%);box-shadow:4px 0 24px rgba(0,0,0,.5)}
334
+ #sidebar.open{transform:none}
335
+ #menu-btn{display:flex}
336
+ .model-grid{grid-template-columns:1fr 1fr}
337
+ }
338
+ </style>
339
339
  </head>
340
340
  <body>
341
341
 
342
- <!-- TOAST CONTAINER -->
343
- <div id="toast-container"></div>
344
-
345
342
  <!-- SIDEBAR -->
346
343
  <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>
344
+ <div id="sb-logo">
345
+ <div class="logo-icon">🦞</div>
346
+ <div>
347
+ <h1>PS Claw</h1>
348
+ <span>AI AGENT v1.1</span>
378
349
  </div>
379
- <div id="chat-list"></div>
380
350
  </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>
351
+ <button id="new-btn" onclick="newChat()">
352
+ <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>
353
+ Novo Chat
354
+ </button>
355
+ <div id="chat-list"></div>
356
+ <div id="sb-footer">
357
+ <div id="st-badge">
358
+ <div id="st-dot" class="warn"></div>
359
+ <span id="st-txt">Configure uma API key</span>
412
360
  </div>
413
361
  </div>
414
362
  </div>
@@ -416,764 +364,679 @@
416
364
  <!-- MAIN -->
417
365
  <div id="main">
418
366
  <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>
367
+ <div id="tb-left">
368
+ <button id="menu-btn" onclick="toggleSb()">☰</button>
369
+ <span id="tb-title">Chat</span>
426
370
  </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>
371
+ <div id="tb-right">
372
+ <div id="model-pill" onclick="goTab('models')">
373
+ <span class="dot"></span>
374
+ <span id="model-name-badge">Selecionar modelo</span>
439
375
  </div>
440
376
  </div>
441
377
  </div>
442
378
 
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>
379
+ <div id="tabs">
380
+ <div class="tab active" data-t="chat" onclick="goTab('chat')">πŸ’¬ Chat</div>
381
+ <div class="tab" data-t="keys" onclick="goTab('keys')">πŸ”‘ API Keys</div>
382
+ <div class="tab" data-t="models" onclick="goTab('models')">πŸ€– Modelos</div>
383
+ <div class="tab" data-t="gateways" onclick="goTab('gateways')">πŸ”Œ Gateways</div>
384
+ <div class="tab" data-t="settings" onclick="goTab('settings')">βš™οΈ Config</div>
454
385
  </div>
455
- </div>
456
386
 
457
- <!-- MODAL -->
458
- <div id="modal-overlay" onclick="closeModal(event)">
459
- <div id="modal-box">
460
- <div id="modal-content"></div>
387
+ <!-- CHAT -->
388
+ <div class="page active" id="chat-page">
389
+ <div id="messages">
390
+ <div id="welcome">
391
+ <div class="w-icon">🦞</div>
392
+ <h2>Bem-vindo ao PS Claw</h2>
393
+ <p>Configure uma chave de API na aba <strong>πŸ”‘ API Keys</strong> e selecione um modelo em <strong>πŸ€– Modelos</strong> para comeΓ§ar.</p>
394
+ <div class="chips">
395
+ <div class="chip" onclick="sendQ('O que vocΓͺ consegue fazer?')">O que vocΓͺ faz?</div>
396
+ <div class="chip" onclick="sendQ('Me ajude a escrever um script Python para automaΓ§Γ£o')">Escrever cΓ³digo</div>
397
+ <div class="chip" onclick="sendQ('Quais sΓ£o as principais tendΓͺncias de IA em 2025?')">TendΓͺncias de IA</div>
398
+ <div class="chip" onclick="sendQ('Explique o que Γ© o PS Claw e como funciona')">Sobre o PS Claw</div>
399
+ </div>
400
+ </div>
401
+ </div>
402
+ <div id="input-wrap">
403
+ <div id="input-box">
404
+ <textarea id="msg-in" placeholder="Mensagem..." rows="1"
405
+ onkeydown="handleKey(event)" oninput="autoResize(this)"></textarea>
406
+ <button id="send-btn" onclick="sendMsg()">
407
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
408
+ <line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
409
+ </svg>
410
+ </button>
411
+ </div>
412
+ <div id="input-hint">Enter para enviar Β· Shift+Enter para nova linha</div>
413
+ </div>
461
414
  </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
415
 
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
- }
416
+ <!-- API KEYS -->
417
+ <div class="page" id="keys-page">
418
+ <div class="page-inner">
419
+ <div class="sp-header">
420
+ <div class="sp-title">πŸ”‘ API Keys</div>
421
+ <div class="sp-sub">Configure suas chaves de API. SΓ£o salvas localmente no navegador e nunca enviadas a terceiros.</div>
422
+ </div>
587
423
 
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)}
424
+ <div class="card">
425
+ <h3>🟠 Anthropic β€” Claude</h3>
426
+ <div class="field">
427
+ <label>API Key</label>
428
+ <div class="key-row">
429
+ <div class="field">
430
+ <input type="password" id="k-anthropic" placeholder="sk-ant-api03-..." oninput="saveKeys()"/>
431
+ </div>
432
+ <div class="key-status" id="ks-anthropic"></div>
597
433
  </div>
598
- <span style="font-size:11px;color:var(--text-muted);">${gw.type}</span>
599
434
  </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>` : ''}
435
+ <div style="font-size:12px;color:var(--txt3);margin-top:8px">
436
+ 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
437
  </div>
606
438
  </div>
607
- </div>
608
- `).join('');
609
- }
610
439
 
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>
440
+ <div class="card">
441
+ <h3>🟒 OpenAI β€” GPT-4</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-openai" placeholder="sk-proj-..." oninput="saveKeys()"/>
447
+ </div>
448
+ <div class="key-status" id="ks-openai"></div>
646
449
  </div>
647
- `).join('')}
450
+ </div>
451
+ <div style="font-size:12px;color:var(--txt3);margin-top:8px">
452
+ 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
453
+ </div>
648
454
  </div>
649
- </div>
650
- `).join('');
651
- }
652
455
 
653
- function toggleProvider(id) {
654
- const list = document.getElementById('models-' + id);
655
- const toggle = document.getElementById('toggle-' + id);
656
- list.classList.toggle('open');
657
- toggle.classList.toggle('open');
658
- }
659
-
660
- function selectModel(id, name) {
661
- cfg.selectedModel = id;
662
- saveCfg();
663
- renderModelList();
664
- updateModelIndicator();
665
- showToast('Modelo selecionado: ' + name, 'success');
666
- }
456
+ <div class="card">
457
+ <h3>πŸ”΅ Google β€” Gemini</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-google" placeholder="AIzaSy..." oninput="saveKeys()"/>
463
+ </div>
464
+ <div class="key-status" id="ks-google"></div>
465
+ </div>
466
+ </div>
467
+ <div style="font-size:12px;color:var(--txt3);margin-top:8px">
468
+ 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>
469
+ </div>
470
+ </div>
667
471
 
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
- }
472
+ <div class="card">
473
+ <h3>🟣 Mistral AI</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-mistral" placeholder="..." oninput="saveKeys()"/>
479
+ </div>
480
+ <div class="key-status" id="ks-mistral"></div>
481
+ </div>
482
+ </div>
483
+ <div style="font-size:12px;color:var(--txt3);margin-top:8px">
484
+ Obtenha em: <a href="https://console.mistral.ai/api-keys" target="_blank" style="color:var(--acc2)">console.mistral.ai</a>
485
+ </div>
486
+ </div>
681
487
 
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)" />
488
+ <div style="display:flex;gap:10px;margin-top:8px">
489
+ <button class="btn btn-primary" onclick="testAllKeys()">πŸ” Testar todas as chaves</button>
490
+ <button class="btn btn-ghost" onclick="goTab('models')">β†’ Selecionar modelo</button>
691
491
  </div>
692
492
  </div>
493
+ </div>
693
494
 
694
- <div class="settings-group">
695
- <div class="settings-group-title">Modelo e Comportamento</div>
696
- <div class="settings-field">
697
- <label>Temperatura (${cfg.temperature})</label>
698
- <input type="range" min="0" max="2" step="0.1" value="${cfg.temperature}" oninput="updateSetting('temperature', parseFloat(this.value)); this.previousElementSibling.textContent='Temperatura ('+this.value+')'" />
699
- <div class="hint">Valores mais baixos = respostas mais deterministas. Valores mais altos = mais criatividade.</div>
700
- </div>
701
- <div class="settings-field">
702
- <label>Max Tokens</label>
703
- <input type="number" id="s-maxtokens" value="${cfg.maxTokens}" min="100" max="128000" step="100" onchange="updateSetting('maxTokens', parseInt(this.value))" />
704
- <div class="hint">Limite maximo de tokens na resposta.</div>
495
+ <!-- MODELS -->
496
+ <div class="page" id="models-page">
497
+ <div class="page-inner">
498
+ <div class="sp-header">
499
+ <div class="sp-title">πŸ€– Modelos</div>
500
+ <div class="sp-sub">Selecione o modelo a ser usado no chat. Certifique-se de ter a API Key correspondente configurada.</div>
705
501
  </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>
502
+ <div class="prov-filter-bar">
503
+ <button class="pf on" data-p="all" onclick="filterM('all',this)">Todos</button>
504
+ <button class="pf" data-p="anthropic" onclick="filterM('anthropic',this)">🟠 Anthropic</button>
505
+ <button class="pf" data-p="openai" onclick="filterM('openai',this)">🟒 OpenAI</button>
506
+ <button class="pf" data-p="google" onclick="filterM('google',this)">πŸ”΅ Google</button>
507
+ <button class="pf" data-p="mistral" onclick="filterM('mistral',this)">🟣 Mistral</button>
508
+ <button class="pf" data-p="local" onclick="filterM('local',this)">⚫ Local</button>
710
509
  </div>
510
+ <div id="model-grid" class="model-grid"></div>
711
511
  </div>
512
+ </div>
712
513
 
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>
514
+ <!-- GATEWAYS -->
515
+ <div class="page" id="gateways-page">
516
+ <div class="page-inner">
517
+ <div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:20px;flex-wrap:wrap;gap:12px">
518
+ <div class="sp-header" style="margin:0">
519
+ <div class="sp-title">πŸ”Œ Gateways</div>
520
+ <div class="sp-sub">Opcional. Use se tiver um servidor PS Claw/OpenClaw rodando.</div>
521
+ </div>
522
+ <button class="btn btn-primary btn-sm" onclick="openAddGw()">+ Adicionar</button>
718
523
  </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>
524
+ <div class="card" style="margin-bottom:16px">
525
+ <h3>πŸ’‘ O que Γ© um Gateway?</h3>
526
+ <p style="font-size:13px;color:var(--txt2);line-height:1.75">
527
+ 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>
528
+ 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>),
529
+ adicione aqui para rotear as mensagens atravΓ©s dele.
530
+ </p>
722
531
  </div>
532
+ <div id="gw-list"></div>
723
533
  </div>
534
+ </div>
724
535
 
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)" />
536
+ <!-- SETTINGS -->
537
+ <div class="page" id="settings-page">
538
+ <div class="page-inner">
539
+ <div class="sp-header">
540
+ <div class="sp-title">βš™οΈ ConfiguraΓ§Γ΅es</div>
730
541
  </div>
731
- <div class="settings-field">
732
- <label>Anthropic API Key</label>
733
- <input type="password" id="s-key-anthropic" value="${escAttr(cfg.apiKeys?.anthropic || '')}" placeholder="sk-ant-..." onchange="updateApiKey('anthropic', this.value)" />
734
- </div>
735
- <div class="settings-field">
736
- <label>Google / Gemini API Key</label>
737
- <input type="password" id="s-key-google" value="${escAttr(cfg.apiKeys?.google || '')}" placeholder="AI..." onchange="updateApiKey('google', this.value)" />
542
+ <div class="card">
543
+ <h3>πŸ‘€ Perfil</h3>
544
+ <div class="row">
545
+ <div class="field">
546
+ <label>Seu nome</label>
547
+ <input type="text" id="cfg-name" placeholder="VocΓͺ" oninput="saveCfg()"/>
548
+ </div>
549
+ <div class="field">
550
+ <label>Nome do agente</label>
551
+ <input type="text" id="cfg-agent" placeholder="PS Claw" oninput="saveCfg()"/>
552
+ </div>
553
+ </div>
738
554
  </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)" />
555
+ <div class="card">
556
+ <h3>πŸ’¬ Comportamento</h3>
557
+ <div class="field">
558
+ <label>System prompt</label>
559
+ <textarea id="cfg-sys" placeholder="VocΓͺ Γ© um assistente de IA ΓΊtil e direto." oninput="saveCfg()"></textarea>
560
+ </div>
561
+ <div class="row">
562
+ <div class="field">
563
+ <label>Temperatura (0–2)</label>
564
+ <input type="number" id="cfg-temp" min="0" max="2" step="0.1" placeholder="0.7" oninput="saveCfg()"/>
565
+ </div>
566
+ <div class="field">
567
+ <label>Max tokens</label>
568
+ <input type="number" id="cfg-tok" min="256" max="128000" step="256" placeholder="4096" oninput="saveCfg()"/>
569
+ </div>
570
+ </div>
742
571
  </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)" />
572
+ <div class="card">
573
+ <h3>πŸ—‘οΈ Dados</h3>
574
+ <p style="font-size:13px;color:var(--txt2);margin-bottom:14px">Todos os dados sΓ£o salvos localmente no seu navegador.</p>
575
+ <div style="display:flex;gap:10px;flex-wrap:wrap">
576
+ <button class="btn btn-danger btn-sm" onclick="clearChats()">Apagar histΓ³rico</button>
577
+ <button class="btn btn-danger btn-sm" onclick="clearAll()">Resetar tudo</button>
578
+ </div>
746
579
  </div>
747
580
  </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>
581
+ </div>
582
+ </div><!-- /main -->
583
+
584
+ <!-- MODAL ADD GATEWAY -->
585
+ <div id="modal-bg" onclick="closeMod(event)">
586
+ <div id="modal">
587
+ <h2>πŸ”Œ Adicionar Gateway</h2>
588
+ <div class="field"><label>Nome</label><input type="text" id="gw-n" placeholder="PS Claw Local"/></div>
589
+ <div class="field"><label>URL</label><input type="text" id="gw-u" placeholder="http://localhost:18789"/></div>
590
+ <div class="field"><label>Token (opcional)</label><input type="password" id="gw-t" placeholder="Bearer token"/></div>
591
+ <div class="modal-acts">
592
+ <button class="btn btn-ghost" onclick="closeMod()">Cancelar</button>
593
+ <button class="btn btn-primary" onclick="addGw()">Adicionar</button>
755
594
  </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
- }
595
+ </div>
596
+ </div>
769
597
 
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
- }
598
+ <div id="toast"></div>
776
599
 
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
- }
600
+ <script>
601
+ // ─── MODELOS ──────────────────────────────────────────────────────────────
602
+ const MODELS = [
603
+ // Anthropic
604
+ {id:'claude-opus-4-5', name:'Claude Opus 4.5', p:'anthropic', desc:'Mais poderoso Β· raciocΓ­nio complexo',
605
+ api:'https://api.anthropic.com/v1/messages'},
606
+ {id:'claude-sonnet-4-5', name:'Claude Sonnet 4.5', p:'anthropic', desc:'RΓ‘pido e inteligente Β· uso geral',
607
+ api:'https://api.anthropic.com/v1/messages'},
608
+ {id:'claude-haiku-4-5', name:'Claude Haiku 4.5', p:'anthropic', desc:'Ultra rΓ‘pido Β· econΓ΄mico',
609
+ api:'https://api.anthropic.com/v1/messages'},
610
+ // OpenAI
611
+ {id:'gpt-4o', name:'GPT-4o', p:'openai', desc:'Flagship multimodal da OpenAI',
612
+ api:'https://api.openai.com/v1/chat/completions'},
613
+ {id:'gpt-4o-mini', name:'GPT-4o mini', p:'openai', desc:'RΓ‘pido e barato Β· uso geral',
614
+ api:'https://api.openai.com/v1/chat/completions'},
615
+ {id:'gpt-4-turbo', name:'GPT-4 Turbo', p:'openai', desc:'Alta capacidade Β· longo contexto',
616
+ api:'https://api.openai.com/v1/chat/completions'},
617
+ {id:'o3-mini', name:'o3-mini', p:'openai', desc:'RaciocΓ­nio avanΓ§ado Β· lΓ³gica',
618
+ api:'https://api.openai.com/v1/chat/completions'},
619
+ // Google
620
+ {id:'gemini-2.5-pro', name:'Gemini 2.5 Pro', p:'google', desc:'Mais poderoso do Google',
621
+ api:'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent'},
622
+ {id:'gemini-2.5-flash', name:'Gemini 2.5 Flash', p:'google', desc:'RΓ‘pido Β· contexto longo Β· gratuito',
623
+ api:'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent'},
624
+ {id:'gemini-1.5-flash', name:'Gemini 1.5 Flash', p:'google', desc:'EstΓ‘vel e confiΓ‘vel Β· gratuito',
625
+ api:'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent'},
626
+ // Mistral
627
+ {id:'mistral-large-latest', name:'Mistral Large', p:'mistral', desc:'Poderoso Β· multilingual',
628
+ api:'https://api.mistral.ai/v1/chat/completions'},
629
+ {id:'mistral-small-latest', name:'Mistral Small', p:'mistral', desc:'RΓ‘pido Β· barato Β· europeu',
630
+ api:'https://api.mistral.ai/v1/chat/completions'},
631
+ // Local
632
+ {id:'llama3.2', name:'Llama 3.2 (Ollama)', p:'local', desc:'Roda localmente Β· configure URL do gateway',
633
+ api:''},
634
+ ];
786
635
 
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
- }
636
+ // ─── ESTADO ───────────────────────────────────────────────────────────────
637
+ let S = JSON.parse(localStorage.getItem('psc2') || '{}');
638
+ S.chats = S.chats || [];
639
+ S.model = S.model || null;
640
+ S.gws = S.gws || [];
641
+ S.activeGw = S.activeGw || null;
642
+ S.keys = S.keys || {anthropic:'',openai:'',google:'',mistral:''};
643
+ S.cfg = S.cfg || {name:'VocΓͺ',agent:'PS Claw',sys:'',temp:'0.7',tok:'4096'};
807
644
 
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();
645
+ let curChat = null;
646
+ let isTyping = false;
647
+ let mFilter = 'all';
648
+
649
+ function save(){ localStorage.setItem('psc2', JSON.stringify(S)) }
650
+
651
+ // ─── TABS ─────────────────────────────────────────────────────────────────
652
+ function goTab(t){
653
+ document.querySelectorAll('.tab').forEach(e=>e.classList.toggle('active',e.dataset.t===t));
654
+ document.querySelectorAll('.page').forEach(e=>e.classList.remove('active'));
655
+ document.getElementById(t+'-page').classList.add('active');
656
+ document.getElementById('tb-title').textContent={chat:'Chat',keys:'API Keys',models:'Modelos',gateways:'Gateways',settings:'ConfiguraΓ§Γ΅es'}[t]||t;
657
+ if(t==='models') renderModels();
658
+ if(t==='gateways') renderGws();
659
+ if(t==='keys') loadKeys();
660
+ if(t==='settings') loadCfg();
815
661
  }
816
662
 
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');
663
+ // ─── CHAT ─────────────────────────────────────────────────────────────────
664
+ function newChat(){
665
+ const id='c'+Date.now();
666
+ S.chats.unshift({id,title:'Nova conversa',msgs:[],ts:Date.now()});
667
+ save(); selectChat(id); goTab('chat');
825
668
  }
826
-
827
- function selectChat(id) {
828
- currentSession = sessions.find(s => s.id === id);
829
- if (!currentSession) return;
830
- renderChatList();
831
- renderMessages();
669
+ function selectChat(id){
670
+ curChat=S.chats.find(c=>c.id===id);
671
+ renderChatList(); renderMsgs();
832
672
  }
833
-
834
- function deleteChat(id, e) {
673
+ function deleteChat(id,e){
835
674
  e.stopPropagation();
836
- sessions = sessions.filter(s => s.id !== id);
837
- saveSessions();
838
- if (currentSession?.id === id) { currentSession = null; renderMessages(); }
839
- renderChatList();
840
- }
841
-
842
- function saveSessions() {
843
- localStorage.setItem('ps-claw-sessions', JSON.stringify(sessions));
675
+ S.chats=S.chats.filter(c=>c.id!==id);
676
+ if(curChat?.id===id){curChat=null;renderMsgs();}
677
+ save(); renderChatList();
844
678
  }
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('');
679
+ function renderChatList(){
680
+ document.getElementById('chat-list').innerHTML=S.chats.map(c=>`
681
+ <div class="ci ${c.id===curChat?.id?'active':''}" onclick="selectChat('${c.id}')">
682
+ <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1">${h(c.title)}</span>
683
+ <button class="ci-del" onclick="deleteChat('${c.id}',event)">βœ•</button>
684
+ </div>`).join('');
855
685
  }
856
-
857
- function renderMessages() {
858
- const box = document.getElementById('messages');
859
- if (!currentSession || currentSession.messages.length === 0) {
860
- box.innerHTML = `<div id="welcome">
861
- <div style="font-size:56px;">🦞</div>
862
- <h2>PS Claw</h2>
863
- <p>Agente de IA autonomo. Configure sua chave de API e comece a conversar.</p>
864
- <div class="welcome-chips">
865
- <div class="chip" onclick="sendQuick('Ola! O que voce consegue fazer?')">O que voce consegue fazer?</div>
866
- <div class="chip" onclick="sendQuick('Me ajude a escrever um script Python')">Escrever codigo</div>
867
- <div class="chip" onclick="sendQuick('Pesquise na web sobre IA em 2025')">Pesquisar na web</div>
868
- <div class="chip" onclick="sendQuick('Explique como funciona o PS Claw')">Sobre o PS Claw</div>
686
+ function renderMsgs(){
687
+ const box=document.getElementById('messages');
688
+ if(!curChat||!curChat.msgs.length){
689
+ box.innerHTML=`<div id="welcome">
690
+ <div class="w-icon">🦞</div>
691
+ <h2>${h(S.cfg.agent||'PS Claw')}</h2>
692
+ <p>Configure uma chave de API na aba <strong>πŸ”‘ API Keys</strong> e selecione um modelo em <strong>πŸ€– Modelos</strong> para comeΓ§ar.</p>
693
+ <div class="chips">
694
+ <div class="chip" onclick="sendQ('O que vocΓͺ consegue fazer?')">O que vocΓͺ faz?</div>
695
+ <div class="chip" onclick="sendQ('Me ajude com um script Python')">Escrever cΓ³digo</div>
696
+ <div class="chip" onclick="sendQ('TendΓͺncias de IA em 2025')">TendΓͺncias de IA</div>
697
+ <div class="chip" onclick="sendQ('Explique o PS Claw')">Sobre o PS Claw</div>
869
698
  </div>
870
699
  </div>`;
871
700
  return;
872
701
  }
873
- box.innerHTML = currentSession.messages.map(m => renderMsg(m)).join('');
874
- box.scrollTop = box.scrollHeight;
875
- }
876
-
877
- function renderMsg(m) {
878
- const isUser = m.role === 'user';
879
- return `
880
- <div class="msg-row ${isUser ? 'user' : 'ai'}">
881
- <div class="avatar ${isUser ? 'user' : 'ai'}">${isUser ? (cfg.displayName[0]||'V').toUpperCase() : '🦞'}</div>
882
- <div class="bubble ${isUser ? 'user' : 'ai'}">${formatText(m.content)}</div>
883
- </div>`;
884
- }
885
-
886
- function addTypingIndicator() {
887
- const box = document.getElementById('messages');
888
- const el = document.createElement('div');
889
- el.id = 'typing-row';
890
- el.className = 'msg-row ai';
891
- el.innerHTML = `<div class="avatar ai">🦞</div><div class="bubble ai"><div class="typing-dots"><span>β€’</span><span>β€’</span><span>β€’</span></div></div>`;
892
- box.appendChild(el);
893
- box.scrollTop = box.scrollHeight;
702
+ box.innerHTML=curChat.msgs.map(m=>`
703
+ <div class="msg-row ${m.role==='user'?'user':''}">
704
+ <div class="av ${m.role==='user'?'user':'ai'}">${m.role==='user'?(S.cfg.name||'V')[0].toUpperCase():'🦞'}</div>
705
+ <div class="bbl ${m.role==='user'?'user':'ai'}">${fmt(m.content)}</div>
706
+ </div>`).join('');
707
+ box.scrollTop=box.scrollHeight;
894
708
  }
895
- function removeTypingIndicator() {
896
- document.getElementById('typing-row')?.remove();
709
+ function addTyping(){
710
+ const box=document.getElementById('messages');
711
+ document.getElementById('welcome')?.remove();
712
+ const el=document.createElement('div');
713
+ el.id='typdiv';el.className='msg-row';
714
+ el.innerHTML=`<div class="av ai">🦞</div><div class="bbl ai"><div class="typing"><span></span><span></span><span></span></div></div>`;
715
+ box.appendChild(el);box.scrollTop=box.scrollHeight;
897
716
  }
898
-
899
- // === SEND MESSAGE ===
900
- async function sendMessage() {
901
- const input = document.getElementById('msg-input');
902
- const text = input.value.trim();
903
- if (!text || isTyping) return;
904
- if (!currentSession) newChat();
905
-
906
- input.value = '';
907
- input.style.height = 'auto';
908
- document.getElementById('send-btn').disabled = true;
909
- isTyping = true;
910
-
717
+ function rmTyping(){document.getElementById('typdiv')?.remove()}
718
+
719
+ // ─── ENVIO ────────────────────────────────────────────────────────────────
720
+ async function sendMsg(){
721
+ const inp=document.getElementById('msg-in');
722
+ const text=inp.value.trim();
723
+ if(!text||isTyping)return;
724
+ if(!curChat)newChat();
725
+
726
+ // Verificar se tem modelo e chave
727
+ const model=MODELS.find(m=>m.id===S.model);
728
+ if(!model){toast('⚠️ Selecione um modelo na aba πŸ€– Modelos');goTab('models');return;}
729
+ const key=S.keys[model.p];
730
+ if(!key&&model.p!=='local'){toast('⚠️ Configure a API Key na aba πŸ”‘ API Keys');goTab('keys');return;}
731
+
732
+ inp.value='';inp.style.height='auto';
733
+ document.getElementById('send-btn').disabled=true;
734
+ isTyping=true;
911
735
  document.getElementById('welcome')?.remove();
912
736
 
913
- currentSession.messages.push({ role: 'user', content: text });
914
- if (currentSession.messages.length === 1) {
915
- currentSession.title = text.slice(0, 40) + (text.length > 40 ? '...' : '');
916
- renderChatList();
917
- }
918
- saveSessions();
919
- renderMessages();
920
- addTypingIndicator();
921
-
922
- try {
923
- const gw = getActiveGateway();
924
- const body = {
925
- text,
926
- ...(cfg.selectedModel && { model: cfg.selectedModel }),
927
- temperature: cfg.temperature,
928
- max_tokens: cfg.maxTokens,
929
- ...(cfg.systemPrompt && { system: cfg.systemPrompt }),
930
- history: currentSession.messages.slice(0, -1).map(m => ({ role: m.role, content: m.content }))
931
- };
932
-
933
- const resp = await fetch(gw.url + '/chat', {
934
- method: 'POST',
935
- headers: {
936
- 'Content-Type': 'application/json',
937
- ...(gw.token && { Authorization: 'Bearer ' + gw.token })
938
- },
939
- body: JSON.stringify(body),
940
- signal: AbortSignal.timeout(120000)
941
- });
942
-
943
- removeTypingIndicator();
944
-
945
- let replyText;
946
- if (resp.ok) {
947
- const data = await resp.json();
948
- replyText = data.reply || data.text || data.content || data.message || JSON.stringify(data, null, 2);
949
- } else {
950
- replyText = await tryFallbackChat(text);
951
- }
737
+ curChat.msgs.push({role:'user',content:text});
738
+ if(curChat.msgs.length===1)curChat.title=text.slice(0,44)+(text.length>44?'…':'');
739
+ save();renderChatList();renderMsgs();addTyping();
952
740
 
953
- currentSession.messages.push({ role: 'assistant', content: replyText });
954
- saveSessions();
955
- renderMessages();
741
+ let reply;
742
+ try{
743
+ reply = await callApi(model, text, key);
744
+ }catch(e){
745
+ reply=`❌ **Erro:** ${e.message}`;
746
+ }
747
+ rmTyping();
748
+ curChat.msgs.push({role:'assistant',content:reply});
749
+ save();renderMsgs();
750
+ isTyping=false;
751
+ document.getElementById('send-btn').disabled=false;
752
+ inp.focus();
753
+ }
956
754
 
957
- } catch (err) {
958
- removeTypingIndicator();
959
- const fallback = await tryFallbackChat(text);
960
- currentSession.messages.push({ role: 'assistant', content: fallback });
961
- saveSessions();
962
- renderMessages();
755
+ // ─── CHAMADAS DE API ──────────────────────────────────────────────────────
756
+ async function callApi(model, text, key){
757
+ const history = curChat.msgs.slice(0,-1).map(m=>({role:m.role,content:m.content}));
758
+ const messages = [...history, {role:'user',content:text}];
759
+ const sys = S.cfg.sys || 'VocΓͺ Γ© um assistente de IA ΓΊtil e direto.';
760
+ const temp = parseFloat(S.cfg.temp)||0.7;
761
+ const maxTok = parseInt(S.cfg.tok)||4096;
762
+
763
+ // Verificar se tem gateway ativo (prioridade sobre chamada direta)
764
+ const gw = S.gws.find(g=>g.id===S.activeGw);
765
+ if(gw){
766
+ return await callGateway(gw, model, messages, sys, temp, maxTok);
963
767
  }
964
768
 
965
- isTyping = false;
966
- document.getElementById('send-btn').disabled = false;
967
- document.getElementById('msg-input').focus();
769
+ // Chamada direta por provedor
770
+ if(model.p==='anthropic') return await callAnthropic(model, messages, sys, key, maxTok, temp);
771
+ if(model.p==='openai') return await callOpenAI(model, messages, sys, key, maxTok, temp);
772
+ if(model.p==='google') return await callGoogle(model, messages, sys, key, maxTok, temp);
773
+ if(model.p==='mistral') return await callOpenAI(model, messages, sys, key, maxTok, temp, 'https://api.mistral.ai/v1/chat/completions');
774
+ throw new Error('Provedor desconhecido. Configure um gateway ou selecione outro modelo.');
968
775
  }
969
776
 
970
- async function tryFallbackChat(text) {
971
- const gw = getActiveGateway();
972
- const endpoints = ['/v1/chat', '/api/chat', '/message'];
973
- for (const ep of endpoints) {
974
- try {
975
- const r = await fetch(gw.url + ep, {
976
- method: 'POST',
977
- headers: { 'Content-Type': 'application/json', ...(gw.token && { Authorization: 'Bearer ' + gw.token }) },
978
- body: JSON.stringify({
979
- message: text,
980
- model: cfg.selectedModel || undefined,
981
- temperature: cfg.temperature,
982
- max_tokens: cfg.maxTokens,
983
- messages: [{ role: 'user', content: text }]
984
- }),
985
- signal: AbortSignal.timeout(10000)
986
- });
987
- if (r.ok) {
988
- const d = await r.json();
989
- return d.reply || d.text || d.content || d.message || JSON.stringify(d, null, 2);
990
- }
991
- } catch {}
992
- }
993
- const gwName = gw ? gw.name : 'Desconhecido';
994
- const gwUrl = gw ? gw.url : 'N/A';
995
- return `**Gateway offline ou configuracao necessaria**\n\nO PS Claw nao esta respondendo. Verifique:\n\n1. Execute o PS Claw: \`ps-claw gateway run\`\n2. Va na aba **Gateways** e configure a URL\n3. Adicione seu token se necessario\n4. Selecione um modelo na aba **Modelos**\n\nGateway: ${gwName} (${gwUrl})`;
777
+ // Proxy URL para evitar CORS (usa o servidor local)
778
+ function px(url){ return `/proxy?url=${encodeURIComponent(url)}`; }
779
+
780
+ async function callAnthropic(model, messages, sys, key, maxTok, temp){
781
+ // Separar mensagens de sistema
782
+ const msgs = messages.filter(m=>m.role!=='system').map(m=>({role:m.role,content:m.content}));
783
+ const r = await fetch(px('https://api.anthropic.com/v1/messages'),{
784
+ method:'POST',
785
+ headers:{'Content-Type':'application/json','x-api-key':key,'anthropic-version':'2023-06-01'},
786
+ body:JSON.stringify({model:model.id,max_tokens:maxTok,temperature:temp,system:sys,messages:msgs})
787
+ });
788
+ const d=await r.json();
789
+ if(!r.ok)throw new Error(d.error?.message||JSON.stringify(d));
790
+ return d.content?.[0]?.text||JSON.stringify(d);
996
791
  }
997
792
 
998
- function sendQuick(text) {
999
- document.getElementById('msg-input').value = text;
1000
- sendMessage();
793
+ async function callOpenAI(model, messages, sys, key, maxTok, temp, overrideUrl){
794
+ const url = overrideUrl || 'https://api.openai.com/v1/chat/completions';
795
+ const msgs = [{role:'system',content:sys},...messages];
796
+ const r = await fetch(px(url),{
797
+ method:'POST',
798
+ headers:{'Content-Type':'application/json','Authorization':'Bearer '+key},
799
+ body:JSON.stringify({model:model.id,max_tokens:maxTok,temperature:temp,messages:msgs})
800
+ });
801
+ const d=await r.json();
802
+ if(!r.ok)throw new Error(d.error?.message||JSON.stringify(d));
803
+ return d.choices?.[0]?.message?.content||JSON.stringify(d);
1001
804
  }
1002
805
 
1003
- // === MODALS ===
1004
- function openModal(type, data) {
1005
- const content = document.getElementById('modal-content');
806
+ async function callGoogle(model, messages, sys, key, maxTok, temp){
807
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${model.id}:generateContent?key=${key}`;
808
+ const contents = messages.map(m=>({
809
+ role: m.role==='assistant'?'model':'user',
810
+ parts:[{text:m.content}]
811
+ }));
812
+ const r = await fetch(px(url),{
813
+ method:'POST',
814
+ headers:{'Content-Type':'application/json'},
815
+ body:JSON.stringify({
816
+ contents,
817
+ systemInstruction:{parts:[{text:sys}]},
818
+ generationConfig:{maxOutputTokens:maxTok,temperature:temp}
819
+ })
820
+ });
821
+ const d=await r.json();
822
+ if(!r.ok)throw new Error(d.error?.message||JSON.stringify(d));
823
+ return d.candidates?.[0]?.content?.parts?.[0]?.text||JSON.stringify(d);
824
+ }
1006
825
 
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
- `;
826
+ async function callGateway(gw, model, messages, sys, temp, maxTok){
827
+ const endpoints=['/v1/chat/completions','/api/chat','/chat'];
828
+ const headers={'Content-Type':'application/json',...(gw.token&&{Authorization:'Bearer '+gw.token})};
829
+ const body={model:model.id,messages:[{role:'system',content:sys},...messages],temperature:temp,max_tokens:maxTok};
830
+ for(const ep of endpoints){
831
+ try{
832
+ const r=await fetch(px(gw.url+ep),{method:'POST',headers,body:JSON.stringify(body),signal:AbortSignal.timeout(60000)});
833
+ if(!r.ok)continue;
834
+ const d=await r.json();
835
+ return d.choices?.[0]?.message?.content||d.reply||d.text||d.content||JSON.stringify(d);
836
+ }catch{}
1092
837
  }
1093
-
1094
- document.getElementById('modal-overlay').classList.add('open');
838
+ throw new Error('Gateway nΓ£o respondeu. Verifique a URL.');
1095
839
  }
1096
840
 
1097
- function closeModal(e) {
1098
- if (e && e.target !== document.getElementById('modal-overlay')) return;
1099
- document.getElementById('modal-overlay').classList.remove('open');
841
+ // ─── API KEY TEST ─────────────────────────────────────────────────────────
842
+ async function testAllKeys(){
843
+ toast('πŸ” Testando chaves...',3000);
844
+ const tests=[
845
+ {id:'anthropic',fn:async()=>{
846
+ if(!S.keys.anthropic)return false;
847
+ const r=await fetch(px('https://api.anthropic.com/v1/models'),{headers:{'x-api-key':S.keys.anthropic,'anthropic-version':'2023-06-01'}});
848
+ return r.ok;
849
+ }},
850
+ {id:'openai',fn:async()=>{
851
+ if(!S.keys.openai)return false;
852
+ const r=await fetch(px('https://api.openai.com/v1/models'),{headers:{Authorization:'Bearer '+S.keys.openai}});
853
+ return r.ok;
854
+ }},
855
+ {id:'google',fn:async()=>{
856
+ if(!S.keys.google)return false;
857
+ const r=await fetch(px(`https://generativelanguage.googleapis.com/v1beta/models?key=${S.keys.google}`));
858
+ return r.ok;
859
+ }},
860
+ {id:'mistral',fn:async()=>{
861
+ if(!S.keys.mistral)return false;
862
+ const r=await fetch(px('https://api.mistral.ai/v1/models'),{headers:{Authorization:'Bearer '+S.keys.mistral}});
863
+ return r.ok;
864
+ }},
865
+ ];
866
+ for(const t of tests){
867
+ try{
868
+ const ok=await t.fn();
869
+ const el=document.getElementById('ks-'+t.id);
870
+ if(el)el.className='key-status '+(ok?'ok':'');
871
+ }catch{}
872
+ }
873
+ updateStatus();
874
+ toast('βœ… Teste concluΓ­do!');
1100
875
  }
1101
876
 
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');
877
+ function updateStatus(){
878
+ const dot=document.getElementById('st-dot');
879
+ const txt=document.getElementById('st-txt');
880
+ const model=MODELS.find(m=>m.id===S.model);
881
+ const key=model?S.keys[model.p]:'';
882
+ if(!S.model){dot.className='warn';txt.textContent='Selecione um modelo';return;}
883
+ if(!key&&model?.p!=='local'){dot.className='off';txt.textContent='Sem API Key para '+model.p;return;}
884
+ dot.className='on';txt.textContent=(model?.name||S.model)+' Β· Pronto';
1115
885
  }
1116
886
 
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');
887
+ // ─── MODELOS ──────────────────────────────────────────────────────────────
888
+ function renderModels(){
889
+ const list=mFilter==='all'?MODELS:MODELS.filter(m=>m.p===mFilter);
890
+ document.getElementById('model-grid').innerHTML=list.map(m=>`
891
+ <div class="mc prov-${m.p} ${S.model===m.id?'sel':''}" onclick="selModel('${m.id}')">
892
+ <div class="mc-check">βœ“</div>
893
+ <div class="mc-prov">${{anthropic:'🟠 Anthropic',openai:'🟒 OpenAI',google:'πŸ”΅ Google',mistral:'🟣 Mistral',local:'⚫ Local'}[m.p]}</div>
894
+ <div class="mc-name">${h(m.name)}</div>
895
+ <div class="mc-desc">${h(m.desc)}</div>
896
+ </div>`).join('');
897
+ }
898
+ function filterM(p,btn){
899
+ mFilter=p;
900
+ document.querySelectorAll('.pf').forEach(b=>b.classList.remove('on'));
901
+ btn.classList.add('on');
902
+ renderModels();
903
+ }
904
+ function selModel(id){
905
+ S.model=id;save();
906
+ renderModels();
907
+ const m=MODELS.find(x=>x.id===id);
908
+ document.getElementById('model-name-badge').textContent=m?.name||id;
909
+ updateStatus();
910
+ toast('βœ… Modelo: '+m?.name);
1129
911
  }
1130
912
 
1131
- // === UTILS ===
1132
- function handleKey(e) {
1133
- if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
913
+ // ─── KEYS ─────────────────────────────────────────────────────────────────
914
+ function loadKeys(){
915
+ document.getElementById('k-anthropic').value=S.keys.anthropic||'';
916
+ document.getElementById('k-openai').value=S.keys.openai||'';
917
+ document.getElementById('k-google').value=S.keys.google||'';
918
+ document.getElementById('k-mistral').value=S.keys.mistral||'';
1134
919
  }
1135
- function autoResize(el) {
1136
- el.style.height = 'auto';
1137
- el.style.height = Math.min(el.scrollHeight, 200) + 'px';
920
+ function saveKeys(){
921
+ S.keys.anthropic=document.getElementById('k-anthropic').value.trim();
922
+ S.keys.openai=document.getElementById('k-openai').value.trim();
923
+ S.keys.google=document.getElementById('k-google').value.trim();
924
+ S.keys.mistral=document.getElementById('k-mistral').value.trim();
925
+ save();updateStatus();
1138
926
  }
1139
- function escHtml(s) {
1140
- return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
927
+
928
+ // ─── GATEWAYS ─────────────────────────────────────────────────────────────
929
+ function renderGws(){
930
+ const el=document.getElementById('gw-list');
931
+ if(!S.gws.length){
932
+ el.innerHTML=`<div style="text-align:center;padding:32px;color:var(--txt3)">
933
+ <div style="font-size:32px;margin-bottom:10px">πŸ”Œ</div>
934
+ Nenhum gateway. O PS Claw chamarΓ‘ as APIs diretamente com suas API Keys.
935
+ </div>`;return;
936
+ }
937
+ el.innerHTML=S.gws.map(g=>`
938
+ <div class="gw-card ${S.activeGw===g.id?'active-gw':''}">
939
+ <div class="gw-left">
940
+ <div class="gw-name">
941
+ ${h(g.name)}
942
+ ${S.activeGw===g.id?'<span class="badge badge-acc">Ativo</span>':''}
943
+ <span class="badge badge-gray" id="gst-${g.id}">...</span>
944
+ </div>
945
+ <div class="gw-url">${h(g.url)}</div>
946
+ </div>
947
+ <div class="gw-acts">
948
+ <button class="btn btn-ghost btn-xs" onclick="testGw('${g.id}')">Testar</button>
949
+ ${S.activeGw!==g.id?`<button class="btn btn-primary btn-xs" onclick="setGw('${g.id}')">Usar</button>`:''}
950
+ <button class="btn btn-danger btn-xs" onclick="delGw('${g.id}')">βœ•</button>
951
+ </div>
952
+ </div>`).join('');
953
+ S.gws.forEach(g=>testGw(g.id,true));
1141
954
  }
1142
- function escAttr(s) {
1143
- return (s||'').replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
955
+ function openAddGw(){
956
+ document.getElementById('gw-n').value='';
957
+ document.getElementById('gw-u').value='http://localhost:18789';
958
+ document.getElementById('gw-t').value='';
959
+ document.getElementById('modal-bg').classList.add('open');
1144
960
  }
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;
961
+ function closeMod(e){if(!e||e.target===document.getElementById('modal-bg'))document.getElementById('modal-bg').classList.remove('open')}
962
+ function addGw(){
963
+ const id='gw'+Date.now();
964
+ const name=document.getElementById('gw-n').value.trim()||'Gateway';
965
+ const url=document.getElementById('gw-u').value.trim()||'http://localhost:18789';
966
+ const token=document.getElementById('gw-t').value.trim();
967
+ S.gws.push({id,name,url,token});
968
+ if(!S.activeGw)S.activeGw=id;
969
+ save();closeMod();renderGws();toast('βœ… Gateway adicionado!');
1160
970
  }
1161
- function toggleSidebar() {
1162
- document.getElementById('sidebar').classList.toggle('open');
971
+ 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();}
972
+ function setGw(id){S.activeGw=id;save();renderGws();toast('βœ… Gateway ativo!');}
973
+ async function testGw(id,silent){
974
+ const gw=S.gws.find(g=>g.id===id);const el=document.getElementById('gst-'+id);
975
+ if(!el)return;el.textContent='...';el.className='badge badge-gray';
976
+ try{
977
+ const r=await fetch(px(gw.url+'/health'),{headers:gw.token?{Authorization:'Bearer '+gw.token}:{},signal:AbortSignal.timeout(4000)});
978
+ el.textContent=r.ok?'Online':'Erro';
979
+ el.className='badge '+(r.ok?'badge-green':'badge-red');
980
+ if(!silent&&r.ok)toast('βœ… Gateway online!');
981
+ }catch{el.textContent='Offline';el.className='badge badge-red';if(!silent)toast('❌ Inacessível');}
1163
982
  }
1164
983
 
1165
- function saveCfg() {
1166
- localStorage.setItem('ps-claw-cfg', JSON.stringify(cfg));
984
+ // ─── CFG ──────────────────────────────────────────────────────────────────
985
+ function loadCfg(){
986
+ document.getElementById('cfg-name').value=S.cfg.name||'VocΓͺ';
987
+ document.getElementById('cfg-agent').value=S.cfg.agent||'PS Claw';
988
+ document.getElementById('cfg-sys').value=S.cfg.sys||'';
989
+ document.getElementById('cfg-temp').value=S.cfg.temp||'0.7';
990
+ document.getElementById('cfg-tok').value=S.cfg.tok||'4096';
1167
991
  }
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);
992
+ function saveCfg(){
993
+ S.cfg.name=document.getElementById('cfg-name').value;
994
+ S.cfg.agent=document.getElementById('cfg-agent').value;
995
+ S.cfg.sys=document.getElementById('cfg-sys').value;
996
+ S.cfg.temp=document.getElementById('cfg-temp').value;
997
+ S.cfg.tok=document.getElementById('cfg-tok').value;
998
+ save();
999
+ }
1000
+ function clearChats(){if(!confirm('Apagar histΓ³rico?'))return;S.chats=[];curChat=null;save();renderChatList();renderMsgs();toast('πŸ—‘οΈ HistΓ³rico apagado');}
1001
+ function clearAll(){if(!confirm('Resetar tudo?'))return;localStorage.removeItem('psc2');location.reload();}
1002
+
1003
+ // ─── UTILS ────────────────────────────────────────────────────────────────
1004
+ function h(s=''){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
1005
+ function fmt(text=''){
1006
+ let t=h(text);
1007
+ t=t.replace(/```(\w*)\n?([\s\S]*?)```/g,(_,l,c)=>`<pre><code>${c.trim()}</code></pre>`);
1008
+ t=t.replace(/`([^`]+)`/g,'<code>$1</code>');
1009
+ t=t.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>');
1010
+ t=t.replace(/\*(.+?)\*/g,'<em>$1</em>');
1011
+ t=t.replace(/^### (.+)$/gm,'<h3>$1</h3>');
1012
+ t=t.replace(/^## (.+)$/gm,'<h2>$1</h2>');
1013
+ t=t.replace(/^# (.+)$/gm,'<h1>$1</h1>');
1014
+ t=t.replace(/^- (.+)$/gm,'<li>$1</li>');
1015
+ t=t.replace(/(\|.+\|\n?)+/g,tbl=>{
1016
+ const rows=tbl.trim().split('\n').filter(r=>!r.match(/^\|[-\s|]+\|$/));
1017
+ return '<table>'+rows.map((r,i)=>{
1018
+ const cells=r.split('|').filter((_,j)=>j>0&&j<r.split('|').length-1);
1019
+ return i===0?`<tr>${cells.map(c=>`<th>${c.trim()}</th>`).join('')}</tr>`:`<tr>${cells.map(c=>`<td>${c.trim()}</td>`).join('')}</tr>`;
1020
+ }).join('')+'</table>';
1021
+ });
1022
+ t=t.split('\n\n').map(p=>p.startsWith('<')?p:`<p>${p.replace(/\n/g,'<br>')}</p>`).join('');
1023
+ return t;
1024
+ }
1025
+ function toast(msg,dur=2600){
1026
+ const el=document.getElementById('toast');
1027
+ el.textContent=msg;el.classList.add('show');
1028
+ setTimeout(()=>el.classList.remove('show'),dur);
1176
1029
  }
1030
+ function handleKey(e){if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendMsg();}}
1031
+ function autoResize(el){el.style.height='auto';el.style.height=Math.min(el.scrollHeight,180)+'px';}
1032
+ function sendQ(t){document.getElementById('msg-in').value=t;goTab('chat');sendMsg();}
1033
+ function toggleSb(){document.getElementById('sidebar').classList.toggle('open');}
1034
+
1035
+ // ─── BOOT ─────────────────────────────────────────────────────────────────
1036
+ renderChatList();renderMsgs();updateStatus();
1037
+ const bm=MODELS.find(m=>m.id===S.model);
1038
+ if(bm)document.getElementById('model-name-badge').textContent=bm.name;
1039
+ loadKeys();
1177
1040
  </script>
1178
1041
  </body>
1179
1042
  </html>