plain-rails 0.1.0

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