plain-rails 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.
- checksums.yaml +7 -0
- data/README.md +162 -0
- data/Rakefile +8 -0
- data/app/assets/builds/plain.css +1 -0
- data/app/assets/config/plain_manifest.js +1 -0
- data/app/assets/stylesheets/plain/application.css +15 -0
- data/app/assets/stylesheets/plain/application.tailwind.css +7 -0
- data/app/controllers/plain/application_controller.rb +4 -0
- data/app/controllers/plain/conversations_controller.rb +55 -0
- data/app/controllers/plain/docs_controller.rb +63 -0
- data/app/controllers/plain/home_controller.rb +10 -0
- data/app/controllers/plain/messages_controller.rb +30 -0
- data/app/helpers/plain/application_helper.rb +4 -0
- data/app/helpers/plain/conversations_helper.rb +4 -0
- data/app/helpers/plain/docs_helper.rb +4 -0
- data/app/helpers/plain/home_helper.rb +4 -0
- data/app/helpers/plain/messages_helper.rb +4 -0
- data/app/javascripts/loader_controller.js +27 -0
- data/app/javascripts/scroll_controller.js +8 -0
- data/app/javascripts/toggle_class_controller.js +11 -0
- data/app/jobs/plain/application_job.rb +4 -0
- data/app/jobs/plain/message_processor_job.rb +9 -0
- data/app/mailers/plain/application_mailer.rb +6 -0
- data/app/models/plain/application_record.rb +5 -0
- data/app/models/plain/conversation.rb +53 -0
- data/app/models/plain/message.rb +62 -0
- data/app/services/plain/ai_docs.rb +163 -0
- data/app/services/plain/docs_service.rb +127 -0
- data/app/views/layouts/plain/application.html.erb +51 -0
- data/app/views/plain/conversations/_conversation_item.erb +15 -0
- data/app/views/plain/conversations/_conversation_item.html.erb +21 -0
- data/app/views/plain/conversations/_conversation_list.erb +18 -0
- data/app/views/plain/conversations/_conversation_list.html.erb +13 -0
- data/app/views/plain/conversations/_modal.html.erb +73 -0
- data/app/views/plain/conversations/index.html.erb +17 -0
- data/app/views/plain/conversations/show.html.erb +14 -0
- data/app/views/plain/docs/_footer.erb +61 -0
- data/app/views/plain/docs/_menu.erb +20 -0
- data/app/views/plain/docs/_sections.erb +73 -0
- data/app/views/plain/docs/show.html.erb +126 -0
- data/app/views/plain/home/index.html.erb +35 -0
- data/app/views/plain/messages/_form.html.erb +25 -0
- data/app/views/plain/messages/_message_item.html.erb +26 -0
- data/app/views/plain/messages/_message_list.html.erb +16 -0
- data/app/views/plain/messages/_new_message.turbo_stream.erb +3 -0
- data/app/views/plain/messages/create.turbo_stream.erb +8 -0
- data/app/views/plain/messages/index.html.erb +1 -0
- data/config/database.yml +53 -0
- data/config/routes.rb +11 -0
- data/db/migrate/20230729010703_create_plain_conversations.rb +10 -0
- data/db/migrate/20230729010745_create_plain_messages.rb +11 -0
- data/lib/plain/configuration.rb +12 -0
- data/lib/plain/engine.rb +16 -0
- data/lib/plain/version.rb +3 -0
- data/lib/plain.rb +17 -0
- data/lib/tasks/plain_tasks.rake +21 -0
- metadata +196 -0
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Plain
|
4
|
+
class Message < ApplicationRecord
|
5
|
+
belongs_to :conversation, foreign_key: :plain_conversation_id
|
6
|
+
|
7
|
+
after_create_commit -> {
|
8
|
+
broadcast_render_to(
|
9
|
+
self.conversation,
|
10
|
+
partial: "plain/messages/new_message",
|
11
|
+
locals: {message: self}
|
12
|
+
)
|
13
|
+
broadcast_update_to(
|
14
|
+
self.conversation,
|
15
|
+
target: "message-form",
|
16
|
+
partial: "plain/messages/form",
|
17
|
+
locals: {message: self, conversation: self.conversation}
|
18
|
+
)
|
19
|
+
}
|
20
|
+
|
21
|
+
after_update_commit -> {
|
22
|
+
broadcast_replace_to(
|
23
|
+
self.conversation,
|
24
|
+
target: "message-item-#{id}",
|
25
|
+
partial: "plain/messages/message_item",
|
26
|
+
locals: { message: self, scroll: true }
|
27
|
+
)
|
28
|
+
}
|
29
|
+
|
30
|
+
def assistant?
|
31
|
+
role == "assistant"
|
32
|
+
end
|
33
|
+
|
34
|
+
def persist_as_document(path)
|
35
|
+
# Remove starting '/' if present
|
36
|
+
path = path.sub(/\A\//, '')
|
37
|
+
|
38
|
+
# Add '.md' extension if it's not already present
|
39
|
+
path += '.md' unless path.end_with?('.md')
|
40
|
+
|
41
|
+
# Construct the full path to the desired file
|
42
|
+
full_path = Rails.root.join('docs', path)
|
43
|
+
|
44
|
+
# Create the directory path, unless it already exists
|
45
|
+
dirname = File.dirname(full_path)
|
46
|
+
FileUtils.mkdir_p(dirname) unless File.directory?(dirname)
|
47
|
+
|
48
|
+
# Write content from self.body to the file, unless it already exists
|
49
|
+
File.write(full_path, self.content) unless File.exist?(full_path)
|
50
|
+
end
|
51
|
+
|
52
|
+
def type
|
53
|
+
case self.role
|
54
|
+
when "user"
|
55
|
+
"human"
|
56
|
+
when "assistant"
|
57
|
+
"ai"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
module Langchain
|
2
|
+
module Processors
|
3
|
+
class RB < Langchain::Processors::Base
|
4
|
+
EXTENSIONS = [".rb", ".js", ".erb", ".md", ".gemspec"]
|
5
|
+
CONTENT_TYPES = ["text/plain"]
|
6
|
+
|
7
|
+
# Parse the document and return the text
|
8
|
+
# @param [File] data
|
9
|
+
# @return [String]
|
10
|
+
def parse(data)
|
11
|
+
data.read
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class JS < Langchain::Processors::Base
|
16
|
+
EXTENSIONS = [".js", ".tsx", ".jsx"]
|
17
|
+
CONTENT_TYPES = ["application/javascript"]
|
18
|
+
|
19
|
+
# Parse the document and return the text
|
20
|
+
# @param [File] data
|
21
|
+
# @return [Hash]
|
22
|
+
def parse(data)
|
23
|
+
data.read
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class JSON < Langchain::Processors::Base
|
28
|
+
EXTENSIONS = [".json"]
|
29
|
+
CONTENT_TYPES = ["application/json"]
|
30
|
+
|
31
|
+
# Parse the document and return the text
|
32
|
+
# @param [File] data
|
33
|
+
# @return [Hash]
|
34
|
+
def parse(data)
|
35
|
+
data.read
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
module Plain
|
43
|
+
class Markdownray < Redcarpet::Render::HTML
|
44
|
+
def block_code(code, language)
|
45
|
+
CodeRay.scan(code, language).div() rescue "xxx"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class AiDocs
|
50
|
+
|
51
|
+
def conversation_client(&block)
|
52
|
+
llm = Langchain::LLM::OpenAI.new(api_key: ENV["OPENAI_API_KEY"], default_options: {
|
53
|
+
#chat_completion_model_name: "gpt-3.5-turbo-16k"
|
54
|
+
chat_completion_model_name: "gpt-4"
|
55
|
+
})
|
56
|
+
# refactor this
|
57
|
+
if block_given?
|
58
|
+
@conversation_client = Langchain::Conversation.new(llm: llm) do |chunk|
|
59
|
+
yield chunk
|
60
|
+
end
|
61
|
+
else
|
62
|
+
@conversation_client = Langchain::Conversation.new(llm: llm)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def client
|
67
|
+
@client ||= Plain.configuration.vector_search
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.set_conversation_title(conversation)
|
71
|
+
chat = Plain::AiDocs.new.conversation_client
|
72
|
+
chat.add_examples(conversation.messages)
|
73
|
+
title = chat.message("from the whole conversation please summarize it 4 words")
|
74
|
+
if title.is_a?(Hash)
|
75
|
+
title = title["choices"].map{|o| o["message"]["content"]}.join("") rescue "no subject"
|
76
|
+
end
|
77
|
+
puts "TITLE: #{title}"
|
78
|
+
conversation.update(subject: title)
|
79
|
+
end
|
80
|
+
|
81
|
+
def load_all
|
82
|
+
client.create_default_schema
|
83
|
+
load_configuration_paths
|
84
|
+
end
|
85
|
+
|
86
|
+
def load_configuration_paths
|
87
|
+
# Distinguishing files from directories
|
88
|
+
files_without_extension, directories = Plain.configuration.paths.partition { |path| File.file?(path) }
|
89
|
+
|
90
|
+
# Getting files from directories based on the allowed extensions
|
91
|
+
files_with_extension = directories.flat_map do |dir|
|
92
|
+
Dir[File.join(dir, "*.{#{Plain.configuration.extensions.join(',')}}")]
|
93
|
+
end
|
94
|
+
|
95
|
+
puts "FILES WITH EXTENSIONS"
|
96
|
+
puts files_with_extension
|
97
|
+
# Add files with extensions to the client
|
98
|
+
client.add_data(paths: files_with_extension)
|
99
|
+
|
100
|
+
puts "FILES WITHOUT EXTENSIONS"
|
101
|
+
puts files_with_extension
|
102
|
+
# Process files without extensions
|
103
|
+
files_without_extension.each do |file|
|
104
|
+
content = File.read(file)
|
105
|
+
#texts = Langchain::Chunker::Text.new(content).chunks
|
106
|
+
client.add_texts(texts: content)
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.convert_markdown(text)
|
112
|
+
rndr = Markdownray.new(filter_html: true, hard_wrap: true)
|
113
|
+
options = {
|
114
|
+
fenced_code_blocks: true,
|
115
|
+
no_intra_emphasis: true,
|
116
|
+
autolink: true,
|
117
|
+
lax_html_blocks: true
|
118
|
+
}
|
119
|
+
# markdown_to_html = Redcarpet::Markdown.new(Redcarpet::Render::HTML, autolink: true, tables: true)
|
120
|
+
markdown_to_html = Redcarpet::Markdown.new(rndr, options)
|
121
|
+
markdown_to_html.render(text) #rescue nil
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.create_file_with_path(path, content)
|
125
|
+
# Sanitize the path by replacing spaces with underscores and removing unsafe characters
|
126
|
+
sanitized_path = path.strip.gsub(/[^0-9A-Za-z\/]/, '').gsub(/\s+/, '_')
|
127
|
+
|
128
|
+
# Create the full path, joining the Rails root directory, "app/docs", and the sanitized path
|
129
|
+
full_path = Rails.root.join("app/views/docs", sanitized_path)
|
130
|
+
|
131
|
+
# Create directories for the path if they don't exist
|
132
|
+
FileUtils.mkdir_p(File.dirname(full_path))
|
133
|
+
|
134
|
+
# Create the file and write content to it
|
135
|
+
File.write(full_path, content)
|
136
|
+
end
|
137
|
+
|
138
|
+
def self.functions
|
139
|
+
[
|
140
|
+
{
|
141
|
+
name: "create_rails_controller",
|
142
|
+
description: "gives a command to create a rails controller",
|
143
|
+
parameters: {
|
144
|
+
type: :object,
|
145
|
+
properties: {
|
146
|
+
controller_name: {
|
147
|
+
type: :string,
|
148
|
+
description: "the controller name, e.g. users_controller"
|
149
|
+
},
|
150
|
+
unit: {
|
151
|
+
type: "string",
|
152
|
+
enum: %w[celsius fahrenheit]
|
153
|
+
}
|
154
|
+
},
|
155
|
+
required: ["controller_name"]
|
156
|
+
}
|
157
|
+
}
|
158
|
+
]
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'front_matter_parser'
|
2
|
+
|
3
|
+
class Plain::DocsService
|
4
|
+
DEFAULT_POSITION = 999
|
5
|
+
|
6
|
+
def self.get_structure
|
7
|
+
# Get markdown files at root directory
|
8
|
+
root_files = get_files('docs', true)
|
9
|
+
{
|
10
|
+
name: 'docs',
|
11
|
+
type: 'directory',
|
12
|
+
children: root_files[:children] + parse_main_sections # Add root files to children array
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.parse_section_items
|
17
|
+
config = self.config
|
18
|
+
main_sections = config['sections']
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.config
|
22
|
+
file_path = Rails.root.join('docs', 'config.yml')
|
23
|
+
return {} if !File.exist?(file_path)
|
24
|
+
config = YAML.safe_load(File.read(file_path)) || {}
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.parse_main_sections
|
28
|
+
#file_path = Rails.root.join('docs', 'config.yml')
|
29
|
+
#config = YAML.safe_load(File.read(file_path)) || {}
|
30
|
+
main_sections = config['sections'] || []
|
31
|
+
|
32
|
+
# Get all directories under 'docs'
|
33
|
+
all_sections = Dir.children(Rails.root.join('docs')).select do |entry|
|
34
|
+
File.directory?(Rails.root.join('docs', entry))
|
35
|
+
end.map do |dir|
|
36
|
+
section = get_files(File.join('docs', dir)) # Get children here
|
37
|
+
section[:name] = dir
|
38
|
+
section[:position] = DEFAULT_POSITION
|
39
|
+
section
|
40
|
+
end
|
41
|
+
|
42
|
+
# Overwrite positions with those found in config.yml
|
43
|
+
main_sections.each do |section|
|
44
|
+
matching_section = all_sections.find { |s| s[:name] == section['name'] }
|
45
|
+
if matching_section
|
46
|
+
matching_section[:position] = section['position']
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Sort by position and return children
|
51
|
+
all_sections.sort_by { |section| section[:position] }
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.get_content(file_path)
|
55
|
+
parsed = FrontMatterParser::Parser.parse_file(Rails.root.join('docs', "#{file_path}.md"))
|
56
|
+
|
57
|
+
# markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML, autolink: true, tables: true)
|
58
|
+
# markdown.render(parsed.content).html_safe
|
59
|
+
# binding.pry
|
60
|
+
Plain::AiDocs.convert_markdown(parsed.content)
|
61
|
+
end
|
62
|
+
|
63
|
+
#def self.parse_main_sections
|
64
|
+
# file_path = Rails.root.join('docs', 'config.yml')
|
65
|
+
# config = YAML.load_file(file_path)
|
66
|
+
# main_sections = config['sections']
|
67
|
+
#end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def self.get_files(directory, only_files_at_root = false)
|
72
|
+
FileUtils.mkdir_p(Rails.root.join(directory))
|
73
|
+
Dir.entries(Rails.root.join(directory)).sort.each_with_object({ name: directory.split('/').last, type: 'directory', children: [], position: 999 }) do |entry, parent|
|
74
|
+
next if ['.', '..'].include?(entry)
|
75
|
+
|
76
|
+
path = "#{directory}/#{entry}"
|
77
|
+
path_in_root = Rails.root.join(path)
|
78
|
+
|
79
|
+
if entry == 'config.yml' && File.exist?(path_in_root)
|
80
|
+
config = YAML.load_file(path_in_root)
|
81
|
+
parent[:position] = config['position'] || 999
|
82
|
+
elsif File.directory?(path_in_root)
|
83
|
+
# Skip directories if only_files_at_root is true
|
84
|
+
parent[:children] << get_files(path_in_root) unless only_files_at_root
|
85
|
+
else
|
86
|
+
parsed = FrontMatterParser::Parser.parse_file(path_in_root)
|
87
|
+
parent[:children] << { name: parsed.front_matter['title'] || entry.sub('.md', ''), type: 'file', path: path.sub('docs/', '').sub('.md', ''), position: parsed.front_matter['menu_position'] || 999 }
|
88
|
+
end
|
89
|
+
|
90
|
+
# After adding all children and before returning, sort them by position and assign prev and next links
|
91
|
+
sorted_children = parent[:children].sort_by { |child| child[:position] }
|
92
|
+
|
93
|
+
sorted_children.each_with_index do |child, index|
|
94
|
+
if index > 0
|
95
|
+
child[:prev] = {name: sorted_children[index - 1][:name], path: sorted_children[index - 1][:path]}
|
96
|
+
end
|
97
|
+
if index < sorted_children.size - 1
|
98
|
+
child[:next] = {name: sorted_children[index + 1][:name], path: sorted_children[index + 1][:path]}
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
parent[:children] = sorted_children
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
|
108
|
+
def self.assign_position_from_config(parent, path)
|
109
|
+
config = YAML.load_file(path)
|
110
|
+
parent[:position] = config&.fetch('position', DEFAULT_POSITION)
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.add_child_from_file(parent, path)
|
114
|
+
parsed = FrontMatterParser::Parser.parse_file(path)
|
115
|
+
parent[:children] << { name: parsed.front_matter['title'] || File.basename(path, '.md'), type: 'file', path: path.sub('docs/', '').sub('.md', ''), position: parsed.front_matter['menu_position'] || DEFAULT_POSITION }
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.sort_children(parent)
|
119
|
+
parent[:children] = parent[:children].sort_by { |child| child[:position] }
|
120
|
+
|
121
|
+
parent[:children].each_with_index do |child, index|
|
122
|
+
child[:prev] = parent[:children][index - 1] if index > 0
|
123
|
+
child[:next] = parent[:children][index + 1] if index < parent[:children].size - 1
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Plain</title>
|
5
|
+
<%= csrf_meta_tags %>
|
6
|
+
<%= csp_meta_tag %>
|
7
|
+
|
8
|
+
<%= stylesheet_link_tag "plain", "data-turbo-track": "reload" %>
|
9
|
+
<!-- actual rails host app -->
|
10
|
+
<% #= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
|
11
|
+
|
12
|
+
|
13
|
+
<script type="module">
|
14
|
+
// import railsActioncable from 'https://cdn.skypack.dev/@rails/actioncable';
|
15
|
+
// import railsActioncable from 'https://cdn.skypack.dev/@rails/actioncable'
|
16
|
+
import {Turbo} from 'https://jspm.dev/@hotwired/turbo-rails';
|
17
|
+
//Turbo.session.drive = false
|
18
|
+
window.Turbo = Turbo
|
19
|
+
</script>
|
20
|
+
|
21
|
+
<script type="module">
|
22
|
+
import { Application, Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js"
|
23
|
+
window.Stimulus = Application.start()
|
24
|
+
|
25
|
+
Stimulus.debug = true
|
26
|
+
Stimulus.register("scroll", class extends Controller {
|
27
|
+
// static targets = [ "scroll" ]
|
28
|
+
connect() {
|
29
|
+
this.element.scrollIntoView()
|
30
|
+
}
|
31
|
+
})
|
32
|
+
|
33
|
+
Stimulus.register("toggle-class", class extends Controller {
|
34
|
+
toggle(event) {
|
35
|
+
// Toggle 'active' class on button
|
36
|
+
event.currentTarget.classList.toggle('active');
|
37
|
+
|
38
|
+
// Toggle 'hidden' class on controller's root element
|
39
|
+
this.element.classList.toggle('hidden');
|
40
|
+
}
|
41
|
+
})
|
42
|
+
</script>
|
43
|
+
|
44
|
+
|
45
|
+
</head>
|
46
|
+
<body>
|
47
|
+
|
48
|
+
<%= yield %>
|
49
|
+
|
50
|
+
</body>
|
51
|
+
</html>
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<div class="p-2 rounded-sm hover:bg-black/5">
|
2
|
+
<p class="flex space-x-2">
|
3
|
+
<% if conversation.pinned? %>
|
4
|
+
<div class="w-5 h-5">
|
5
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
6
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0111.186 0z" />
|
7
|
+
</svg>
|
8
|
+
<% end %>
|
9
|
+
</div>
|
10
|
+
<%= link_to conversation.subject || "new chat", conversation_path(conversation), data: {"turbo-frame": "plain"} %>
|
11
|
+
</p>
|
12
|
+
<p class="text-xs">
|
13
|
+
<%= time_ago_in_words(conversation.created_at) %>
|
14
|
+
</p>
|
15
|
+
</div>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
<div class="p-2 rounded-sm hover:bg-black/5">
|
2
|
+
<%= link_to conversation_path(conversation), data: {"turbo-frame": "plain"} do %>
|
3
|
+
<div class="p-2 rounded-sm hover:bg-black/5">
|
4
|
+
|
5
|
+
<div class="flex space-x-2">
|
6
|
+
<% if conversation.pinned? %>
|
7
|
+
<div class="w-5 h-5">
|
8
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
9
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0111.186 0z" />
|
10
|
+
</svg>
|
11
|
+
</div>
|
12
|
+
<% end %>
|
13
|
+
|
14
|
+
<span><%= conversation.subject || "new chat" %> </span>
|
15
|
+
</div>
|
16
|
+
<p class="text-xs">
|
17
|
+
<%= time_ago_in_words(conversation.created_at) %>
|
18
|
+
</p>
|
19
|
+
</div>
|
20
|
+
<% end %>
|
21
|
+
</div>
|
@@ -0,0 +1,18 @@
|
|
1
|
+
<%= turbo_frame_tag "conversation-list-#{@current_page}" do %>
|
2
|
+
current: <%= @current_page %>
|
3
|
+
next: <%= @next_page %>
|
4
|
+
<div class="flex flex-col">
|
5
|
+
<% if @current_page.zero? %>
|
6
|
+
<span data-controller="scroll-to"></span>
|
7
|
+
<% end %>
|
8
|
+
<%= render partial: "plain/conversations/conversation_item", collection: @conversations, as: :conversation %>
|
9
|
+
<% if @conversations.size.positive? %>
|
10
|
+
<%= turbo_frame_tag "conversation-list-#{@next_page}", loading: :lazy,
|
11
|
+
src: conversations_path(page: @next_page),
|
12
|
+
class: 'empty:mt-[600px] empty:mb-[-600px] empty:block group' %>
|
13
|
+
<div class="mt-4 group-[&:not(:empty)]:hidden">
|
14
|
+
<%= t('conversations.loading') %>
|
15
|
+
</div>
|
16
|
+
<% end %>
|
17
|
+
</div>
|
18
|
+
<% end %>
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<%= turbo_frame_tag "conversation-list-#{@current_page}" do %>
|
2
|
+
<div class="divide divide-y">
|
3
|
+
<%= render partial: "plain/conversations/conversation_item", collection: @conversations, as: :conversation %>
|
4
|
+
</div>
|
5
|
+
<% if @conversations.size.positive? %>
|
6
|
+
<%= turbo_frame_tag "conversation-list-#{@next_page}", loading: :lazy,
|
7
|
+
src: conversations_path(page: @next_page),
|
8
|
+
class: 'empty:mt-[-300px] empty:mb-[300px] empty:block group' %>
|
9
|
+
<div class="mt-4 group-[&:not(:empty)]:hidden">
|
10
|
+
<%= t('conversations.loading') %>
|
11
|
+
</div>
|
12
|
+
<% end %>
|
13
|
+
<% end %>
|
@@ -0,0 +1,73 @@
|
|
1
|
+
<div class="relative z-50"
|
2
|
+
id="ai-assistant"
|
3
|
+
data-controller="toggle-class"
|
4
|
+
aria-labelledby="slide-over-title"
|
5
|
+
role="dialog"
|
6
|
+
aria-modal="true">
|
7
|
+
<!-- Background backdrop, show/hide based on slide-over state. -->
|
8
|
+
<div class="fixed inset-0-"></div>
|
9
|
+
|
10
|
+
<div class="fixed inset-0- overflow-hidden">
|
11
|
+
<div class="absolute inset-0 overflow-hidden">
|
12
|
+
<div class="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10 sm:pl-16">
|
13
|
+
<!--
|
14
|
+
Slide-over panel, show/hide based on slide-over state.
|
15
|
+
|
16
|
+
Entering: "transform transition ease-in-out duration-500 sm:duration-700"
|
17
|
+
From: "translate-x-full"
|
18
|
+
To: "translate-x-0"
|
19
|
+
Leaving: "transform transition ease-in-out duration-500 sm:duration-700"
|
20
|
+
From: "translate-x-0"
|
21
|
+
To: "translate-x-full"
|
22
|
+
-->
|
23
|
+
<div class="pointer-events-auto w-screen max-w-md">
|
24
|
+
<div class="flex h-full flex-col divide-y divide-gray-200 bg-white shadow-xl">
|
25
|
+
|
26
|
+
|
27
|
+
<div class="h-0 flex-1 overflow-y-auto bg-black/5">
|
28
|
+
<div class="bg-brand-700 px-4 py-6 sm:px-6">
|
29
|
+
<div class="flex items-center justify-between">
|
30
|
+
|
31
|
+
<% if @conversation.present? || @needs_back %>
|
32
|
+
<%= link_to plain.root_path, class: "p-4 rounded-md hover:bg-black/10" do %>
|
33
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
34
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15m0 0l6.75 6.75M4.5 12l6.75-6.75" />
|
35
|
+
</svg>
|
36
|
+
<% end %>
|
37
|
+
<% end %>
|
38
|
+
|
39
|
+
<h2 class="text-base font-semibold leading-6 text-white" id="slide-over-title">
|
40
|
+
PLAIN AI ASSISTANT
|
41
|
+
</h2>
|
42
|
+
|
43
|
+
<div class="ml-3 flex h-7 items-center">
|
44
|
+
<button type="button"
|
45
|
+
data-action="toggle-class#toggle"
|
46
|
+
class="rounded-md bg-brand-700 text-brand-200 hover:text-white focus:outline-none focus:ring-2 focus:ring-white">
|
47
|
+
<span class="sr-only">Close panel</span>
|
48
|
+
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
49
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
50
|
+
</svg>
|
51
|
+
</button>
|
52
|
+
</div>
|
53
|
+
</div>
|
54
|
+
<div class="mt-1 hidden">
|
55
|
+
<p class="text-sm text-brand-300">
|
56
|
+
Get started by filling in the information below to create your new project.
|
57
|
+
</p>
|
58
|
+
</div>
|
59
|
+
</div>
|
60
|
+
<div class="flex flex-1 flex-col justify-between">
|
61
|
+
<div class="divide-y divide-gray-200">
|
62
|
+
<div class="space-y-6-- py-6--">
|
63
|
+
<%= yield %>
|
64
|
+
</div>
|
65
|
+
</div>
|
66
|
+
</div>
|
67
|
+
</div>
|
68
|
+
</div>
|
69
|
+
</div>
|
70
|
+
</div>
|
71
|
+
</div>
|
72
|
+
</div>
|
73
|
+
</div>
|
@@ -0,0 +1,17 @@
|
|
1
|
+
<%= turbo_frame_tag "plain" do %>
|
2
|
+
<%= render "modal" do %>
|
3
|
+
<div class="border shadow-sm rounded-md bg-white m-4">
|
4
|
+
<h2 class=" p-4 inline-block text-2xl sm:text-3xl font-extrabold text-slate-900 tracking-tight dark:text-slate-200">
|
5
|
+
Continue conversations
|
6
|
+
</h2>
|
7
|
+
|
8
|
+
<div class="h-[calc(100vh-255px)] overflow-y-scroll group">
|
9
|
+
<%= render "plain/conversations/conversation_list" %>
|
10
|
+
</div>
|
11
|
+
|
12
|
+
<div class="m-2">
|
13
|
+
<%= link_to "New conversation", new_conversation_path, class: "rounded-md bg-brand-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-brand-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-600" %>
|
14
|
+
</div>
|
15
|
+
</div>
|
16
|
+
<% end %>
|
17
|
+
<% end %>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<%= turbo_frame_tag "plain" do %>
|
2
|
+
<%= render "modal" do %>
|
3
|
+
<%= turbo_stream_from(@conversation) %>
|
4
|
+
<div class="h-[calc(100vh-155px)] overflow-y-scroll">
|
5
|
+
<%= turbo_frame_tag "message-list-0", loading: :lazy,
|
6
|
+
src: plain.conversation_messages_path(@conversation, page: 0),
|
7
|
+
class: 'empty:block group' %>
|
8
|
+
<div id="new-messages-container"></div>
|
9
|
+
</div>
|
10
|
+
<div class="bg-red-600">
|
11
|
+
<%= render "plain/messages/form", conversation: @conversation %>
|
12
|
+
</div>
|
13
|
+
<% end %>
|
14
|
+
<% end %>
|
@@ -0,0 +1,61 @@
|
|
1
|
+
<footer class="mx-auto max-w-2xl space-y-10 pb-16 lg:max-w-5xl">
|
2
|
+
<div class="relative h-8">
|
3
|
+
<form class="absolute inset-0 flex items-center justify-center gap-6 md:justify-start">
|
4
|
+
<p class="text-sm text-zinc-600 dark:text-zinc-400">Was this page helpful?</p>
|
5
|
+
<div class="group grid h-8 grid-cols-[1fr,1px,1fr] overflow-hidden rounded-full border border-zinc-900/10 dark:border-white/10">
|
6
|
+
<button type="submit" class="px-3 text-sm font-medium text-zinc-600 transition hover:bg-zinc-900/2.5 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-white/5 dark:hover:text-white" data-response="yes">Yes</button>
|
7
|
+
<div class="bg-zinc-900/10 dark:bg-white/10"></div>
|
8
|
+
<button type="submit" class="px-3 text-sm font-medium text-zinc-600 transition hover:bg-zinc-900/2.5 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-white/5 dark:hover:text-white" data-response="no">No</button>
|
9
|
+
</div>
|
10
|
+
</form>
|
11
|
+
</div>
|
12
|
+
<div class="flex">
|
13
|
+
<% if @file.present? and @file[:prev] %>
|
14
|
+
<div class="flex flex-col items-start gap-3">
|
15
|
+
<a class="inline-flex gap-0.5 justify-center overflow-hidden text-sm font-medium transition rounded-full bg-zinc-100 py-1 px-3 text-zinc-900 hover:bg-zinc-200 dark:bg-zinc-800/40 dark:text-zinc-400 dark:ring-1 dark:ring-inset dark:ring-zinc-800 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
|
16
|
+
aria-label="Previous: <%= @file[:prev][:name] %>"
|
17
|
+
href="/plain/docs/<%= @file[:prev][:path] %>">
|
18
|
+
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" class="mt-0.5 h-5 w-5 -ml-1 rotate-180"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="m11.5 6.5 3 3.5m0 0-3 3.5m3-3.5h-9"></path>
|
19
|
+
</svg>
|
20
|
+
Previous
|
21
|
+
</a>
|
22
|
+
<a tabindex="-1" aria-hidden="true" class="text-base font-semibold text-zinc-900 transition hover:text-zinc-600 dark:text-white dark:hover:text-zinc-300"
|
23
|
+
href="/plain/docs/<%= @file[:prev][:path] %>">
|
24
|
+
<%= @file[:prev][:name] %>
|
25
|
+
</a>
|
26
|
+
</div>
|
27
|
+
<% end %>
|
28
|
+
|
29
|
+
<% if @file.present? and @file[:next] %>
|
30
|
+
<div class="ml-auto flex flex-col items-end gap-3">
|
31
|
+
<a class="inline-flex gap-0.5 justify-center overflow-hidden text-sm font-medium transition rounded-full bg-zinc-100 py-1 px-3 text-zinc-900 hover:bg-zinc-200 dark:bg-zinc-800/40 dark:text-zinc-400 dark:ring-1 dark:ring-inset dark:ring-zinc-800 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
|
32
|
+
aria-label="Next: <%= @file[:next][:name] %>"
|
33
|
+
href="/plain/docs/<%= @file[:next][:path] %>">
|
34
|
+
Next
|
35
|
+
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" class="mt-0.5 h-5 w-5 -mr-1">
|
36
|
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="m11.5 6.5 3 3.5m0 0-3 3.5m3-3.5h-9"></path>
|
37
|
+
</svg>
|
38
|
+
</a>
|
39
|
+
<a tabindex="-1" aria-hidden="true" class="text-base font-semibold text-zinc-900 transition hover:text-zinc-600 dark:text-white dark:hover:text-zinc-300"
|
40
|
+
href="/plain/docs/<%= @file[:next][:path] %>">
|
41
|
+
<%= @file[:next][:name] %>
|
42
|
+
</a>
|
43
|
+
</div>
|
44
|
+
<% end %>
|
45
|
+
</div>
|
46
|
+
|
47
|
+
<div class="flex flex-col items-center justify-between gap-5 border-t border-zinc-900/5 pt-8 dark:border-white/5 sm:flex-row">
|
48
|
+
<p class="text-xs text-zinc-600 dark:text-zinc-400">
|
49
|
+
<%= @config.dig("footer", "legend") || "© Copyright #{Time.now.year} . All rights reserved." %>
|
50
|
+
</p>
|
51
|
+
<div class="flex gap-4">
|
52
|
+
<% links = @config.dig("footer", "links") %>
|
53
|
+
<% if links.is_a?(Array) %>
|
54
|
+
<% @config.dig("footer", "links").each do |link| %>
|
55
|
+
<%= link_to link["name"], link["url"] %>
|
56
|
+
<% end %>
|
57
|
+
<% end %>
|
58
|
+
|
59
|
+
</div>
|
60
|
+
</div>
|
61
|
+
</footer>
|
@@ -0,0 +1,20 @@
|
|
1
|
+
|
2
|
+
<ul role="list" class="border-l border-transparent">
|
3
|
+
|
4
|
+
|
5
|
+
<% nodes.each do |node| %>
|
6
|
+
<% if node[:type] == 'directory' %>
|
7
|
+
<li class="relative">
|
8
|
+
<span class="<%= level == 1 ? 'text-xs font-semibold text-zinc-900 dark:text-white' : 'flex justify-between gap-2 py-1 pr-3 text-sm transition pl-4 text-zinc-900 dark:text-white'%>">
|
9
|
+
<%= node[:name].capitalize.humanize %>
|
10
|
+
</span>
|
11
|
+
<%= render partial: 'menu', locals: { nodes: node[:children], level: level + 1 } %>
|
12
|
+
</li>
|
13
|
+
<% else %>
|
14
|
+
<li class="relative">
|
15
|
+
<%= link_to node[:name], docs_path(file_path: node[:path]), class: level == 1 ? 'text-xs font-semibold text-zinc-900 dark:text-white' : 'flex justify-between gap-2 py-1 pr-3 text-sm transition pl-4 text-zinc-900 dark:text-white' rescue "nil" %>
|
16
|
+
</li>
|
17
|
+
<% end %>
|
18
|
+
<% end %>
|
19
|
+
|
20
|
+
</ul>
|