rails-llm 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 39eedacae9a3c2f4d9a66b014aa94311596fff997382d8bfc82ef078e181abd0
4
- data.tar.gz: 0dee69794bf8e1100ea3a708844e1563b366249f5d156f46a678abdb50a54124
3
+ metadata.gz: efa61c893aa2e1e5c832f40e5bcb8ce69d8b50818da940b8f6de356e3548f682
4
+ data.tar.gz: 0d953138a349b41eaa8280e725a61952af83ec61b1012440e086dafae7ed113e
5
5
  SHA512:
6
- metadata.gz: 93d1d518fdb7fdb1ba226db963bf0c5bb818e35cdfff114e8a08312ccbf618e9f2e27bf5a29c16fb7a51114d25f75d9681e3b395d86819853d5165f1acaa3f2d
7
- data.tar.gz: c4f18a1b658f776822558e135822f837f19a9977d6ac7a5e5e6db535dcf34cecc4952febc62222a04ad57019bd8335b38e733edf5771c3793d160fc99638b991
6
+ metadata.gz: 0525225b76f8b987c1bd3d1cb2bfe7874f7ee6a5f388508866dd1ad79879828e906af6f5b173d2758e637cd52f6c8e8a1f21277b8ef156af6262d455b3a571f5
7
+ data.tar.gz: 34f65efc1bc37e9a1156e6070b4c329ddbe1c37566ee1f8ebb3ffbc5c1a223d80a67af175b86c065a957edfdc25d9996e4993540e0ee397cd22a8b604875c03d
data/Gemfile CHANGED
@@ -1,4 +1,3 @@
1
1
  source "https://rubygems.org"
2
2
 
3
3
  gemspec
4
- gem "llm.rb", github: "llmrb/llm.rb"
data/README.md CHANGED
@@ -73,7 +73,7 @@ export DEEPSEEK_API_KEY=...
73
73
 
74
74
  **5. Profit**
75
75
 
76
- Open your browser:
76
+ Open your browser:
77
77
 
