@pmcai/codex-chat-widget 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/index.cjs +276 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.css +476 -0
- package/dist/index.css.map +7 -0
- package/dist/index.js +246 -0
- package/dist/index.js.map +7 -0
- package/dist/styles.css +476 -0
- package/package.json +54 -0
- package/src/CodexChatWidget.jsx +84 -0
- package/src/styles.css +3 -0
package/dist/styles.css
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
/* src/styles.css */
|
|
2
|
+
.codex-chat-app .codex-chat-app {
|
|
3
|
+
box-sizing: border-box;
|
|
4
|
+
}
|
|
5
|
+
.codex-chat-app *,
|
|
6
|
+
.codex-chat-app *:before,
|
|
7
|
+
.codex-chat-app *:after {
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
}
|
|
10
|
+
.codex-chat-app {
|
|
11
|
+
height: 100dvh;
|
|
12
|
+
min-height: 520px;
|
|
13
|
+
overflow: hidden;
|
|
14
|
+
display: grid;
|
|
15
|
+
grid-template-columns: 250px minmax(0, 900px);
|
|
16
|
+
justify-content: center;
|
|
17
|
+
background: #08121f;
|
|
18
|
+
color: #edf4ff;
|
|
19
|
+
font-family:
|
|
20
|
+
ui-sans-serif,
|
|
21
|
+
system-ui,
|
|
22
|
+
-apple-system,
|
|
23
|
+
sans-serif;
|
|
24
|
+
border-radius: 12px;
|
|
25
|
+
position: relative;
|
|
26
|
+
text-align: left;
|
|
27
|
+
}
|
|
28
|
+
.codex-chat-sidebar {
|
|
29
|
+
min-height: 0;
|
|
30
|
+
overflow: hidden;
|
|
31
|
+
padding: 12px;
|
|
32
|
+
border-right: 1px solid #20344e;
|
|
33
|
+
background: #0a1625;
|
|
34
|
+
display: grid;
|
|
35
|
+
grid-template-rows: auto minmax(0, 1fr) auto;
|
|
36
|
+
gap: 12px;
|
|
37
|
+
}
|
|
38
|
+
.codex-chat-app .new,
|
|
39
|
+
.codex-chat-app .settingsBtn,
|
|
40
|
+
.codex-chat-save {
|
|
41
|
+
border: 0;
|
|
42
|
+
border-radius: 8px;
|
|
43
|
+
padding: 10px;
|
|
44
|
+
background: #236fc1;
|
|
45
|
+
color: white;
|
|
46
|
+
font-weight: 700;
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
}
|
|
49
|
+
.codex-chat-sessionList {
|
|
50
|
+
min-height: 0;
|
|
51
|
+
display: grid;
|
|
52
|
+
align-content: start;
|
|
53
|
+
gap: 5px;
|
|
54
|
+
overflow-y: auto;
|
|
55
|
+
}
|
|
56
|
+
.codex-chat-settingsBtn {
|
|
57
|
+
width: 100%;
|
|
58
|
+
align-self: end;
|
|
59
|
+
}
|
|
60
|
+
.codex-chat-session {
|
|
61
|
+
background: transparent;
|
|
62
|
+
border: 0;
|
|
63
|
+
border-radius: 7px;
|
|
64
|
+
padding: 9px;
|
|
65
|
+
text-align: left;
|
|
66
|
+
color: #bfcede;
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
overflow: hidden;
|
|
69
|
+
text-overflow: ellipsis;
|
|
70
|
+
white-space: nowrap;
|
|
71
|
+
}
|
|
72
|
+
.codex-chat-app .session small {
|
|
73
|
+
display: block;
|
|
74
|
+
color: #7189a2;
|
|
75
|
+
margin-top: 3px;
|
|
76
|
+
font-size: 10px;
|
|
77
|
+
}
|
|
78
|
+
.codex-chat-activeSession {
|
|
79
|
+
background: #173859;
|
|
80
|
+
color: #fff;
|
|
81
|
+
}
|
|
82
|
+
.codex-chat-settingsBtn {
|
|
83
|
+
margin-top: auto;
|
|
84
|
+
background: #142943;
|
|
85
|
+
color: #cce1f7;
|
|
86
|
+
}
|
|
87
|
+
.codex-chat-shell {
|
|
88
|
+
height: 100%;
|
|
89
|
+
min-height: 0;
|
|
90
|
+
overflow: hidden;
|
|
91
|
+
min-width: 0;
|
|
92
|
+
display: grid;
|
|
93
|
+
grid-template-rows: auto minmax(0, 1fr) auto;
|
|
94
|
+
background: #0d1a2b;
|
|
95
|
+
}
|
|
96
|
+
.codex-chat-app header {
|
|
97
|
+
padding: 14px 18px;
|
|
98
|
+
border-bottom: 1px solid #20344e;
|
|
99
|
+
display: flex;
|
|
100
|
+
align-items: center;
|
|
101
|
+
justify-content: space-between;
|
|
102
|
+
}
|
|
103
|
+
.codex-chat-app header b {
|
|
104
|
+
display: block;
|
|
105
|
+
font-size: 16px;
|
|
106
|
+
}
|
|
107
|
+
.codex-chat-app header span {
|
|
108
|
+
font-size: 12px;
|
|
109
|
+
color: #9db1c8;
|
|
110
|
+
}
|
|
111
|
+
.codex-chat-headerActions {
|
|
112
|
+
display: flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
gap: 8px;
|
|
115
|
+
flex: 0 0 auto;
|
|
116
|
+
}
|
|
117
|
+
.codex-chat-app .modelBtn,
|
|
118
|
+
.codex-chat-sessionsBtn {
|
|
119
|
+
border: 1px solid #35658f;
|
|
120
|
+
border-radius: 7px;
|
|
121
|
+
background: #122b45;
|
|
122
|
+
color: #dceeff;
|
|
123
|
+
padding: 7px 10px;
|
|
124
|
+
font: 700 12px ui-sans-serif, system-ui;
|
|
125
|
+
cursor: pointer;
|
|
126
|
+
white-space: nowrap;
|
|
127
|
+
}
|
|
128
|
+
.codex-chat-sessionsBtn {
|
|
129
|
+
display: none;
|
|
130
|
+
}
|
|
131
|
+
.codex-chat-sessionRow {
|
|
132
|
+
display: flex;
|
|
133
|
+
gap: 4px;
|
|
134
|
+
min-width: 0;
|
|
135
|
+
}
|
|
136
|
+
.codex-chat-app .sessionRow .codex-chat-session {
|
|
137
|
+
flex: 1;
|
|
138
|
+
min-width: 0;
|
|
139
|
+
}
|
|
140
|
+
.codex-chat-sessionActions {
|
|
141
|
+
display: flex;
|
|
142
|
+
align-items: center;
|
|
143
|
+
}
|
|
144
|
+
.codex-chat-app .sessionActions button {
|
|
145
|
+
border: 0;
|
|
146
|
+
background: transparent;
|
|
147
|
+
color: #9db1c8;
|
|
148
|
+
padding: 5px;
|
|
149
|
+
cursor: pointer;
|
|
150
|
+
}
|
|
151
|
+
.codex-chat-sessionDrawer {
|
|
152
|
+
display: none;
|
|
153
|
+
}
|
|
154
|
+
.codex-chat-status {
|
|
155
|
+
color: #5ee0a1 !important;
|
|
156
|
+
}
|
|
157
|
+
.codex-chat-messages {
|
|
158
|
+
overflow: auto;
|
|
159
|
+
padding: 20px;
|
|
160
|
+
display: flex;
|
|
161
|
+
flex-direction: column;
|
|
162
|
+
gap: 14px;
|
|
163
|
+
}
|
|
164
|
+
.codex-chat-welcome {
|
|
165
|
+
margin: auto;
|
|
166
|
+
max-width: 530px;
|
|
167
|
+
text-align: center;
|
|
168
|
+
color: #b6c8db;
|
|
169
|
+
}
|
|
170
|
+
.codex-chat-app .welcome h1 {
|
|
171
|
+
color: #fff;
|
|
172
|
+
font-size: 26px;
|
|
173
|
+
}
|
|
174
|
+
.codex-chat-app .welcome code,
|
|
175
|
+
.codex-chat-app pre {
|
|
176
|
+
font-family:
|
|
177
|
+
ui-monospace,
|
|
178
|
+
SFMono-Regular,
|
|
179
|
+
Menlo,
|
|
180
|
+
monospace;
|
|
181
|
+
}
|
|
182
|
+
.codex-chat-app article {
|
|
183
|
+
max-width: 92%;
|
|
184
|
+
padding: 12px 14px;
|
|
185
|
+
border-radius: 12px;
|
|
186
|
+
border: 1px solid #29415e;
|
|
187
|
+
}
|
|
188
|
+
.codex-chat-user {
|
|
189
|
+
align-self: flex-end;
|
|
190
|
+
background: #15477f;
|
|
191
|
+
}
|
|
192
|
+
.codex-chat-assistant {
|
|
193
|
+
align-self: flex-start;
|
|
194
|
+
background: #13243a;
|
|
195
|
+
}
|
|
196
|
+
.codex-chat-app label {
|
|
197
|
+
display: block;
|
|
198
|
+
margin-bottom: 8px;
|
|
199
|
+
color: #9db1c8;
|
|
200
|
+
font-size: 12px;
|
|
201
|
+
}
|
|
202
|
+
.codex-chat-app pre {
|
|
203
|
+
white-space: pre-wrap;
|
|
204
|
+
word-break: break-word;
|
|
205
|
+
margin: 0;
|
|
206
|
+
line-height: 1.5;
|
|
207
|
+
font-size: 13px;
|
|
208
|
+
}
|
|
209
|
+
.codex-chat-markdown {
|
|
210
|
+
color: #edf4ff;
|
|
211
|
+
font-size: 15px;
|
|
212
|
+
line-height: 1.68;
|
|
213
|
+
overflow-wrap: anywhere;
|
|
214
|
+
}
|
|
215
|
+
.codex-chat-app .markdown > *:first-child {
|
|
216
|
+
margin-top: 0;
|
|
217
|
+
}
|
|
218
|
+
.codex-chat-app .markdown > *:last-child {
|
|
219
|
+
margin-bottom: 0;
|
|
220
|
+
}
|
|
221
|
+
.codex-chat-app .markdown h1,
|
|
222
|
+
.codex-chat-app .markdown h2,
|
|
223
|
+
.codex-chat-app .markdown h3 {
|
|
224
|
+
color: #fff;
|
|
225
|
+
line-height: 1.25;
|
|
226
|
+
margin: 1.2em 0 .45em;
|
|
227
|
+
}
|
|
228
|
+
.codex-chat-app .markdown h1 {
|
|
229
|
+
font-size: 1.35em;
|
|
230
|
+
}
|
|
231
|
+
.codex-chat-app .markdown h2 {
|
|
232
|
+
font-size: 1.18em;
|
|
233
|
+
}
|
|
234
|
+
.codex-chat-app .markdown h3 {
|
|
235
|
+
font-size: 1.05em;
|
|
236
|
+
}
|
|
237
|
+
.codex-chat-app .markdown p {
|
|
238
|
+
margin: .65em 0;
|
|
239
|
+
}
|
|
240
|
+
.codex-chat-app .markdown ul,
|
|
241
|
+
.codex-chat-app .markdown ol {
|
|
242
|
+
padding-left: 1.35em;
|
|
243
|
+
}
|
|
244
|
+
.codex-chat-app .markdown strong {
|
|
245
|
+
color: #fff;
|
|
246
|
+
}
|
|
247
|
+
.codex-chat-app .markdown code {
|
|
248
|
+
font-family:
|
|
249
|
+
ui-monospace,
|
|
250
|
+
SFMono-Regular,
|
|
251
|
+
Menlo,
|
|
252
|
+
monospace;
|
|
253
|
+
font-size: .88em;
|
|
254
|
+
background: #091523;
|
|
255
|
+
border: 1px solid #2b4666;
|
|
256
|
+
padding: .12em .32em;
|
|
257
|
+
border-radius: 4px;
|
|
258
|
+
}
|
|
259
|
+
.codex-chat-app .markdown pre {
|
|
260
|
+
background: #07111d;
|
|
261
|
+
border: 1px solid #2b4666;
|
|
262
|
+
border-radius: 8px;
|
|
263
|
+
padding: 12px;
|
|
264
|
+
overflow: auto;
|
|
265
|
+
}
|
|
266
|
+
.codex-chat-progress {
|
|
267
|
+
border-color: #3b618d;
|
|
268
|
+
}
|
|
269
|
+
.codex-chat-app .progress ol {
|
|
270
|
+
list-style: none;
|
|
271
|
+
padding: 0;
|
|
272
|
+
margin: 0;
|
|
273
|
+
display: grid;
|
|
274
|
+
gap: 8px;
|
|
275
|
+
}
|
|
276
|
+
.codex-chat-app .progress li {
|
|
277
|
+
color: #91a9c2;
|
|
278
|
+
font-size: 13px;
|
|
279
|
+
}
|
|
280
|
+
.codex-chat-app .progress li:before {
|
|
281
|
+
content: "\2713";
|
|
282
|
+
display: inline-grid;
|
|
283
|
+
place-items: center;
|
|
284
|
+
width: 16px;
|
|
285
|
+
height: 16px;
|
|
286
|
+
margin-right: 8px;
|
|
287
|
+
border-radius: 50%;
|
|
288
|
+
background: #274761;
|
|
289
|
+
color: #b9d6ee;
|
|
290
|
+
font-size: 11px;
|
|
291
|
+
}
|
|
292
|
+
.codex-chat-app .progress li.active {
|
|
293
|
+
color: #eaf5ff;
|
|
294
|
+
}
|
|
295
|
+
.codex-chat-app .progress li.active:before {
|
|
296
|
+
content: "\2026";
|
|
297
|
+
background: #2d86d1;
|
|
298
|
+
color: white;
|
|
299
|
+
animation: pulse 1.2s infinite;
|
|
300
|
+
}
|
|
301
|
+
@keyframes pulse {
|
|
302
|
+
50% {
|
|
303
|
+
opacity: .5;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
.codex-chat-composer {
|
|
307
|
+
border-top: 1px solid #20344e;
|
|
308
|
+
background: #0b1727;
|
|
309
|
+
}
|
|
310
|
+
.codex-chat-modelFooter {
|
|
311
|
+
padding: 5px 14px 0;
|
|
312
|
+
color: #89a3be;
|
|
313
|
+
font-size: 11px;
|
|
314
|
+
overflow: hidden;
|
|
315
|
+
text-overflow: ellipsis;
|
|
316
|
+
white-space: nowrap;
|
|
317
|
+
}
|
|
318
|
+
.codex-chat-systemMessage {
|
|
319
|
+
align-self: center;
|
|
320
|
+
max-width: 90%;
|
|
321
|
+
padding: 6px 10px;
|
|
322
|
+
border-radius: 999px;
|
|
323
|
+
background: #172d45;
|
|
324
|
+
color: #9fc4e5;
|
|
325
|
+
font-size: 12px;
|
|
326
|
+
}
|
|
327
|
+
.codex-chat-app form {
|
|
328
|
+
display: flex;
|
|
329
|
+
gap: 10px;
|
|
330
|
+
padding: 10px 14px 14px;
|
|
331
|
+
background: #0b1727;
|
|
332
|
+
}
|
|
333
|
+
.codex-chat-app textarea {
|
|
334
|
+
resize: vertical;
|
|
335
|
+
flex: 1;
|
|
336
|
+
border: 1px solid #2b4666;
|
|
337
|
+
background: #07111d;
|
|
338
|
+
color: #fff;
|
|
339
|
+
border-radius: 8px;
|
|
340
|
+
padding: 10px;
|
|
341
|
+
font: inherit;
|
|
342
|
+
}
|
|
343
|
+
.codex-chat-app form button {
|
|
344
|
+
border: 0;
|
|
345
|
+
border-radius: 8px;
|
|
346
|
+
padding: 0 18px;
|
|
347
|
+
background: #3b89dc;
|
|
348
|
+
color: white;
|
|
349
|
+
font-weight: 700;
|
|
350
|
+
cursor: pointer;
|
|
351
|
+
}
|
|
352
|
+
.codex-chat-app form button:disabled {
|
|
353
|
+
background: #39516b;
|
|
354
|
+
color: #a5b5c5;
|
|
355
|
+
cursor: wait;
|
|
356
|
+
}
|
|
357
|
+
.codex-chat-modal {
|
|
358
|
+
position: fixed;
|
|
359
|
+
inset: 0;
|
|
360
|
+
display: grid;
|
|
361
|
+
place-items: center;
|
|
362
|
+
background: #000a;
|
|
363
|
+
padding: 16px;
|
|
364
|
+
overflow: auto;
|
|
365
|
+
}
|
|
366
|
+
.codex-chat-settings {
|
|
367
|
+
width: min(460px, calc(100vw - 32px));
|
|
368
|
+
max-height: calc(100dvh - 32px);
|
|
369
|
+
min-width: 0;
|
|
370
|
+
overflow-y: auto;
|
|
371
|
+
border: 1px solid #2b4666;
|
|
372
|
+
background: #102035;
|
|
373
|
+
border-radius: 12px;
|
|
374
|
+
padding: 16px;
|
|
375
|
+
}
|
|
376
|
+
.codex-chat-app .settings header {
|
|
377
|
+
padding: 0 0 12px;
|
|
378
|
+
margin-bottom: 12px;
|
|
379
|
+
}
|
|
380
|
+
.codex-chat-app .settings header button {
|
|
381
|
+
background: transparent;
|
|
382
|
+
color: #fff;
|
|
383
|
+
border: 0;
|
|
384
|
+
font-size: 24px;
|
|
385
|
+
}
|
|
386
|
+
.codex-chat-app .settings label {
|
|
387
|
+
display: grid;
|
|
388
|
+
gap: 6px;
|
|
389
|
+
margin: 12px 0;
|
|
390
|
+
min-width: 0;
|
|
391
|
+
}
|
|
392
|
+
.codex-chat-app .settings input,
|
|
393
|
+
.codex-chat-app .settings select {
|
|
394
|
+
width: 100%;
|
|
395
|
+
max-width: 100%;
|
|
396
|
+
min-width: 0;
|
|
397
|
+
background: #08121f;
|
|
398
|
+
color: #fff;
|
|
399
|
+
border: 1px solid #2b4666;
|
|
400
|
+
border-radius: 7px;
|
|
401
|
+
padding: 9px;
|
|
402
|
+
}
|
|
403
|
+
.codex-chat-app .settings select {
|
|
404
|
+
overflow: hidden;
|
|
405
|
+
text-overflow: ellipsis;
|
|
406
|
+
white-space: nowrap;
|
|
407
|
+
}
|
|
408
|
+
.codex-chat-app .settings p {
|
|
409
|
+
font-size: 12px;
|
|
410
|
+
color: #9db1c8;
|
|
411
|
+
}
|
|
412
|
+
.codex-chat-catalogStatus {
|
|
413
|
+
display: block;
|
|
414
|
+
color: #89a3be;
|
|
415
|
+
font-size: 11px;
|
|
416
|
+
margin-top: 4px;
|
|
417
|
+
}
|
|
418
|
+
@media (max-width:640px) {
|
|
419
|
+
.codex-chat-app {
|
|
420
|
+
grid-template-columns: 1fr;
|
|
421
|
+
}
|
|
422
|
+
.codex-chat-sidebar {
|
|
423
|
+
display: none;
|
|
424
|
+
}
|
|
425
|
+
.codex-chat-sessionsBtn {
|
|
426
|
+
display: inline-flex;
|
|
427
|
+
}
|
|
428
|
+
.codex-chat-status {
|
|
429
|
+
display: none;
|
|
430
|
+
}
|
|
431
|
+
.codex-chat-messages {
|
|
432
|
+
padding: 12px;
|
|
433
|
+
}
|
|
434
|
+
.codex-chat-app article {
|
|
435
|
+
max-width: 96%;
|
|
436
|
+
}
|
|
437
|
+
.codex-chat-modal {
|
|
438
|
+
align-items: end;
|
|
439
|
+
padding: 0;
|
|
440
|
+
}
|
|
441
|
+
.codex-chat-settings {
|
|
442
|
+
width: 100%;
|
|
443
|
+
max-height: 82dvh;
|
|
444
|
+
border-radius: 18px 18px 0 0;
|
|
445
|
+
border-bottom: 0;
|
|
446
|
+
padding: 18px 16px 28px;
|
|
447
|
+
}
|
|
448
|
+
.codex-chat-sessionDrawer {
|
|
449
|
+
display: block;
|
|
450
|
+
position: fixed;
|
|
451
|
+
inset: 0;
|
|
452
|
+
background: #000a;
|
|
453
|
+
z-index: 20;
|
|
454
|
+
}
|
|
455
|
+
.codex-chat-sessionDrawer section {
|
|
456
|
+
position: absolute;
|
|
457
|
+
inset: 0 auto 0 0;
|
|
458
|
+
width: min(320px, 86vw);
|
|
459
|
+
padding: 16px;
|
|
460
|
+
background: #0a1625;
|
|
461
|
+
border-right: 1px solid #29415e;
|
|
462
|
+
display: grid;
|
|
463
|
+
grid-template-rows: auto auto minmax(0, 1fr);
|
|
464
|
+
gap: 12px;
|
|
465
|
+
}
|
|
466
|
+
.codex-chat-sessionDrawer header {
|
|
467
|
+
padding: 0 0 12px;
|
|
468
|
+
}
|
|
469
|
+
.codex-chat-sessionDrawer header button {
|
|
470
|
+
background: transparent;
|
|
471
|
+
color: #fff;
|
|
472
|
+
border: 0;
|
|
473
|
+
font-size: 24px;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
/*# sourceMappingURL=index.css.map */
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pmcai/codex-chat-widget",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Embeddable React chat widget that talks to a Codex harness API (sessions, streaming, model settings).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.cjs",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./styles.css": "./dist/styles.css"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"src",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"sideEffects": [
|
|
23
|
+
"*.css"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "node build.mjs",
|
|
27
|
+
"prepare": "node build.mjs",
|
|
28
|
+
"prepublishOnly": "node build.mjs",
|
|
29
|
+
"test": "node smoke-test.mjs"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"react": ">=18",
|
|
33
|
+
"react-dom": ">=18",
|
|
34
|
+
"react-markdown": ">=9",
|
|
35
|
+
"remark-gfm": ">=4"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"codex",
|
|
39
|
+
"chat",
|
|
40
|
+
"widget",
|
|
41
|
+
"react",
|
|
42
|
+
"openrouter",
|
|
43
|
+
"harness",
|
|
44
|
+
"embed"
|
|
45
|
+
],
|
|
46
|
+
"author": "Suphot (pemo)",
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"esbuild": "^0.24.0"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import './styles.css'
|
|
3
|
+
import ReactMarkdown from 'react-markdown'
|
|
4
|
+
import remarkGfm from 'remark-gfm'
|
|
5
|
+
|
|
6
|
+
const defaultSettings = { provider: 'server', model: 'cohere/north-mini-code:free', apiKey: '', baseUrl: '' }
|
|
7
|
+
|
|
8
|
+
export default function CodexChatWidget({
|
|
9
|
+
apiBase = '',
|
|
10
|
+
storageKey = 'codex-chat',
|
|
11
|
+
initialSessionId,
|
|
12
|
+
title = 'Codex Harness'
|
|
13
|
+
}) {
|
|
14
|
+
const apiBaseUrl = String(apiBase || '').replace(/\/$/, '')
|
|
15
|
+
const [sessions, setSessions] = useState([])
|
|
16
|
+
const [session, setSession] = useState(null)
|
|
17
|
+
const [prompt, setPrompt] = useState('')
|
|
18
|
+
const [activityBySession, setActivityBySession] = useState({})
|
|
19
|
+
const [runningBySession, setRunningBySession] = useState({})
|
|
20
|
+
const [settings, setSettings] = useState(defaultSettings)
|
|
21
|
+
const [models, setModels] = useState([])
|
|
22
|
+
const [modelsState, setModelsState] = useState('')
|
|
23
|
+
const [showSettings, setShowSettings] = useState(false)
|
|
24
|
+
const [showSessions, setShowSessions] = useState(false)
|
|
25
|
+
const [homeLabel, setHomeLabel] = useState('~/.pmai/workspace')
|
|
26
|
+
|
|
27
|
+
async function refreshSessions(selectId) {
|
|
28
|
+
let items = await (await fetch(apiBaseUrl + '/api/sessions')).json()
|
|
29
|
+
if (!items.length) {
|
|
30
|
+
const created = await (await fetch(apiBaseUrl + '/api/sessions', { method: 'POST' })).json()
|
|
31
|
+
items = [created]
|
|
32
|
+
}
|
|
33
|
+
setSessions(items)
|
|
34
|
+
const wanted = selectId || session?.id
|
|
35
|
+
const id = items.some((item) => item.id === wanted) ? wanted : items[0].id
|
|
36
|
+
const response = await fetch(apiBaseUrl + `/api/sessions?id=${id}`)
|
|
37
|
+
if (response.ok) { setSession(await response.json()); localStorage.setItem(`${storageKey}-active-session`, id) }
|
|
38
|
+
}
|
|
39
|
+
useEffect(() => { const savedSession = localStorage.getItem(`${storageKey}-active-session`); refreshSessions(initialSessionId || savedSession || undefined); fetch(apiBaseUrl + '/api/settings').then((r) => r.json()).then(setSettings); fetch(apiBaseUrl + '/api/health').then((r) => r.json()).then((h) => { if (h.workspace) setHomeLabel('~/.pmai' + String(h.workspace).replace(/^\/pmai/, '')) }).catch(() => {}) }, [])
|
|
40
|
+
|
|
41
|
+
async function loadModels(nextSettings = settings) {
|
|
42
|
+
setModelsState('กำลังโหลด model list…')
|
|
43
|
+
const data = await (await fetch(apiBaseUrl + '/api/models', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(nextSettings), cache: 'no-store' })).json()
|
|
44
|
+
setModels(data.models || []); setModelsState(data.error || data.message || data.label || `${(data.models || []).length} models`)
|
|
45
|
+
}
|
|
46
|
+
async function openSettings() { setShowSettings(true); await loadModels() }
|
|
47
|
+
|
|
48
|
+
async function saveGlobalSettings() {
|
|
49
|
+
const previous = `${settings.provider}/${settings.model}`
|
|
50
|
+
const saved = await (await fetch(apiBaseUrl + '/api/settings', { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify(settings) })).json()
|
|
51
|
+
setSettings(saved)
|
|
52
|
+
if (session && `${saved.provider}/${saved.model}` !== previous) setSession((current) => ({ ...current, messages: [...current.messages, { role: 'system', text: `Model changed: ${saved.model} (${saved.provider})` }] }))
|
|
53
|
+
setShowSettings(false)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function newSession() { const created = await (await fetch(apiBaseUrl + '/api/sessions', { method: 'POST' })).json(); await refreshSessions(created.id) }
|
|
57
|
+
async function choose(id) { await refreshSessions(id) }
|
|
58
|
+
async function archive(id) { await fetch(apiBaseUrl + '/api/sessions', { method: 'PATCH', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id, archived: true }) }); await refreshSessions() }
|
|
59
|
+
async function remove(id) { if (!confirm('Delete this chat permanently?')) return; await fetch(apiBaseUrl + `/api/sessions?id=${id}`, { method: 'DELETE' }); if (session?.id === id) setSession(null); await refreshSessions() }
|
|
60
|
+
|
|
61
|
+
async function send(event) {
|
|
62
|
+
event.preventDefault(); const text = prompt.trim(); const sessionId = session?.id
|
|
63
|
+
if (!text || runningBySession[sessionId] || !sessionId) return
|
|
64
|
+
setSession((x) => ({ ...x, messages: [...x.messages, { role: 'user', text }] }))
|
|
65
|
+
setPrompt(''); setActivityBySession((x) => ({ ...x, [sessionId]: [{ text: 'กำลังเตรียมงาน' }] })); setRunningBySession((x) => ({ ...x, [sessionId]: true }))
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch(apiBaseUrl + '/api/chat', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ prompt: text, sessionId, settings }) })
|
|
68
|
+
if (!response.ok || !response.body) { const error = await response.json(); throw new Error(error.error || `Request failed (${response.status})`) }
|
|
69
|
+
const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let finalText = ''
|
|
70
|
+
while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const frames = buffer.split('\n\n'); buffer = frames.pop() || ''
|
|
71
|
+
for (const frame of frames) { const line = frame.split('\n').find((x) => x.startsWith('data: ')); if (!line) continue; const data = JSON.parse(line.slice(6)); if (data.type === 'status') setActivityBySession((x) => ({ ...x, [sessionId]: [...(x[sessionId] || []), { text: data.text }] })); if (data.type === 'final') finalText = data.text; if (data.type === 'error') throw new Error(data.text) }
|
|
72
|
+
}
|
|
73
|
+
if (session?.id === sessionId) setSession((x) => ({ ...x, messages: [...x.messages, { role: 'assistant', text: finalText || 'งานเสร็จแล้ว แต่ไม่มีข้อความสรุป' }] })); await refreshSessions(sessionId)
|
|
74
|
+
} catch (error) { if (session?.id === sessionId) setSession((x) => ({ ...x, messages: [...x.messages, { role: 'assistant', text: `เกิดข้อผิดพลาด: ${error.message}` }] })) }
|
|
75
|
+
finally { setActivityBySession((x) => ({ ...x, [sessionId]: [] })); setRunningBySession((x) => ({ ...x, [sessionId]: false })) }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return <main className="codex-chat-app">
|
|
79
|
+
<aside className="codex-chat-sidebar"><button className="codex-chat-new" onClick={newSession}>+ New chat</button><div className="codex-chat-sessionList">{sessions.map((item) => <div className="codex-chat-sessionRow" key={item.id}><button className={item.id === session?.id ? 'session activeSession' : 'session'} onClick={() => choose(item.id)}>{item.title}<small>{new Date(item.updatedAt).toLocaleString()}</small></button><div className="codex-chat-sessionActions"><button title="Archive" onClick={() => archive(item.id)}>⌫</button><button title="Delete" onClick={() => remove(item.id)}>×</button></div></div>)}</div><button className="codex-chat-settingsBtn" onClick={openSettings}>⚙ Settings</button></aside>
|
|
80
|
+
<section className="codex-chat-shell"><header><div><b>{title}</b><span>{settings.model} · {homeLabel}</span></div><div className="codex-chat-headerActions"><button className="codex-chat-sessionsBtn" onClick={() => setShowSessions(true)}>☰ Chats</button><button className="codex-chat-modelBtn" onClick={openSettings}>⌄ Model</button><span className="codex-chat-status">● Ready</span></div></header><section className="codex-chat-messages" aria-live="polite">{!session ? <div className="codex-chat-welcome">กำลังโหลด session…</div> : <>{session.messages.length === 0 && <div className="codex-chat-welcome"><h1>เริ่มคุยกับ Codex</h1><p>เลือก model หรือ provider ของคุณได้จาก Settings</p></div>}{session.messages.map((message, index) => message.role === 'system' ? <div className="codex-chat-systemMessage" key={index}>{message.text}</div> : <article className={message.role} key={index}><label>{message.role === 'user' ? 'คุณ' : 'คำตอบ'}</label>{message.role === 'assistant' ? <div className="codex-chat-markdown"><ReactMarkdown remarkPlugins={[remarkGfm]}>{message.text}</ReactMarkdown></div> : <pre>{message.text}</pre>}</article>)}{runningBySession[session.id] && <article className="codex-chat-assistant progress"><label>กำลังทำงาน</label><ol>{(activityBySession[session.id] || []).map((item, index) => <li key={index} className={index === (activityBySession[session.id] || []).length - 1 ? 'active' : ''}>{item.text}</li>)}</ol></article>}</>}</section><div className="codex-chat-composer"><div className="codex-chat-modelFooter">{settings.provider} · {settings.model}</div><form onSubmit={send}><textarea value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder="เช่น ตรวจไฟล์ใน workspace แล้วสรุปโครงสร้าง" rows="3" maxLength="8000" /><button disabled={runningBySession[session?.id] || !prompt.trim() || !session}>{runningBySession[session?.id] ? 'กำลังทำงาน' : 'ส่ง'}</button></form></div></section>
|
|
81
|
+
{showSessions && <div className="codex-chat-sessionDrawer"><section><header><b>Chats</b><button onClick={() => setShowSessions(false)}>×</button></header><button className="codex-chat-new" onClick={async () => { await newSession(); setShowSessions(false) }}>+ New chat</button><div className="codex-chat-sessionList">{sessions.map((item) => <div className="codex-chat-sessionRow" key={item.id}><button className={item.id === session?.id ? 'session activeSession' : 'session'} onClick={async () => { await choose(item.id); setShowSessions(false) }}>{item.title}</button><div className="codex-chat-sessionActions"><button onClick={() => archive(item.id)}>⌫</button><button onClick={() => remove(item.id)}>×</button></div></div>)}</div></section></div>}
|
|
82
|
+
{showSettings && <div className="codex-chat-modal"><section className="codex-chat-settings"><header><b>Settings</b><button onClick={() => setShowSettings(false)}>×</button></header><label>Provider<select value={settings.provider} onChange={(e) => { const next = { ...settings, provider: e.target.value }; setSettings(next); loadModels(next) }}><option value="server">OpenRouter key หลักของระบบ</option><option value="openrouter">OpenRouter key ของฉัน</option><option value="compatible">OpenAI-compatible provider ของฉัน</option><option value="google-ai-proxy">Google AI (server key, Responses adapter)</option></select></label>{settings.provider !== 'server' && <label>API key<input type="password" value={settings.apiKey} placeholder={settings.provider === 'google-ai-proxy' ? 'Google AI API key (override server key)' : 'API key จะบันทึกใน global settings'} onChange={(e) => setSettings({ ...settings, apiKey: e.target.value })}/></label>}{settings.provider === 'compatible' && <label>Base URL<input value={settings.baseUrl} placeholder="https://provider.example/v1" onChange={(e) => setSettings({ ...settings, baseUrl: e.target.value })}/></label>}<label>Model{settings.provider === 'compatible' ? <input value={settings.model} placeholder="provider/model-id" onChange={(e) => setSettings({ ...settings, model: e.target.value })}/> : <select value={settings.model} onChange={(e) => setSettings({ ...settings, model: e.target.value })}>{models.length ? models.map((model) => <option key={model.id} value={model.id}>{model.name} — {model.id}</option>) : <option value={settings.model}>{modelsState || settings.model}</option>}</select>}<small className="codex-chat-catalogStatus">{modelsState}</small></label><p>Settings ทั้งหมดเป็น global และเก็บใน SQLite volume ของ service</p><button className="codex-chat-save" onClick={saveGlobalSettings}>บันทึก</button></section></div>}
|
|
83
|
+
</main>
|
|
84
|
+
}
|
package/src/styles.css
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
.codex-chat-app /* Scoped styles for @suphot/codex-chat-widget */
|
|
2
|
+
.codex-chat-app{box-sizing:border-box}.codex-chat-app *,.codex-chat-app *:before,.codex-chat-app *:after{box-sizing:border-box}.codex-chat-app{height:100dvh;min-height:520px;overflow:hidden;display:grid;grid-template-columns:250px minmax(0,900px);justify-content:center;background:#08121f;color:#edf4ff;font-family:ui-sans-serif,system-ui,-apple-system,sans-serif;border-radius:12px;position:relative;text-align:left}.codex-chat-sidebar{min-height:0;overflow:hidden;padding:12px;border-right:1px solid #20344e;background:#0a1625;display:grid;grid-template-rows:auto minmax(0,1fr) auto;gap:12px}.codex-chat-app .new,.codex-chat-app .settingsBtn,.codex-chat-save{border:0;border-radius:8px;padding:10px;background:#236fc1;color:white;font-weight:700;cursor:pointer}.codex-chat-sessionList{min-height:0;display:grid;align-content:start;gap:5px;overflow-y:auto}.codex-chat-settingsBtn{width:100%;align-self:end}.codex-chat-session{background:transparent;border:0;border-radius:7px;padding:9px;text-align:left;color:#bfcede;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.codex-chat-app .session small{display:block;color:#7189a2;margin-top:3px;font-size:10px}.codex-chat-activeSession{background:#173859;color:#fff}.codex-chat-settingsBtn{margin-top:auto;background:#142943;color:#cce1f7}.codex-chat-shell{height:100%;min-height:0;overflow:hidden;min-width:0;display:grid;grid-template-rows:auto minmax(0,1fr) auto;background:#0d1a2b}.codex-chat-app header{padding:14px 18px;border-bottom:1px solid #20344e;display:flex;align-items:center;justify-content:space-between}.codex-chat-app header b{display:block;font-size:16px}.codex-chat-app header span{font-size:12px;color:#9db1c8}.codex-chat-headerActions{display:flex;align-items:center;gap:8px;flex:0 0 auto}.codex-chat-app .modelBtn,.codex-chat-sessionsBtn{border:1px solid #35658f;border-radius:7px;background:#122b45;color:#dceeff;padding:7px 10px;font:700 12px ui-sans-serif,system-ui;cursor:pointer;white-space:nowrap}.codex-chat-sessionsBtn{display:none}.codex-chat-sessionRow{display:flex;gap:4px;min-width:0}.codex-chat-app .sessionRow .codex-chat-session{flex:1;min-width:0}.codex-chat-sessionActions{display:flex;align-items:center}.codex-chat-app .sessionActions button{border:0;background:transparent;color:#9db1c8;padding:5px;cursor:pointer}.codex-chat-sessionDrawer{display:none}.codex-chat-status{color:#5ee0a1!important}.codex-chat-messages{overflow:auto;padding:20px;display:flex;flex-direction:column;gap:14px}.codex-chat-welcome{margin:auto;max-width:530px;text-align:center;color:#b6c8db}.codex-chat-app .welcome h1{color:#fff;font-size:26px}.codex-chat-app .welcome code,.codex-chat-app pre{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.codex-chat-app article{max-width:92%;padding:12px 14px;border-radius:12px;border:1px solid #29415e}.codex-chat-user{align-self:flex-end;background:#15477f}.codex-chat-assistant{align-self:flex-start;background:#13243a}.codex-chat-app label{display:block;margin-bottom:8px;color:#9db1c8;font-size:12px}.codex-chat-app pre{white-space:pre-wrap;word-break:break-word;margin:0;line-height:1.5;font-size:13px}.codex-chat-markdown{color:#edf4ff;font-size:15px;line-height:1.68;overflow-wrap:anywhere}.codex-chat-app .markdown>*:first-child{margin-top:0}.codex-chat-app .markdown>*:last-child{margin-bottom:0}.codex-chat-app .markdown h1,.codex-chat-app .markdown h2,.codex-chat-app .markdown h3{color:#fff;line-height:1.25;margin:1.2em 0 .45em}.codex-chat-app .markdown h1{font-size:1.35em}.codex-chat-app .markdown h2{font-size:1.18em}.codex-chat-app .markdown h3{font-size:1.05em}.codex-chat-app .markdown p{margin:.65em 0}.codex-chat-app .markdown ul,.codex-chat-app .markdown ol{padding-left:1.35em}.codex-chat-app .markdown strong{color:#fff}.codex-chat-app .markdown code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.88em;background:#091523;border:1px solid #2b4666;padding:.12em .32em;border-radius:4px}.codex-chat-app .markdown pre{background:#07111d;border:1px solid #2b4666;border-radius:8px;padding:12px;overflow:auto}.codex-chat-progress{border-color:#3b618d}.codex-chat-app .progress ol{list-style:none;padding:0;margin:0;display:grid;gap:8px}.codex-chat-app .progress li{color:#91a9c2;font-size:13px}.codex-chat-app .progress li:before{content:'✓';display:inline-grid;place-items:center;width:16px;height:16px;margin-right:8px;border-radius:50%;background:#274761;color:#b9d6ee;font-size:11px}.codex-chat-app .progress li.active{color:#eaf5ff}.codex-chat-app .progress li.active:before{content:'…';background:#2d86d1;color:white;animation:pulse 1.2s infinite}@keyframes pulse{50%{opacity:.5}}.codex-chat-composer{border-top:1px solid #20344e;background:#0b1727}.codex-chat-modelFooter{padding:5px 14px 0;color:#89a3be;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.codex-chat-systemMessage{align-self:center;max-width:90%;padding:6px 10px;border-radius:999px;background:#172d45;color:#9fc4e5;font-size:12px}.codex-chat-app form{display:flex;gap:10px;padding:10px 14px 14px;background:#0b1727}.codex-chat-app textarea{resize:vertical;flex:1;border:1px solid #2b4666;background:#07111d;color:#fff;border-radius:8px;padding:10px;font:inherit}.codex-chat-app form button{border:0;border-radius:8px;padding:0 18px;background:#3b89dc;color:white;font-weight:700;cursor:pointer}.codex-chat-app form button:disabled{background:#39516b;color:#a5b5c5;cursor:wait}.codex-chat-modal{position:fixed;inset:0;display:grid;place-items:center;background:#000a;padding:16px;overflow:auto}.codex-chat-settings{width:min(460px,calc(100vw - 32px));max-height:calc(100dvh - 32px);min-width:0;overflow-y:auto;border:1px solid #2b4666;background:#102035;border-radius:12px;padding:16px}.codex-chat-app .settings header{padding:0 0 12px;margin-bottom:12px}.codex-chat-app .settings header button{background:transparent;color:#fff;border:0;font-size:24px}.codex-chat-app .settings label{display:grid;gap:6px;margin:12px 0;min-width:0}.codex-chat-app .settings input,.codex-chat-app .settings select{width:100%;max-width:100%;min-width:0;background:#08121f;color:#fff;border:1px solid #2b4666;border-radius:7px;padding:9px}.codex-chat-app .settings select{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.codex-chat-app .settings p{font-size:12px;color:#9db1c8}.codex-chat-catalogStatus{display:block;color:#89a3be;font-size:11px;margin-top:4px}@media(max-width:640px){.codex-chat-app{grid-template-columns:1fr}.codex-chat-sidebar{display:none}.codex-chat-sessionsBtn{display:inline-flex}.codex-chat-status{display:none}.codex-chat-messages{padding:12px}.codex-chat-app article{max-width:96%}.codex-chat-modal{align-items:end;padding:0}.codex-chat-settings{width:100%;max-height:82dvh;border-radius:18px 18px 0 0;border-bottom:0;padding:18px 16px 28px}.codex-chat-sessionDrawer{display:block;position:fixed;inset:0;background:#000a;z-index:20}.codex-chat-sessionDrawer section{position:absolute;inset:0 auto 0 0;width:min(320px,86vw);padding:16px;background:#0a1625;border-right:1px solid #29415e;display:grid;grid-template-rows:auto auto minmax(0,1fr);gap:12px}.codex-chat-sessionDrawer header{padding:0 0 12px}.codex-chat-sessionDrawer header button{background:transparent;color:#fff;border:0;font-size:24px}}
|
|
3
|
+
|