plain-rails 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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>
|