layered-assistant-rails 0.1.0 → 0.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.
- checksums.yaml +4 -4
- data/AGENTS.md +2 -0
- data/README.md +9 -0
- data/app/controllers/concerns/layered/assistant/stoppable_response.rb +13 -0
- data/app/controllers/layered/assistant/conversations_controller.rb +3 -1
- data/app/controllers/layered/assistant/panel/conversations_controller.rb +2 -1
- data/app/controllers/layered/assistant/public/conversations_controller.rb +3 -1
- data/app/controllers/layered/assistant/public/panel/conversations_controller.rb +2 -1
- data/app/javascript/layered_assistant/composer_controller.js +87 -5
- data/app/javascript/layered_assistant/message_streaming.js +9 -0
- data/app/jobs/layered/assistant/messages/response_job.rb +3 -0
- data/app/models/layered/assistant/conversation.rb +17 -0
- data/app/models/layered/assistant/message.rb +6 -0
- data/app/services/layered/assistant/chunk_service.rb +12 -0
- data/app/views/layered/assistant/messages/_composer.html.erb +2 -14
- data/app/views/layered/assistant/messages/_composer_fields.html.erb +14 -0
- data/app/views/layered/assistant/messages/_message.html.erb +5 -2
- data/app/views/layered/assistant/messages/create.turbo_stream.erb +1 -1
- data/app/views/layered/assistant/panel/messages/_composer.html.erb +2 -14
- data/app/views/layered/assistant/panel/messages/create.turbo_stream.erb +1 -1
- data/app/views/layered/assistant/public/messages/_composer.html.erb +2 -6
- data/app/views/layered/assistant/public/messages/create.turbo_stream.erb +1 -1
- data/app/views/layered/assistant/public/panel/messages/_composer.html.erb +2 -6
- data/app/views/layered/assistant/public/panel/messages/create.turbo_stream.erb +1 -1
- data/config/routes.rb +4 -0
- data/db/migrate/20260315000000_add_stopped_to_layered_assistant_messages.rb +5 -0
- data/lib/layered/assistant/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c09f437cf1d50e987e065bab63c5a974c569511d6095a09e0fb5e3072119d08b
|
|
4
|
+
data.tar.gz: 24476722cc3bf5d342cd308974e0fb888f059fd838cb406215e6772211dde587
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f26719babe3ba1885f3f96021f5900d202d9aa4ba70353477f6559ac8b04871a5d7bd24d81be0cd3b93866bf8d3e2d7cb83c5fab3b354c618e45555fe96476d4
|
|
7
|
+
data.tar.gz: 25fcf070c6222f1fca1234939f7f283cdabe0fd232ea407c704c2a8f270ae9ac2caaf57fb775f7d35a15d83c4c3a5921d616a847adf200190c5ecaf4e2bf5017
|
data/AGENTS.md
CHANGED
|
@@ -46,6 +46,8 @@ Verify dependencies before modifying host app files. Use `inject_into_file` for
|
|
|
46
46
|
## Making changes to `layered-ui-rails`
|
|
47
47
|
|
|
48
48
|
The UI kit gem for this app is located at `../layered-ui-rails`.
|
|
49
|
+
Use existing classes where possible to keep the UI project lean.
|
|
50
|
+
If there’s a class missing that would be genuinely useful, let me know before making any edits.
|
|
49
51
|
You can find and make edits to that project directly if necessary.
|
|
50
52
|
It has its own `AGENTS.md` file — inspect it for context on the apps's conventions, architecture, and development workflows.
|
|
51
53
|
|
data/README.md
CHANGED
|
@@ -51,6 +51,15 @@ This will:
|
|
|
51
51
|
|
|
52
52
|
All steps are idempotent - re-running the generator will not duplicate imports, routes, or migrations.
|
|
53
53
|
|
|
54
|
+
### Upgrading
|
|
55
|
+
|
|
56
|
+
After updating the gem, copy any new migrations and run them:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
bin/rails generate layered:assistant:migrations
|
|
60
|
+
bin/rails db:migrate
|
|
61
|
+
```
|
|
62
|
+
|
|
54
63
|
## Authorization
|
|
55
64
|
|
|
56
65
|
All non-public engine routes are **blocked by default** (403 Forbidden) until you configure an `authorize` block. The install generator creates a starter initialiser at `config/initializers/layered_assistant.rb` - uncomment one of the examples to get started.
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
module Layered
|
|
2
2
|
module Assistant
|
|
3
3
|
class ConversationsController < ApplicationController
|
|
4
|
-
|
|
4
|
+
include StoppableResponse
|
|
5
|
+
|
|
6
|
+
before_action :set_conversation, only: [:show, :edit, :update, :destroy, :stop]
|
|
5
7
|
before_action :set_assistants, only: [:new, :create]
|
|
6
8
|
|
|
7
9
|
def index
|
|
@@ -2,9 +2,10 @@ module Layered
|
|
|
2
2
|
module Assistant
|
|
3
3
|
module Panel
|
|
4
4
|
class ConversationsController < ApplicationController
|
|
5
|
+
include StoppableResponse
|
|
5
6
|
layout false
|
|
6
7
|
|
|
7
|
-
before_action :set_conversation, only: [:show, :destroy]
|
|
8
|
+
before_action :set_conversation, only: [:show, :destroy, :stop]
|
|
8
9
|
before_action :set_conversations, only: [:index, :show]
|
|
9
10
|
before_action :set_assistants, only: [:index, :show, :new, :create]
|
|
10
11
|
|
|
@@ -2,8 +2,10 @@ module Layered
|
|
|
2
2
|
module Assistant
|
|
3
3
|
module Public
|
|
4
4
|
class ConversationsController < ApplicationController
|
|
5
|
+
include StoppableResponse
|
|
6
|
+
|
|
5
7
|
before_action :set_public_assistant, only: [:create]
|
|
6
|
-
before_action :set_conversation, only: [:show]
|
|
8
|
+
before_action :set_conversation, only: [:show, :stop]
|
|
7
9
|
|
|
8
10
|
def create
|
|
9
11
|
@conversation = @assistant.conversations.new(name: Conversation.default_name)
|
|
@@ -3,10 +3,11 @@ module Layered
|
|
|
3
3
|
module Public
|
|
4
4
|
module Panel
|
|
5
5
|
class ConversationsController < Public::ApplicationController
|
|
6
|
+
include StoppableResponse
|
|
6
7
|
layout false
|
|
7
8
|
|
|
8
9
|
before_action :set_public_assistant, only: [:index, :new, :create]
|
|
9
|
-
before_action :set_conversation, only: [:show]
|
|
10
|
+
before_action :set_conversation, only: [:show, :stop]
|
|
10
11
|
before_action :set_session_conversations, only: [:index, :show]
|
|
11
12
|
|
|
12
13
|
def index
|
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
2
|
|
|
3
|
+
// Manages the message composer form. Toggles the submit button between
|
|
4
|
+
// "Send" and "Stop" depending on whether the assistant is responding,
|
|
5
|
+
// and disables "Send" when the input is empty.
|
|
3
6
|
export default class extends Controller {
|
|
4
|
-
static targets = ["form", "input", "
|
|
7
|
+
static targets = ["form", "input", "sendButton", "stopButton"]
|
|
8
|
+
static values = {
|
|
9
|
+
responding: { type: Boolean, default: false },
|
|
10
|
+
stopUrl: { type: String, default: "" }
|
|
11
|
+
}
|
|
5
12
|
|
|
6
13
|
connect() {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
14
|
+
this._onChunkReceived = () => this._resetRespondingTimeout()
|
|
15
|
+
document.addEventListener("assistant:chunk-received", this._onChunkReceived)
|
|
16
|
+
this._applyRespondingState()
|
|
17
|
+
this.updateButtonDisabled()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
disconnect() {
|
|
21
|
+
document.removeEventListener("assistant:chunk-received", this._onChunkReceived)
|
|
22
|
+
clearTimeout(this._respondingTimeout)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
respondingValueChanged() {
|
|
26
|
+
this._applyRespondingState()
|
|
10
27
|
}
|
|
11
28
|
|
|
29
|
+
// Enter submits, Shift+Enter is a no-op (default newline),
|
|
30
|
+
// Alt+Enter inserts a newline without submitting.
|
|
12
31
|
submitOnEnter(event) {
|
|
13
32
|
if (event.key !== "Enter") return
|
|
14
33
|
if (event.shiftKey) return
|
|
@@ -17,14 +36,77 @@ export default class extends Controller {
|
|
|
17
36
|
|
|
18
37
|
if (event.altKey) {
|
|
19
38
|
this.inputTarget.setRangeText("\n", this.inputTarget.selectionStart, this.inputTarget.selectionEnd, "end")
|
|
20
|
-
} else if (this.inputTarget.value.trim() !== "") {
|
|
39
|
+
} else if (!this.respondingValue && this.inputTarget.value.trim() !== "") {
|
|
21
40
|
this.formTarget.requestSubmit()
|
|
22
41
|
}
|
|
23
42
|
}
|
|
24
43
|
|
|
25
44
|
submit() {
|
|
45
|
+
this.respondingValue = true
|
|
46
|
+
|
|
47
|
+
// Dismiss the on-screen keyboard on touch devices
|
|
26
48
|
if ("ontouchstart" in window) {
|
|
27
49
|
this.inputTarget.blur()
|
|
28
50
|
}
|
|
29
51
|
}
|
|
52
|
+
|
|
53
|
+
// Ask the server to stop the current response, then reset the composer.
|
|
54
|
+
// On failure the composer resets anyway so the user is not stuck.
|
|
55
|
+
stop(event) {
|
|
56
|
+
event.preventDefault()
|
|
57
|
+
|
|
58
|
+
if (!this.stopUrlValue) return
|
|
59
|
+
|
|
60
|
+
const token = document.querySelector('meta[name="csrf-token"]')?.content
|
|
61
|
+
fetch(this.stopUrlValue, {
|
|
62
|
+
method: "PATCH",
|
|
63
|
+
headers: {
|
|
64
|
+
"X-CSRF-Token": token,
|
|
65
|
+
"Accept": "text/vnd.turbo-stream.html"
|
|
66
|
+
}
|
|
67
|
+
}).then(response => {
|
|
68
|
+
if (response.ok) this.respondingValue = false
|
|
69
|
+
}).catch(() => {
|
|
70
|
+
this.respondingValue = false
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Disable the send button when the input is empty. Called from the
|
|
75
|
+
// `input` event on the textarea and after responding state changes.
|
|
76
|
+
updateButtonDisabled() {
|
|
77
|
+
if (this.respondingValue) return
|
|
78
|
+
|
|
79
|
+
const empty = this.inputTarget.value.trim() === ""
|
|
80
|
+
this.sendButtonTarget.disabled = empty
|
|
81
|
+
this.sendButtonTarget.classList.toggle("l-ui-button--disabled", empty)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Toggle visibility of the Send and Stop buttons. While responding a
|
|
85
|
+
// 60-second safety timeout resets the composer in case the server
|
|
86
|
+
// never signals completion. The timeout is reset each time a chunk
|
|
87
|
+
// is received so long-running responses are not interrupted.
|
|
88
|
+
_applyRespondingState() {
|
|
89
|
+
clearTimeout(this._respondingTimeout)
|
|
90
|
+
|
|
91
|
+
if (this.respondingValue) {
|
|
92
|
+
this.sendButtonTarget.hidden = true
|
|
93
|
+
this.stopButtonTarget.hidden = false
|
|
94
|
+
|
|
95
|
+
this._resetRespondingTimeout()
|
|
96
|
+
} else {
|
|
97
|
+
this.stopButtonTarget.hidden = true
|
|
98
|
+
this.sendButtonTarget.hidden = false
|
|
99
|
+
this.updateButtonDisabled()
|
|
100
|
+
|
|
101
|
+
if (!this.element.closest("turbo-frame")) {
|
|
102
|
+
this.inputTarget.focus()
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
_resetRespondingTimeout() {
|
|
108
|
+
if (!this.respondingValue) return
|
|
109
|
+
clearTimeout(this._respondingTimeout)
|
|
110
|
+
this._respondingTimeout = setTimeout(() => { this.respondingValue = false }, 60000)
|
|
111
|
+
}
|
|
30
112
|
}
|
|
@@ -105,6 +105,12 @@ function scheduleRender(target) {
|
|
|
105
105
|
renderTimers.set(target, id)
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
Turbo.StreamActions.enable_composer = function () {
|
|
109
|
+
this.targetElements.forEach((form) => {
|
|
110
|
+
form.setAttribute("data-composer-responding-value", "false")
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
108
114
|
Turbo.StreamActions.append_chunk = function () {
|
|
109
115
|
this.targetElements.forEach((target) => {
|
|
110
116
|
const text = this.templateContent.textContent || ""
|
|
@@ -121,4 +127,7 @@ Turbo.StreamActions.append_chunk = function () {
|
|
|
121
127
|
// Schedule debounced render
|
|
122
128
|
scheduleRender(target)
|
|
123
129
|
})
|
|
130
|
+
|
|
131
|
+
// Notify the composer so it can reset its safety timeout
|
|
132
|
+
document.dispatchEvent(new CustomEvent("assistant:chunk-received"))
|
|
124
133
|
}
|
|
@@ -10,6 +10,7 @@ module Layered
|
|
|
10
10
|
unless message.model&.provider
|
|
11
11
|
message.update(content: "No provider is configured for this model.")
|
|
12
12
|
message.broadcast_updated
|
|
13
|
+
message.broadcast_response_complete
|
|
13
14
|
return
|
|
14
15
|
end
|
|
15
16
|
|
|
@@ -29,6 +30,8 @@ module Layered
|
|
|
29
30
|
message.update(content: existing.present? ? "#{existing}\n\n---\n\n#{error_note}" : error_note)
|
|
30
31
|
message.broadcast_updated
|
|
31
32
|
end
|
|
33
|
+
|
|
34
|
+
message.broadcast_response_complete unless message.reload.stopped?
|
|
32
35
|
end
|
|
33
36
|
end
|
|
34
37
|
end
|
|
@@ -28,6 +28,23 @@ module Layered
|
|
|
28
28
|
"New conversation"
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
def stop_response!
|
|
32
|
+
message = messages.where(role: :assistant, stopped: false).order(created_at: :desc).first
|
|
33
|
+
return false unless message
|
|
34
|
+
|
|
35
|
+
message.with_lock do
|
|
36
|
+
return false if message.stopped?
|
|
37
|
+
|
|
38
|
+
estimated = TokenEstimator.estimate(message.content) || 0
|
|
39
|
+
message.update!(stopped: true, output_tokens: estimated, tokens_estimated: true)
|
|
40
|
+
update_token_totals!
|
|
41
|
+
message.broadcast_updated
|
|
42
|
+
message.broadcast_response_complete
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
true
|
|
46
|
+
end
|
|
47
|
+
|
|
31
48
|
def update_name_from_content!(content)
|
|
32
49
|
return unless name == self.class.default_name
|
|
33
50
|
return if content.blank?
|
|
@@ -39,6 +39,12 @@ module Layered
|
|
|
39
39
|
locals: { message: self }
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
def broadcast_response_complete
|
|
43
|
+
broadcast_action_to conversation,
|
|
44
|
+
action: :enable_composer,
|
|
45
|
+
targets: ".#{dom_id(conversation)}_composer"
|
|
46
|
+
end
|
|
47
|
+
|
|
42
48
|
def broadcast_chunk(text)
|
|
43
49
|
broadcast_action_to conversation,
|
|
44
50
|
action: :append_chunk,
|
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
module Layered
|
|
2
2
|
module Assistant
|
|
3
3
|
class ChunkService
|
|
4
|
+
STOP_CHECK_INTERVAL = 25
|
|
5
|
+
|
|
4
6
|
def initialize(message, provider:)
|
|
5
7
|
@message = message
|
|
6
8
|
@provider = provider
|
|
7
9
|
@input_tokens = 0
|
|
8
10
|
@output_tokens = 0
|
|
11
|
+
@chunk_count = 0
|
|
12
|
+
@stopped = false
|
|
9
13
|
end
|
|
10
14
|
|
|
11
15
|
def call(chunk)
|
|
16
|
+
return if @stopped
|
|
17
|
+
|
|
18
|
+
@chunk_count += 1
|
|
19
|
+
if @chunk_count % STOP_CHECK_INTERVAL == 0
|
|
20
|
+
@stopped = @message.reload.stopped?
|
|
21
|
+
return if @stopped
|
|
22
|
+
end
|
|
23
|
+
|
|
12
24
|
Rails.logger.debug { "[ChunkService] #{chunk.inspect}" }
|
|
13
25
|
text = extract_text(chunk)
|
|
14
26
|
extract_usage(chunk)
|
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
<%= form_with url: layered_assistant.conversation_messages_path(conversation), id: "composer-form", data: { controller: "composer", composer_target: "form", action: "submit->composer#submit" } do |f| %>
|
|
2
|
-
|
|
3
|
-
<%= f.text_area :content, name: "message[content]", class: "l-ui-conversation__composer-input", placeholder: "Type a message...", "aria-label": "Message", rows: 1, data: { composer_target: "input", action: "keydown->composer#submitOnEnter" } %>
|
|
4
|
-
|
|
5
|
-
<button type="submit" class="l-ui-button--primary" title="Send (Enter)" data-composer-target="button">Send</button>
|
|
6
|
-
</div>
|
|
7
|
-
|
|
8
|
-
<% if models.any? %>
|
|
9
|
-
<div class="l-ui-form__group">
|
|
10
|
-
<div class="l-ui-select-wrapper">
|
|
11
|
-
<%= f.select :model_id, options_for_select(models.map { |m| ["#{m.provider.name}: #{m.name}", m.id] }, selected_model_id), {}, name: "message[model_id]", class: "l-ui-select", "aria-label": "AI model" %>
|
|
12
|
-
</div>
|
|
13
|
-
</div>
|
|
14
|
-
<% end %>
|
|
1
|
+
<%= form_with url: layered_assistant.conversation_messages_path(conversation), id: "composer-form", class: "#{dom_id(conversation)}_composer", data: { controller: "composer", composer_target: "form", action: "submit->composer#submit", composer_responding_value: local_assigns.fetch(:responding, false), composer_stop_url_value: layered_assistant.stop_conversation_path(conversation) } do |f| %>
|
|
2
|
+
<%= render "layered/assistant/messages/composer_fields", f: f, models: models, selected_model_id: selected_model_id %>
|
|
15
3
|
<% end %>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<div class="l-ui-conversation__composer">
|
|
2
|
+
<%= f.text_area :content, name: "message[content]", class: "l-ui-conversation__composer-input", placeholder: "Type a message...", "aria-label": "Message", rows: 1, data: { composer_target: "input", action: "keydown->composer#submitOnEnter input->composer#updateButtonDisabled" } %>
|
|
3
|
+
|
|
4
|
+
<button type="submit" class="l-ui-button--primary" title="Send (Enter)" data-composer-target="sendButton">Send</button>
|
|
5
|
+
<button type="button" class="l-ui-button--primary" title="Stop" data-composer-target="stopButton" data-action="click->composer#stop" hidden>Stop</button>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<% if local_assigns[:models]&.any? %>
|
|
9
|
+
<div class="l-ui-form__group">
|
|
10
|
+
<div class="l-ui-select-wrapper">
|
|
11
|
+
<%= f.select :model_id, options_for_select(models.map { |m| ["#{m.provider.name}: #{m.name}", m.id] }, selected_model_id), {}, name: "message[model_id]", class: "l-ui-select", "aria-label": "AI model" %>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
<% end %>
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
<% unless message.user? %>
|
|
4
4
|
<div class="l-ui-message__author"><%= message.role.capitalize %></div>
|
|
5
5
|
<% end %>
|
|
6
|
-
<div class="<%= dom_id(message) %>_body l-ui-message__body<%= " l-ui-markdown" unless message.assistant? && message.content.blank? %>">
|
|
7
|
-
<% if message.assistant? && message.content.blank? %>
|
|
6
|
+
<div class="<%= dom_id(message) %>_body l-ui-message__body<%= " l-ui-markdown" unless message.assistant? && message.content.blank? && !message.stopped? %>">
|
|
7
|
+
<% if message.assistant? && message.content.blank? && !message.stopped? %>
|
|
8
8
|
<div class="l-ui-typing-indicator" role="status" aria-label="Assistant is typing">
|
|
9
9
|
<span class="l-ui-typing-indicator__dot"></span>
|
|
10
10
|
<span class="l-ui-typing-indicator__dot"></span>
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
<% else %>
|
|
14
14
|
<%= render_message_content(message) %>
|
|
15
15
|
<% end %>
|
|
16
|
+
<% if message.stopped? %>
|
|
17
|
+
<div class="l-ui-notice--warning" role="status">The response was stopped</div>
|
|
18
|
+
<% end %>
|
|
16
19
|
</div>
|
|
17
20
|
<div class="l-ui-message__footer">
|
|
18
21
|
<% total_tokens = message.input_tokens.to_i + message.output_tokens.to_i %>
|
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
<% end %>
|
|
6
6
|
|
|
7
7
|
<%= turbo_stream.replace "composer-form" do %>
|
|
8
|
-
<%= render "layered/assistant/messages/composer", conversation: @conversation, models: @models, selected_model_id: @selected_model_id %>
|
|
8
|
+
<%= render "layered/assistant/messages/composer", conversation: @conversation, models: @models, selected_model_id: @selected_model_id, responding: @error.nil? %>
|
|
9
9
|
<% end %>
|
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
<%= form_with url: layered_assistant.panel_conversation_messages_path(conversation), id: "panel-composer-form", data: { controller: "composer", composer_target: "form", turbo_frame: "_top", action: "submit->composer#submit" } do |f| %>
|
|
2
|
-
|
|
3
|
-
<%= f.text_area :content, name: "message[content]", class: "l-ui-conversation__composer-input", placeholder: "Type a message...", "aria-label": "Message", rows: 1, data: { composer_target: "input", action: "keydown->composer#submitOnEnter" } %>
|
|
4
|
-
|
|
5
|
-
<button type="submit" class="l-ui-button--primary" title="Send (Enter)" data-composer-target="button">Send</button>
|
|
6
|
-
</div>
|
|
7
|
-
|
|
8
|
-
<% if models.any? %>
|
|
9
|
-
<div class="l-ui-form__group">
|
|
10
|
-
<div class="l-ui-select-wrapper">
|
|
11
|
-
<%= f.select :model_id, options_for_select(models.map { |m| ["#{m.provider.name}: #{m.name}", m.id] }, selected_model_id), {}, name: "message[model_id]", class: "l-ui-select", "aria-label": "AI model" %>
|
|
12
|
-
</div>
|
|
13
|
-
</div>
|
|
14
|
-
<% end %>
|
|
1
|
+
<%= form_with url: layered_assistant.panel_conversation_messages_path(conversation), id: "panel-composer-form", class: "#{dom_id(conversation)}_composer", data: { controller: "composer", composer_target: "form", turbo_frame: "_top", action: "submit->composer#submit", composer_responding_value: local_assigns.fetch(:responding, false), composer_stop_url_value: layered_assistant.stop_panel_conversation_path(conversation) } do |f| %>
|
|
2
|
+
<%= render "layered/assistant/messages/composer_fields", f: f, models: models, selected_model_id: selected_model_id %>
|
|
15
3
|
<% end %>
|
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
<% end %>
|
|
6
6
|
|
|
7
7
|
<%= turbo_stream.replace "panel-composer-form" do %>
|
|
8
|
-
<%= render "layered/assistant/panel/messages/composer", conversation: @conversation, models: @models, selected_model_id: @selected_model_id %>
|
|
8
|
+
<%= render "layered/assistant/panel/messages/composer", conversation: @conversation, models: @models, selected_model_id: @selected_model_id, responding: @error.nil? %>
|
|
9
9
|
<% end %>
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
<%= form_with url: layered_assistant.public_conversation_messages_path(conversation), id: "public-composer-form", data: { controller: "composer", composer_target: "form", turbo_frame: "_top", action: "submit->composer#submit" } do |f| %>
|
|
2
|
-
|
|
3
|
-
<%= f.text_area :content, name: "message[content]", class: "l-ui-conversation__composer-input", placeholder: "Type a message...", "aria-label": "Message", rows: 1, data: { composer_target: "input", action: "keydown->composer#submitOnEnter" } %>
|
|
4
|
-
|
|
5
|
-
<button type="submit" class="l-ui-button--primary" title="Send (Enter)" data-composer-target="button">Send</button>
|
|
6
|
-
</div>
|
|
1
|
+
<%= form_with url: layered_assistant.public_conversation_messages_path(conversation), id: "public-composer-form", class: "#{dom_id(conversation)}_composer", data: { controller: "composer", composer_target: "form", turbo_frame: "_top", action: "submit->composer#submit", composer_responding_value: local_assigns.fetch(:responding, false), composer_stop_url_value: layered_assistant.stop_public_conversation_path(conversation) } do |f| %>
|
|
2
|
+
<%= render "layered/assistant/messages/composer_fields", f: f %>
|
|
7
3
|
<% end %>
|
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
<% end %>
|
|
6
6
|
|
|
7
7
|
<%= turbo_stream.replace "public-composer-form" do %>
|
|
8
|
-
<%= render "layered/assistant/public/messages/composer", conversation: @conversation %>
|
|
8
|
+
<%= render "layered/assistant/public/messages/composer", conversation: @conversation, responding: @error.nil? %>
|
|
9
9
|
<% end %>
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
<%= form_with url: layered_assistant.public_panel_conversation_messages_path(conversation), id: "public-panel-composer-form", data: { controller: "composer", composer_target: "form", turbo_frame: "_top", action: "submit->composer#submit" } do |f| %>
|
|
2
|
-
|
|
3
|
-
<%= f.text_area :content, name: "message[content]", class: "l-ui-conversation__composer-input", placeholder: "Type a message...", "aria-label": "Message", rows: 1, data: { composer_target: "input", action: "keydown->composer#submitOnEnter" } %>
|
|
4
|
-
|
|
5
|
-
<button type="submit" class="l-ui-button--primary" title="Send (Enter)" data-composer-target="button">Send</button>
|
|
6
|
-
</div>
|
|
1
|
+
<%= form_with url: layered_assistant.public_panel_conversation_messages_path(conversation), id: "public-panel-composer-form", class: "#{dom_id(conversation)}_composer", data: { controller: "composer", composer_target: "form", turbo_frame: "_top", action: "submit->composer#submit", composer_responding_value: local_assigns.fetch(:responding, false), composer_stop_url_value: layered_assistant.stop_public_panel_conversation_path(conversation) } do |f| %>
|
|
2
|
+
<%= render "layered/assistant/messages/composer_fields", f: f %>
|
|
7
3
|
<% end %>
|
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
<% end %>
|
|
6
6
|
|
|
7
7
|
<%= turbo_stream.replace "public-panel-composer-form" do %>
|
|
8
|
-
<%= render "layered/assistant/public/panel/messages/composer", conversation: @conversation %>
|
|
8
|
+
<%= render "layered/assistant/public/panel/messages/composer", conversation: @conversation, responding: @error.nil? %>
|
|
9
9
|
<% end %>
|
data/config/routes.rb
CHANGED
|
@@ -7,11 +7,13 @@ Layered::Assistant::Engine.routes.draw do
|
|
|
7
7
|
resources :models, only: [:index, :new, :create, :edit, :update, :destroy]
|
|
8
8
|
end
|
|
9
9
|
resources :conversations, only: [:index, :show, :new, :create, :edit, :update, :destroy] do
|
|
10
|
+
patch :stop, on: :member
|
|
10
11
|
resources :messages, only: [:index, :create, :destroy]
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
namespace :panel do
|
|
14
15
|
resources :conversations, only: [:index, :show, :new, :create, :destroy] do
|
|
16
|
+
patch :stop, on: :member
|
|
15
17
|
resources :messages, only: [:create]
|
|
16
18
|
end
|
|
17
19
|
end
|
|
@@ -19,11 +21,13 @@ Layered::Assistant::Engine.routes.draw do
|
|
|
19
21
|
namespace :public do
|
|
20
22
|
resources :assistants, only: [:index, :show]
|
|
21
23
|
resources :conversations, only: [:show, :create] do
|
|
24
|
+
patch :stop, on: :member
|
|
22
25
|
resources :messages, only: [:create]
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
namespace :panel do
|
|
26
29
|
resources :conversations, only: [:index, :show, :new, :create] do
|
|
30
|
+
patch :stop, on: :member
|
|
27
31
|
resources :messages, only: [:create]
|
|
28
32
|
end
|
|
29
33
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: layered-assistant-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- layered.ai
|
|
@@ -305,6 +305,7 @@ files:
|
|
|
305
305
|
- app/assets/tailwind/layered/assistant/styles.css
|
|
306
306
|
- app/controllers/concerns/layered/assistant/message_creation.rb
|
|
307
307
|
- app/controllers/concerns/layered/assistant/public/session_conversations.rb
|
|
308
|
+
- app/controllers/concerns/layered/assistant/stoppable_response.rb
|
|
308
309
|
- app/controllers/layered/assistant/application_controller.rb
|
|
309
310
|
- app/controllers/layered/assistant/assistants_controller.rb
|
|
310
311
|
- app/controllers/layered/assistant/conversations_controller.rb
|
|
@@ -357,6 +358,7 @@ files:
|
|
|
357
358
|
- app/views/layered/assistant/conversations/new.html.erb
|
|
358
359
|
- app/views/layered/assistant/conversations/show.html.erb
|
|
359
360
|
- app/views/layered/assistant/messages/_composer.html.erb
|
|
361
|
+
- app/views/layered/assistant/messages/_composer_fields.html.erb
|
|
360
362
|
- app/views/layered/assistant/messages/_message.html.erb
|
|
361
363
|
- app/views/layered/assistant/messages/_system_prompt.html.erb
|
|
362
364
|
- app/views/layered/assistant/messages/create.turbo_stream.erb
|
|
@@ -394,6 +396,7 @@ files:
|
|
|
394
396
|
- config/routes.rb
|
|
395
397
|
- data/models.json
|
|
396
398
|
- db/migrate/20260312000000_create_layered_assistant_tables.rb
|
|
399
|
+
- db/migrate/20260315000000_add_stopped_to_layered_assistant_messages.rb
|
|
397
400
|
- lib/generators/layered/assistant/install_generator.rb
|
|
398
401
|
- lib/generators/layered/assistant/migrations_generator.rb
|
|
399
402
|
- lib/generators/layered/assistant/templates/initializer.rb
|