78
78
  ```bash
79
79
  open http://localhost:3000/ai/agents
@@ -122,6 +122,10 @@ agent.ask("Tell me a story") { |chunk| print chunk } # streaming
122
122
  | POST | `/ai/agents` | Create a new agent |
123
123
  | POST | `/ai/agents/:id/ask` | Send a message |)
124
124
 
125
+ #### Screenshot
126
+
127
+ ![screenshot](screenshot.png)
128
+
125
129
  ## License
126
130
 
127
131
  [BSD Zero Clause](LICENSE)
@@ -1,9 +1,17 @@
1
1
  ;(function() {
2
2
  const View = (messages) => {
3
- const append = (role, content) => {
3
+ const append = (role, label, content) => {
4
4
  const node = document.createElement("div")
5
5
  node.className = `message ${role}`
6
- node.textContent = content
6
+ const labelDiv = document.createElement("div")
7
+ labelDiv.className = "role-label"
8
+ labelDiv.textContent = `${label}:`
9
+ node.appendChild(labelDiv)
10
+ if (content) {
11
+ const contentDiv = document.createElement("div")
12
+ contentDiv.textContent = content
13
+ node.appendChild(contentDiv)
14
+ }
7
15
  messages.appendChild(node)
8
16
  messages.scrollTop = messages.scrollHeight
9
17
  return node
@@ -28,11 +36,31 @@
28
36
  messages,
29
37
  appendToolCall,
30
38
  appendAssistant() {
31
- const node = append("assistant", "")
39
+ const node = document.createElement("div")
40
+ node.className = "message assistant"
41
+ const labelDiv = document.createElement("div")
42
+ labelDiv.className = "role-label"
43
+ labelDiv.textContent = "Robot:"
44
+ node.appendChild(labelDiv)
45
+ const contentDiv = document.createElement("div")
46
+ node.appendChild(contentDiv)
47
+ messages.appendChild(node)
48
+ messages.scrollTop = messages.scrollHeight
32
49
  return node
33
50
  },
34
51
  appendUser(content) {
35
- return append("user", content)
52
+ const node = document.createElement("div")
53
+ node.className = "message user"
54
+ const labelDiv = document.createElement("div")
55
+ labelDiv.className = "role-label"
56
+ labelDiv.textContent = "You:"
57
+ node.appendChild(labelDiv)
58
+ const contentDiv = document.createElement("div")
59
+ contentDiv.textContent = content
60
+ node.appendChild(contentDiv)
61
+ messages.appendChild(node)
62
+ messages.scrollTop = messages.scrollHeight
63
+ return node
36
64
  },
37
65
  clearEmptyState() {
38
66
  const emptyState = messages.querySelector(".empty-state")
@@ -46,6 +74,16 @@
46
74
  },
47
75
  hideCursor(node) {
48
76
  node.classList.remove("streaming-cursor")
77
+ },
78
+ updateCounters(totalTokens, messageCount) {
79
+ const tokenEl = document.getElementById("token-count")
80
+ const msgEl = document.getElementById("message-count")
81
+ if (tokenEl && totalTokens != null) {
82
+ tokenEl.textContent = `${totalTokens} tokens`
83
+ }
84
+ if (msgEl && messageCount != null) {
85
+ msgEl.textContent = `${messageCount} messages`
86
+ }
49
87
  }
50
88
  }
51
89
  }
@@ -89,10 +127,13 @@
89
127
  const onEvent = (event) => {
90
128
  if (event.type == "done") {
91
129
  state.done = true
130
+ view.updateCounters(event.total_tokens, event.message_count)
92
131
  return
93
132
  }
94
133
  if (event.type == "content") {
95
- ensureAssistantNode().innerHTML = event.content || ""
134
+ const node = ensureAssistantNode()
135
+ const contentDiv = node.querySelector(":scope > div:last-child")
136
+ if (contentDiv) contentDiv.innerHTML = event.content || ""
96
137
  view.focusBottom()
97
138
  return
98
139
  }
@@ -0,0 +1,642 @@
1
+ /*
2
+ * rails-llm chat interface stylesheet
3
+ *
4
+ * Terminal-core design: dark, monospace, high contrast.
5
+ * Inspired by the REPL — messages read like command and output.
6
+ */
7
+
8
+ /* ==========================================================================
9
+ Design tokens
10
+ ========================================================================== */
11
+
12
+ :root {
13
+ --color-bg: #000000;
14
+ --color-surface: #000000;
15
+ --color-sidebar: #0d0d0d;
16
+ --color-sidebar-hover: #1a1a1a;
17
+ --color-sidebar-text: #555555;
18
+ --color-sidebar-text-active: #e5e5e5;
19
+ --color-accent: #22c55e;
20
+ --color-accent-dim: rgba(34, 197, 94, 0.12);
21
+ --color-accent-hover: #4ade80;
22
+ --color-text: #e5e5e5;
23
+ --color-text-secondary: #888888;
24
+ --color-text-muted: #444444;
25
+ --color-border: #1a1a1a;
26
+ --color-prompt: #22c55e;
27
+ --color-output: #e5e5e5;
28
+ --color-code-bg: #0a0a0a;
29
+ --color-code-text: #e5e5e5;
30
+ --color-reasoning-text: #666666;
31
+ --color-tool-text: #666666;
32
+ --color-runtime-bg: #0a0a0a;
33
+ --font-mono: "SF Mono", "Fira Code", "Fira Mono", "JetBrains Mono",
34
+ "Cascadia Code", "Menlo", "Consolas", monospace;
35
+ --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
36
+ "Helvetica Neue", Arial, sans-serif;
37
+ --sidebar-width: 260px;
38
+ --transition-fast: 0.15s ease;
39
+ }
40
+
41
+ /* ==========================================================================
42
+ Reset
43
+ ========================================================================== */
44
+
45
+ *, *::before, *::after {
46
+ box-sizing: border-box;
47
+ margin: 0;
48
+ padding: 0;
49
+ }
50
+
51
+ body {
52
+ font-family: var(--font-ui);
53
+ background: var(--color-bg);
54
+ color: var(--color-text);
55
+ height: 100vh;
56
+ }
57
+
58
+ /* ==========================================================================
59
+ Layout
60
+ ========================================================================== */
61
+
62
+ .layout {
63
+ display: flex;
64
+ height: 100vh;
65
+ }
66
+
67
+ /* ==========================================================================
68
+ Sidebar
69
+ ========================================================================== */
70
+
71
+ .sidebar {
72
+ width: var(--sidebar-width);
73
+ background: var(--color-sidebar);
74
+ color: var(--color-sidebar-text);
75
+ padding: 20px 16px 12px;
76
+ display: flex;
77
+ flex-direction: column;
78
+ gap: 6px;
79
+ flex-shrink: 0;
80
+ border-right: 1px solid var(--color-border);
81
+ }
82
+
83
+ .sidebar .brand {
84
+ margin-bottom: 24px;
85
+ padding-bottom: 16px;
86
+ border-bottom: 1px solid var(--color-border);
87
+ }
88
+
89
+ .sidebar .brand .product-sub {
90
+ font-size: 10px;
91
+ color: var(--color-accent);
92
+ font-weight: 500;
93
+ font-family: var(--font-mono);
94
+ letter-spacing: 0.5px;
95
+ }
96
+
97
+ .sidebar .new-agent {
98
+ display: flex;
99
+ align-items: center;
100
+ gap: 8px;
101
+ background: transparent;
102
+ color: var(--color-sidebar-text);
103
+ border: 1px solid var(--color-border);
104
+ padding: 8px 12px;
105
+ font-size: 12px;
106
+ font-family: var(--font-mono);
107
+ text-decoration: none;
108
+ cursor: pointer;
109
+ transition: all var(--transition-fast);
110
+ margin-bottom: 8px;
111
+ }
112
+
113
+ .sidebar .new-agent:hover {
114
+ background: var(--color-sidebar-hover);
115
+ border-color: var(--color-accent);
116
+ color: var(--color-sidebar-text-active);
117
+ }
118
+
119
+ .sidebar .new-agent .plus {
120
+ font-size: 14px;
121
+ font-weight: 300;
122
+ line-height: 1;
123
+ }
124
+
125
+ .sidebar nav {
126
+ flex: 1;
127
+ overflow-y: auto;
128
+ display: flex;
129
+ flex-direction: column;
130
+ gap: 2px;
131
+ }
132
+
133
+ .sidebar .agent-link {
134
+ display: block;
135
+ padding: 6px 12px;
136
+ color: var(--color-sidebar-text);
137
+ text-decoration: none;
138
+ font-size: 12px;
139
+ font-family: var(--font-mono);
140
+ transition: all var(--transition-fast);
141
+ overflow: hidden;
142
+ text-overflow: ellipsis;
143
+ white-space: nowrap;
144
+ line-height: 1.5;
145
+ border-left: 2px solid transparent;
146
+ }
147
+
148
+ .sidebar .agent-link:hover {
149
+ color: var(--color-sidebar-text-active);
150
+ }
151
+
152
+ .sidebar .agent-link.active {
153
+ color: var(--color-sidebar-text-active);
154
+ border-left-color: var(--color-accent);
155
+ padding-left: 10px;
156
+ }
157
+
158
+ .sidebar .powered-by {
159
+ display: flex;
160
+ align-items: center;
161
+ justify-content: center;
162
+ gap: 8px;
163
+ padding: 8px 12px;
164
+ margin-top: 8px;
165
+ border-top: 1px solid var(--color-border);
166
+ font-size: 11px;
167
+ font-family: var(--font-mono);
168
+ color: var(--color-text-secondary);
169
+ text-decoration: none;
170
+ transition: all var(--transition-fast);
171
+ }
172
+
173
+ .sidebar .powered-by::before {
174
+ content: "";
175
+ width: 6px;
176
+ height: 6px;
177
+ background: var(--color-accent);
178
+ flex-shrink: 0;
179
+ }
180
+
181
+ .sidebar .powered-by:hover {
182
+ color: var(--color-accent);
183
+ }
184
+
185
+ .sidebar .powered-by:hover::before {
186
+ background: var(--color-accent-hover);
187
+ }
188
+
189
+ /* ==========================================================================
190
+ Main content area
191
+ ========================================================================== */
192
+
193
+ .main {
194
+ flex: 1;
195
+ display: flex;
196
+ flex-direction: column;
197
+ min-width: 0;
198
+ background: var(--color-bg);
199
+ }
200
+
201
+ /* ==========================================================================
202
+ Messages container
203
+ ========================================================================== */
204
+
205
+ .messages {
206
+ flex: 1;
207
+ overflow-y: auto;
208
+ padding: 24px 32px;
209
+ display: flex;
210
+ flex-direction: column;
211
+ gap: 20px;
212
+ scroll-behavior: smooth;
213
+ }
214
+
215
+ /* ==========================================================================
216
+ Individual messages (REPL style)
217
+ ========================================================================== */
218
+
219
+ .message {
220
+ max-width: 800px;
221
+ width: 100%;
222
+ line-height: 1.6;
223
+ font-size: 14px;
224
+ font-family: var(--font-mono);
225
+ color: var(--color-output);
226
+ animation: messageFadeIn 0.15s ease;
227
+ }
228
+
229
+ @keyframes messageFadeIn {
230
+ from { opacity: 0; }
231
+ to { opacity: 1; }
232
+ }
233
+
234
+ .message .role-label {
235
+ font-size: 13px;
236
+ font-weight: 600;
237
+ margin-bottom: 2px;
238
+ user-select: none;
239
+ }
240
+
241
+ .message.user .role-label {
242
+ color: var(--color-accent);
243
+ }
244
+
245
+ .message.assistant .role-label {
246
+ color: var(--color-text-muted);
247
+ }
248
+
249
+ /* ==========================================================================
250
+ Message content (markdown rendering)
251
+ ========================================================================== */
252
+
253
+ .message.assistant > :first-child { margin-top: 0; }
254
+ .message.assistant > :last-child { margin-bottom: 0; }
255
+
256
+ .message.assistant p {
257
+ line-height: 1.6;
258
+ }
259
+
260
+ .message.assistant p + p,
261
+ .message.assistant ul,
262
+ .message.assistant ol,
263
+ .message.assistant pre,
264
+ .message.assistant blockquote,
265
+ .message.assistant table {
266
+ margin-top: 10px;
267
+ }
268
+
269
+ .message.assistant h1,
270
+ .message.assistant h2,
271
+ .message.assistant h3,
272
+ .message.assistant h4,
273
+ .message.assistant h5,
274
+ .message.assistant h6 {
275
+ line-height: 1.3;
276
+ color: var(--color-text);
277
+ margin: 14px 0 6px;
278
+ font-weight: 600;
279
+ }
280
+
281
+ .message.assistant h1,
282
+ .message.assistant h2 { font-size: 16px; }
283
+ .message.assistant h3 { font-size: 15px; }
284
+ .message.assistant h4,
285
+ .message.assistant h5,
286
+ .message.assistant h6 { font-size: 14px; }
287
+
288
+ .message.assistant h1:first-child,
289
+ .message.assistant h2:first-child,
290
+ .message.assistant h3:first-child {
291
+ margin-top: 0;
292
+ }
293
+
294
+ .message.assistant ul,
295
+ .message.assistant ol {
296
+ padding-left: 24px;
297
+ }
298
+
299
+ .message.assistant li + li {
300
+ margin-top: 3px;
301
+ }
302
+
303
+ .message.assistant li p {
304
+ margin: 0;
305
+ }
306
+
307
+ .message.assistant a {
308
+ color: var(--color-accent);
309
+ text-decoration: underline;
310
+ text-underline-offset: 2px;
311
+ }
312
+
313
+ .message.assistant a:hover {
314
+ color: var(--color-accent-hover);
315
+ }
316
+
317
+ .message.assistant code {
318
+ font-family: var(--font-mono);
319
+ font-size: 13px;
320
+ color: var(--color-accent);
321
+ word-break: break-word;
322
+ }
323
+
324
+ .message.assistant pre {
325
+ overflow-x: auto;
326
+ padding: 12px 14px;
327
+ background: var(--color-code-bg);
328
+ color: var(--color-code-text);
329
+ font-size: 13px;
330
+ line-height: 1.5;
331
+ border: 1px solid var(--color-border);
332
+ }
333
+
334
+ .message.assistant pre code {
335
+ display: block;
336
+ background: transparent;
337
+ color: inherit;
338
+ padding: 0;
339
+ white-space: pre;
340
+ tab-size: 2;
341
+ word-break: normal;
342
+ }
343
+
344
+ /* Syntax highlighting tokens */
345
+ .message.assistant pre code .c,
346
+ .message.assistant pre code .c1,
347
+ .message.assistant pre code .cm { color: #666666; }
348
+
349
+ .message.assistant pre code .k,
350
+ .message.assistant pre code .kd,
351
+ .message.assistant pre code .kn,
352
+ .message.assistant pre code .kp { color: #f472b6; }
353
+
354
+ .message.assistant pre code .s,
355
+ .message.assistant pre code .s1,
356
+ .message.assistant pre code .s2 { color: #86efac; }
357
+
358
+ .message.assistant pre code .nf,
359
+ .message.assistant pre code .nc,
360
+ .message.assistant pre code .no { color: #7dd3fc; }
361
+
362
+ .message.assistant pre code .mi,
363
+ .message.assistant pre code .mf { color: #fdba74; }
364
+
365
+ .message.assistant pre code .na { color: #a78bfa; }
366
+
367
+ .message.assistant pre code .nb { color: #f5f5f4; }
368
+
369
+ .message.assistant blockquote {
370
+ border-left: 2px solid var(--color-accent);
371
+ padding-left: 12px;
372
+ color: var(--color-text-secondary);
373
+ }
374
+
375
+ .message.assistant table {
376
+ width: 100%;
377
+ border-collapse: collapse;
378
+ font-size: 13px;
379
+ overflow-x: auto;
380
+ display: block;
381
+ }
382
+
383
+ .message.assistant th,
384
+ .message.assistant td {
385
+ border: 1px solid var(--color-border);
386
+ padding: 6px 10px;
387
+ text-align: left;
388
+ vertical-align: top;
389
+ }
390
+
391
+ .message.assistant th {
392
+ font-weight: 600;
393
+ }
394
+
395
+ .message.assistant img {
396
+ max-width: 100%;
397
+ height: auto;
398
+ margin: 8px 0;
399
+ }
400
+
401
+ .message.assistant hr {
402
+ border: none;
403
+ border-top: 1px solid var(--color-border);
404
+ margin: 16px 0;
405
+ }
406
+
407
+ /* ==========================================================================
408
+ Reasoning panel
409
+ ========================================================================== */
410
+
411
+ .reasoning {
412
+ max-width: 800px;
413
+ width: 100%;
414
+ padding: 0 0 0 16px;
415
+ margin-top: -12px;
416
+ font-size: 13px;
417
+ font-family: var(--font-mono);
418
+ color: var(--color-reasoning-text);
419
+ }
420
+
421
+ .reasoning summary {
422
+ cursor: pointer;
423
+ font-weight: 500;
424
+ color: var(--color-reasoning-text);
425
+ user-select: none;
426
+ font-size: 11px;
427
+ }
428
+
429
+ .reasoning summary:hover {
430
+ color: var(--color-text-secondary);
431
+ }
432
+
433
+ .reasoning .content {
434
+ margin-top: 6px;
435
+ font-size: 13px;
436
+ line-height: 1.5;
437
+ color: var(--color-reasoning-text);
438
+ white-space: pre-wrap;
439
+ }
440
+
441
+ /* ==========================================================================
442
+ Tool call panels
443
+ ========================================================================== */
444
+
445
+ .tool-call {
446
+ max-width: 800px;
447
+ width: 100%;
448
+ padding: 0 0 0 16px;
449
+ margin-top: -12px;
450
+ font-size: 13px;
451
+ font-family: var(--font-mono);
452
+ color: var(--color-tool-text);
453
+ }
454
+
455
+ .tool-call summary {
456
+ cursor: pointer;
457
+ font-weight: 500;
458
+ color: var(--color-tool-text);
459
+ user-select: none;
460
+ font-size: 11px;
461
+ }
462
+
463
+ .tool-call summary:hover {
464
+ color: var(--color-text-secondary);
465
+ }
466
+
467
+ .tool-call .tool-name {
468
+ color: var(--color-accent);
469
+ font-weight: 600;
470
+ font-size: 12px;
471
+ }
472
+
473
+ .tool-call pre {
474
+ margin-top: 6px;
475
+ background: var(--color-code-bg);
476
+ color: var(--color-code-text);
477
+ padding: 10px 12px;
478
+ font-size: 12px;
479
+ overflow-x: auto;
480
+ font-family: var(--font-mono);
481
+ border: 1px solid var(--color-border);
482
+ }
483
+
484
+ /* ==========================================================================
485
+ Input area
486
+ ========================================================================== */
487
+
488
+ .input-area {
489
+ border-top: 1px solid var(--color-border);
490
+ padding: 12px 32px;
491
+ background: var(--color-bg);
492
+ }
493
+
494
+ .input-area form {
495
+ display: flex;
496
+ gap: 0;
497
+ max-width: 800px;
498
+ margin: 0 auto;
499
+ align-items: stretch;
500
+ }
501
+
502
+ .input-area .prompt {
503
+ display: flex;
504
+ align-items: center;
505
+ color: var(--color-prompt);
506
+ font-family: var(--font-mono);
507
+ font-size: 14px;
508
+ padding-right: 10px;
509
+ user-select: none;
510
+ flex-shrink: 0;
511
+ }
512
+
513
+ .input-area input[type="text"] {
514
+ flex: 1;
515
+ padding: 8px 0;
516
+ border: none;
517
+ font-size: 14px;
518
+ font-family: var(--font-mono);
519
+ outline: none;
520
+ background: transparent;
521
+ color: var(--color-text);
522
+ }
523
+
524
+ .input-area input[type="text"]::placeholder {
525
+ color: var(--color-text-muted);
526
+ }
527
+
528
+ .input-area button,
529
+ .input-area input[type="submit"] {
530
+ background: transparent;
531
+ color: var(--color-accent);
532
+ border: 1px solid var(--color-accent);
533
+ padding: 8px 16px;
534
+ font-size: 12px;
535
+ font-family: var(--font-mono);
536
+ cursor: pointer;
537
+ transition: all var(--transition-fast);
538
+ flex-shrink: 0;
539
+ }
540
+
541
+ .input-area button:hover,
542
+ .input-area input[type="submit"]:hover {
543
+ background: var(--color-accent-dim);
544
+ }
545
+
546
+ .input-area button:disabled,
547
+ .input-area input[type="submit"]:disabled {
548
+ opacity: 0.3;
549
+ cursor: not-allowed;
550
+ }
551
+
552
+ /* ==========================================================================
553
+ Empty state
554
+ ========================================================================== */
555
+
556
+ .empty-state {
557
+ flex: 1;
558
+ display: flex;
559
+ flex-direction: column;
560
+ align-items: center;
561
+ justify-content: center;
562
+ gap: 12px;
563
+ color: #ffffff;
564
+ text-align: center;
565
+ padding: 40px 20px;
566
+ font-family: var(--font-mono);
567
+ font-size: 13px;
568
+ }
569
+
570
+ .empty-state .icon {
571
+ font-size: 32px;
572
+ opacity: 0.5;
573
+ }
574
+
575
+ .empty-state p {
576
+ font-size: 13px;
577
+ max-width: 300px;
578
+ line-height: 1.5;
579
+ }
580
+
581
+ /* ==========================================================================
582
+ Runtime bar
583
+ ========================================================================== */
584
+
585
+ .runtime-bar {
586
+ display: flex;
587
+ align-items: center;
588
+ gap: 16px;
589
+ padding: 4px 32px;
590
+ background: var(--color-runtime-bg);
591
+ border-top: 1px solid var(--color-border);
592
+ font-size: 10px;
593
+ color: var(--color-text-muted);
594
+ font-family: var(--font-mono);
595
+ }
596
+
597
+ .runtime-bar .dot {
598
+ width: 5px;
599
+ height: 5px;
600
+ border-radius: 0;
601
+ background: var(--color-accent);
602
+ display: inline-block;
603
+ }
604
+
605
+ .runtime-bar .dot.idle {
606
+ background: var(--color-text-muted);
607
+ }
608
+
609
+ .runtime-bar span {
610
+ white-space: nowrap;
611
+ }
612
+
613
+ /* ==========================================================================
614
+ Typewriter cursor
615
+ ========================================================================== */
616
+
617
+ .streaming-cursor::after {
618
+ content: "";
619
+ animation: typewriter 0.6s step-end infinite;
620
+ display: inline-block;
621
+ width: 1px;
622
+ height: 1em;
623
+ background: var(--color-accent);
624
+ margin-left: 2px;
625
+ vertical-align: text-bottom;
626
+ }
627
+
628
+ @keyframes typewriter {
629
+ 0%, 100% { opacity: 1; }
630
+ 50% { opacity: 0; }
631
+ }
632
+
633
+ /* ==========================================================================
634
+ Responsive
635
+ ========================================================================== */
636
+
637
+ @media (max-width: 768px) {
638
+ .sidebar { display: none; }
639
+ .messages { padding: 16px; }
640
+ .input-area { padding: 12px 16px; }
641
+ .message { max-width: 100%; font-size: 13px; }
642
+ }
@@ -32,7 +32,10 @@ module RailsLLM
32
32
  response.headers["X-Accel-Buffering"] = "no"
33
33
  stream = Stream.new(response.stream)
34
34
  @agent.ask(prompt, stream:)
35
- stream.finish
35
+ stream.finish(
36
+ total_tokens: @agent.usage.total_tokens,
37
+ message_count: messages.size
38
+ )
36
39
  ensure
37
40
  response.stream.close
38
41
  end
@@ -1,398 +1,25 @@
1
1
  <!DOCTYPE html>
2
2
  <html>
3
3
  <head>
4
- <title>llm.rb Agents</title>
4
+ <title>rails-llm</title>
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
6
  <%= csrf_meta_tags %>
7
7
  <%= csp_meta_tag %>
8
+ <%= stylesheet_link_tag "rails_llm/application", media: "all" %>
8
9
  <%= javascript_include_tag "rails_llm/application", defer: true %>
9
10
  <%= yield :head %>
10
- <style>
11
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
12
- body {
13
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
14
- Roboto, "Helvetica Neue", Arial, sans-serif;
15
- background: #f8f9fa;
16
- color: #1a1a2e;
17
- height: 100vh;
18
- }
19
- .layout { display: flex; height: 100vh; }
20
- .sidebar {
21
- width: 260px;
22
- background: #1a1a2e;
23
- color: #c8d6e5;
24
- padding: 20px 16px;
25
- display: flex;
26
- flex-direction: column;
27
- gap: 6px;
28
- }
29
- .sidebar .brand {
30
- display: flex;
31
- align-items: center;
32
- gap: 10px;
33
- margin-bottom: 20px;
34
- padding-bottom: 16px;
35
- border-bottom: 1px solid #2d2d4a;
36
- }
37
- .sidebar .brand img {
38
- width: 32px; height: 32px;
39
- border-radius: 6px;
40
- }
41
- .sidebar .brand h1 {
42
- font-size: 16px;
43
- font-weight: 600;
44
- color: #fff;
45
- }
46
- .sidebar .brand span {
47
- font-size: 11px;
48
- color: #00d68f;
49
- font-weight: 500;
50
- text-transform: uppercase;
51
- letter-spacing: 0.5px;
52
- }
53
- .sidebar .new-agent {
54
- background: transparent;
55
- color: #c8d6e5;
56
- border: 1px dashed #2d2d4a;
57
- border-radius: 8px;
58
- padding: 10px;
59
- text-align: center;
60
- font-size: 13px;
61
- font-weight: 500;
62
- text-decoration: none;
63
- transition: all 0.15s;
64
- margin-bottom: 12px;
65
- }
66
- .sidebar .new-agent:hover {
67
- background: #2d2d4a;
68
- border-color: #00d68f;
69
- color: #fff;
70
- }
71
- .sidebar .agent-link {
72
- display: block;
73
- padding: 8px 12px;
74
- border-radius: 6px;
75
- color: #8899b4;
76
- text-decoration: none;
77
- font-size: 13px;
78
- transition: all 0.12s;
79
- overflow: hidden;
80
- text-overflow: ellipsis;
81
- white-space: nowrap;
82
- }
83
- .sidebar .agent-link:hover {
84
- background: #2d2d4a;
85
- color: #e0e6ed;
86
- }
87
- .sidebar .agent-link.active {
88
- background: #2d2d4a;
89
- color: #00d68f;
90
- font-weight: 500;
91
- border-left: 3px solid #00d68f;
92
- }
93
- .main {
94
- flex: 1;
95
- display: flex;
96
- flex-direction: column;
97
- min-width: 0;
98
- }
99
- .messages {
100
- flex: 1;
101
- overflow-y: auto;
102
- padding: 32px 24px;
103
- display: flex;
104
- flex-direction: column;
105
- gap: 20px;
106
- scroll-behavior: smooth;
107
- }
108
- .message {
109
- max-width: 760px;
110
- padding: 14px 18px;
111
- border-radius: 12px;
112
- line-height: 1.6;
113
- font-size: 15px;
114
- animation: fadeIn 0.2s ease;
115
- }
116
- @keyframes fadeIn {
117
- from { opacity: 0; transform: translateY(8px); }
118
- to { opacity: 1; transform: translateY(0); }
119
- }
120
- .message.user {
121
- background: #1a1a2e;
122
- color: #fff;
123
- align-self: flex-end;
124
- border-bottom-right-radius: 4px;
125
- }
126
- .message.assistant {
127
- background: #fff;
128
- border: 1px solid #e8ecf1;
129
- align-self: flex-start;
130
- border-bottom-left-radius: 4px;
131
- box-shadow: 0 1px 3px rgba(0,0,0,0.04);
132
- }
133
- .message.assistant > :first-child { margin-top: 0; }
134
- .message.assistant > :last-child { margin-bottom: 0; }
135
- .message.assistant p + p,
136
- .message.assistant ul,
137
- .message.assistant ol,
138
- .message.assistant pre,
139
- .message.assistant blockquote,
140
- .message.assistant table { margin-top: 12px; }
141
- .message.assistant h1,
142
- .message.assistant h2,
143
- .message.assistant h3,
144
- .message.assistant h4,
145
- .message.assistant h5,
146
- .message.assistant h6 {
147
- line-height: 1.25;
148
- color: #101828;
149
- margin: 14px 0 8px;
150
- }
151
- .message.assistant h1,
152
- .message.assistant h2 { font-size: 17px; }
153
- .message.assistant h3 { font-size: 16px; }
154
- .message.assistant h4,
155
- .message.assistant h5,
156
- .message.assistant h6 { font-size: 15px; }
157
- .message.assistant ul,
158
- .message.assistant ol {
159
- padding-left: 22px;
160
- }
161
- .message.assistant li + li {
162
- margin-top: 4px;
163
- }
164
- .message.assistant a {
165
- color: #0f766e;
166
- text-decoration: underline;
167
- text-underline-offset: 2px;
168
- word-break: break-word;
169
- }
170
- .message.assistant code {
171
- font-family: "SF Mono", "Fira Code", monospace;
172
- font-size: 13px;
173
- background: #f3f5f7;
174
- color: #0f172a;
175
- padding: 2px 5px;
176
- border-radius: 5px;
177
- }
178
- .message.assistant pre {
179
- overflow-x: auto;
180
- padding: 12px 14px;
181
- border-radius: 8px;
182
- background: #0f172a;
183
- color: #e2e8f0;
184
- font-size: 13px;
185
- line-height: 1.55;
186
- }
187
- .message.assistant pre code {
188
- display: block;
189
- background: transparent;
190
- color: inherit;
191
- padding: 0;
192
- border-radius: 0;
193
- white-space: pre;
194
- tab-size: 2;
195
- }
196
- .message.assistant pre code[class*="language-"] .c,
197
- .message.assistant pre code[class*="language-"] .cm,
198
- .message.assistant pre code[class*="language-"] .c1 {
199
- color: #94a3b8;
200
- }
201
- .message.assistant pre code[class*="language-"] .k,
202
- .message.assistant pre code[class*="language-"] .kd,
203
- .message.assistant pre code[class*="language-"] .kn {
204
- color: #f472b6;
205
- }
206
- .message.assistant pre code[class*="language-"] .s,
207
- .message.assistant pre code[class*="language-"] .s1,
208
- .message.assistant pre code[class*="language-"] .s2 {
209
- color: #86efac;
210
- }
211
- .message.assistant pre code[class*="language-"] .nf,
212
- .message.assistant pre code[class*="language-"] .nc {
213
- color: #7dd3fc;
214
- }
215
- .message.assistant pre code[class*="language-"] .mi,
216
- .message.assistant pre code[class*="language-"] .mf {
217
- color: #fdba74;
218
- }
219
- .message.assistant blockquote {
220
- border-left: 3px solid #00d68f;
221
- padding-left: 12px;
222
- color: #52606d;
223
- }
224
- .message.assistant table {
225
- width: 100%;
226
- border-collapse: collapse;
227
- font-size: 14px;
228
- }
229
- .message.assistant th,
230
- .message.assistant td {
231
- border: 1px solid #e5e7eb;
232
- padding: 8px 10px;
233
- text-align: left;
234
- vertical-align: top;
235
- }
236
- .message.assistant th {
237
- background: #f8fafc;
238
- font-weight: 600;
239
- }
240
- .message.assistant img {
241
- max-width: 100%;
242
- height: auto;
243
- border-radius: 8px;
244
- }
245
- .reasoning {
246
- max-width: 760px;
247
- align-self: flex-start;
248
- font-size: 13px;
249
- color: #6b7a8f;
250
- background: #f0f2f5;
251
- border-radius: 8px;
252
- padding: 10px 14px;
253
- margin-top: -12px;
254
- border-left: 3px solid #00d68f;
255
- }
256
- .reasoning summary {
257
- cursor: pointer;
258
- font-weight: 500;
259
- color: #4a5a6f;
260
- user-select: none;
261
- }
262
- .reasoning .content {
263
- margin-top: 8px;
264
- font-size: 13px;
265
- line-height: 1.5;
266
- color: #5a6a7f;
267
- white-space: pre-wrap;
268
- }
269
- .tool-call {
270
- max-width: 760px;
271
- align-self: flex-start;
272
- font-size: 13px;
273
- background: #f8f9fc;
274
- border: 1px solid #e8ecf1;
275
- border-radius: 8px;
276
- padding: 10px 14px;
277
- margin-top: -12px;
278
- }
279
- .tool-call summary {
280
- cursor: pointer;
281
- font-weight: 500;
282
- color: #4a5a6f;
283
- user-select: none;
284
- }
285
- .tool-call .tool-name {
286
- color: #00d68f;
287
- font-weight: 600;
288
- font-family: "SF Mono", "Fira Code", "Fira Mono", monospace;
289
- font-size: 12px;
290
- }
291
- .tool-call pre {
292
- margin-top: 8px;
293
- background: #1a1a2e;
294
- color: #e0e6ed;
295
- padding: 10px 12px;
296
- border-radius: 6px;
297
- font-size: 12px;
298
- overflow-x: auto;
299
- font-family: "SF Mono", "Fira Code", monospace;
300
- }
301
- .input-area {
302
- border-top: 1px solid #e8ecf1;
303
- padding: 16px 24px;
304
- background: #fff;
305
- }
306
- .input-area form {
307
- display: flex;
308
- gap: 8px;
309
- max-width: 800px;
310
- margin: 0 auto;
311
- }
312
- .input-area input[type="text"] {
313
- flex: 1;
314
- padding: 12px 18px;
315
- border: 1px solid #e0e4e8;
316
- border-radius: 10px;
317
- font-size: 15px;
318
- outline: none;
319
- transition: border 0.15s, box-shadow 0.15s;
320
- }
321
- .input-area input[type="text"]:focus {
322
- border-color: #00d68f;
323
- box-shadow: 0 0 0 3px rgba(0,214,143,0.12);
324
- }
325
- .input-area button {
326
- background: #1a1a2e;
327
- color: #fff;
328
- border: none;
329
- border-radius: 10px;
330
- padding: 12px 24px;
331
- font-size: 15px;
332
- font-weight: 500;
333
- cursor: pointer;
334
- transition: background 0.15s;
335
- }
336
- .input-area button:hover { background: #2d2d4a; }
337
- .input-area button:disabled { opacity: 0.5; cursor: not-allowed; }
338
- .empty-state {
339
- flex: 1;
340
- display: flex;
341
- flex-direction: column;
342
- align-items: center;
343
- justify-content: center;
344
- gap: 12px;
345
- color: #8899b4;
346
- }
347
- .empty-state .icon { font-size: 48px; opacity: 0.4; }
348
- .empty-state p { font-size: 16px; }
349
- .runtime-bar {
350
- display: flex;
351
- align-items: center;
352
- gap: 16px;
353
- padding: 6px 24px;
354
- background: #f0f2f5;
355
- border-top: 1px solid #e0e4e8;
356
- font-size: 11px;
357
- color: #8899b4;
358
- font-family: "SF Mono", "Fira Code", monospace;
359
- }
360
- .runtime-bar .dot {
361
- width: 6px; height: 6px;
362
- border-radius: 50%;
363
- background: #00d68f;
364
- display: inline-block;
365
- }
366
- .runtime-bar .dot.idle { background: #8899b4; }
367
- .runtime-bar span { white-space: nowrap; }
368
- .streaming-cursor::after {
369
- content: "▊";
370
- animation: blink 0.8s step-end infinite;
371
- color: #1a1a2e;
372
- margin-left: 2px;
373
- }
374
- @keyframes blink {
375
- 50% { opacity: 0; }
376
- }
377
- @media (max-width: 768px) {
378
- .sidebar { display: none; }
379
- .messages { padding: 16px; }
380
- .input-area { padding: 12px 16px; }
381
- }
382
- </style>
383
11
  </head>
384
12
  <body>
385
13
  <div class="layout">
386
14
  <div class="sidebar">
387
15
  <div class="brand">
388
- <%= image_tag "llm.png", alt: "llm.rb" %>
389
- <div>
390
- <h1>llm.rb</h1>
391
- <span>Agents</span>
392
- </div>
16
+ <div class="product-sub">AGENTS</div>
393
17
  </div>
394
- <%= link_to "+ New Agent", agents_path, method: :post,
395
- class: "new-agent" %>
18
+ <%= link_to agents_path, method: :post,
19
+ class: "new-agent" do %>
20
+ <span class="plus">+</span>
21
+ <span>New Agent</span>
22
+ <% end %>
396
23
  <nav>
397
24
  <% if @agents %>
398
25
  <% @agents.each do |agent| %>
@@ -401,6 +28,10 @@
401
28
  <% end %>
402
29
  <% end %>
403
30
  </nav>
31
+ <a href="https://github.com/llmrb/llm.rb#readme"
32
+ class="powered-by" target="_blank" rel="noopener">
33
+ Powered by llm.rb
34
+ </a>
404
35
  </div>
405
36
  <div class="main">
406
37
  <%= yield %>
@@ -1,4 +1,5 @@
1
1
  <div class="message <%= msg.role %>">
2
+ <div class="role-label"><%= msg.user? ? "You" : "Robot" %>:</div>
2
3
  <%= RailsLLM.markdown(msg.content).html_safe %>
3
4
  </div>
4
5
 
@@ -5,8 +5,9 @@
5
5
 
6
6
  <div class="input-area">
7
7
  <%= form_tag agents_path, method: :post, id: "new-agent-form" do %>
8
+ <div class="prompt">$</div>
8
9
  <%= text_field_tag :prompt, nil,
9
- placeholder: "Ask anything...",
10
+ placeholder: "type your message...",
10
11
  autofocus: true,
11
12
  autocomplete: "off" %>
12
13
  <%= submit_tag "Start Chat", data: { disable_with: "..." } %>
@@ -13,8 +13,9 @@
13
13
 
14
14
  <div class="input-area">
15
15
  <%= form_tag ask_agent_path(@agent), method: :post, id: "ask-form", data: { turbo: false } do %>
16
+ <div class="prompt">$</div>
16
17
  <%= text_field_tag :prompt, nil,
17
- placeholder: "Ask anything...",
18
+ placeholder: "type your message...",
18
19
  autofocus: true,
19
20
  required: true,
20
21
  autocomplete: "off" %>
@@ -25,6 +26,6 @@
25
26
  <div class="runtime-bar">
26
27
  <span class="dot idle"></span>
27
28
  <span>llm.rb</span>
28
- <span><%= @agent.usage.total_tokens || 0 %> tokens</span>
29
- <span><%= @messages.size %> messages</span>
29
+ <span id="token-count"><%= @agent.usage.total_tokens || 0 %> tokens</span>
30
+ <span id="message-count"><%= @messages.size %> messages</span>
30
31
  </div>
@@ -55,9 +55,11 @@ module RailsLLM
55
55
  end
56
56
 
57
57
  ##
58
+ # @param [Integer, nil] total_tokens
59
+ # @param [Integer, nil] message_count
58
60
  # @return [void]
59
- def finish
60
- write(type: "done")
61
+ def finish(total_tokens: nil, message_count: nil)
62
+ write(type: "done", total_tokens:, message_count:)
61
63
  end
62
64
 
63
65
  private
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsLLM
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - '0x1eef'
@@ -129,6 +129,7 @@ files:
129
129
  - README.md
130
130
  - app/assets/images/llm.png
131
131
  - app/assets/javascripts/rails_llm/application.js
132
+ - app/assets/stylesheets/rails_llm/application.css
132
133
  - app/controllers/rails_llm/agents_controller.rb
133
134
  - app/views/layouts/rails_llm/application.html.erb
134
135
  - app/views/rails_llm/agents/_message.html.erb