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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +162 -0
  3. data/Rakefile +8 -0
  4. data/app/assets/builds/plain.css +1 -0
  5. data/app/assets/config/plain_manifest.js +1 -0
  6. data/app/assets/stylesheets/plain/application.css +15 -0
  7. data/app/assets/stylesheets/plain/application.tailwind.css +7 -0
  8. data/app/controllers/plain/application_controller.rb +4 -0
  9. data/app/controllers/plain/conversations_controller.rb +55 -0
  10. data/app/controllers/plain/docs_controller.rb +63 -0
  11. data/app/controllers/plain/home_controller.rb +10 -0
  12. data/app/controllers/plain/messages_controller.rb +30 -0
  13. data/app/helpers/plain/application_helper.rb +4 -0
  14. data/app/helpers/plain/conversations_helper.rb +4 -0
  15. data/app/helpers/plain/docs_helper.rb +4 -0
  16. data/app/helpers/plain/home_helper.rb +4 -0
  17. data/app/helpers/plain/messages_helper.rb +4 -0
  18. data/app/javascripts/loader_controller.js +27 -0
  19. data/app/javascripts/scroll_controller.js +8 -0
  20. data/app/javascripts/toggle_class_controller.js +11 -0
  21. data/app/jobs/plain/application_job.rb +4 -0
  22. data/app/jobs/plain/message_processor_job.rb +9 -0
  23. data/app/mailers/plain/application_mailer.rb +6 -0
  24. data/app/models/plain/application_record.rb +5 -0
  25. data/app/models/plain/conversation.rb +53 -0
  26. data/app/models/plain/message.rb +62 -0
  27. data/app/services/plain/ai_docs.rb +163 -0
  28. data/app/services/plain/docs_service.rb +127 -0
  29. data/app/views/layouts/plain/application.html.erb +51 -0
  30. data/app/views/plain/conversations/_conversation_item.erb +15 -0
  31. data/app/views/plain/conversations/_conversation_item.html.erb +21 -0
  32. data/app/views/plain/conversations/_conversation_list.erb +18 -0
  33. data/app/views/plain/conversations/_conversation_list.html.erb +13 -0
  34. data/app/views/plain/conversations/_modal.html.erb +73 -0
  35. data/app/views/plain/conversations/index.html.erb +17 -0
  36. data/app/views/plain/conversations/show.html.erb +14 -0
  37. data/app/views/plain/docs/_footer.erb +61 -0
  38. data/app/views/plain/docs/_menu.erb +20 -0
  39. data/app/views/plain/docs/_sections.erb +73 -0
  40. data/app/views/plain/docs/show.html.erb +126 -0
  41. data/app/views/plain/home/index.html.erb +35 -0
  42. data/app/views/plain/messages/_form.html.erb +25 -0
  43. data/app/views/plain/messages/_message_item.html.erb +26 -0
  44. data/app/views/plain/messages/_message_list.html.erb +16 -0
  45. data/app/views/plain/messages/_new_message.turbo_stream.erb +3 -0
  46. data/app/views/plain/messages/create.turbo_stream.erb +8 -0
  47. data/app/views/plain/messages/index.html.erb +1 -0
  48. data/config/database.yml +53 -0
  49. data/config/routes.rb +11 -0
  50. data/db/migrate/20230729010703_create_plain_conversations.rb +10 -0
  51. data/db/migrate/20230729010745_create_plain_messages.rb +11 -0
  52. data/lib/plain/configuration.rb +12 -0
  53. data/lib/plain/engine.rb +16 -0
  54. data/lib/plain/version.rb +3 -0
  55. data/lib/plain.rb +17 -0
  56. data/lib/tasks/plain_tasks.rake +21 -0
  57. 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>