mirror of
https://github.com/indentlabs/notebook.git
synced 2025-10-26 11:19:22 +00:00
Merge branch 'master' into tailwind-redesign
This commit is contained in:
commit
e27b133207
196
app/controllers/conversation_controller.rb
Normal file
196
app/controllers/conversation_controller.rb
Normal file
@ -0,0 +1,196 @@
|
||||
class ConversationController < ApplicationController
|
||||
before_action :authenticate_user!, only: [:character_index]
|
||||
|
||||
before_action :set_character, only: [:character_landing, :export]
|
||||
before_action :ensure_character_privacy, only: [:character_landing, :export]
|
||||
|
||||
def character_index
|
||||
@characters = @current_user_content.fetch('Character', [])
|
||||
end
|
||||
|
||||
def character_landing
|
||||
@first_greeting = default_character_greeting
|
||||
@personality = personality_for_character
|
||||
@description = description_for_character
|
||||
end
|
||||
|
||||
def export
|
||||
name = open_characters_persona_params.fetch('name', 'New character').strip
|
||||
description = open_characters_persona_params.fetch('description', '')
|
||||
|
||||
add_character_hash = base_open_characters_export.merge({
|
||||
"uuid": deterministic_uuid(@character.id),
|
||||
"name": name,
|
||||
"roleInstruction": full_role_instruction,
|
||||
"reminderMessage": reminder_message,
|
||||
})
|
||||
|
||||
# Add a character image if one has been uploaded to the page
|
||||
avatar = @character.random_image_including_private
|
||||
add_character_hash[:avatar][:url] = avatar if avatar.present?
|
||||
|
||||
# Provide a default scenario if one wasn't given
|
||||
add_character_hash[:scenario] ||= default_scenario
|
||||
|
||||
# Redirect to OpenCharacters
|
||||
base_oc_url = 'https://josephrocca.github.io/OpenCharacters/'
|
||||
oc_params = { addCharacter: add_character_hash }
|
||||
|
||||
redirect_to "#{base_oc_url}##{ERB::Util.url_encode(oc_params.to_json)}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def deterministic_uuid(id)
|
||||
static_prefix = "notebook-"
|
||||
hashed_id = Digest::SHA1.hexdigest(static_prefix + id.to_s)
|
||||
uuid = "#{hashed_id[0..7]}-#{hashed_id[8..11]}-#{hashed_id[12..15]}-#{hashed_id[16..19]}-#{hashed_id[20..31]}"
|
||||
uuid
|
||||
end
|
||||
|
||||
def full_role_instruction
|
||||
final_text = [
|
||||
"[SYSTEM]: You are roleplaying as #{@character.name}, #{personality_for_character}",
|
||||
"",
|
||||
"Follow this pattern:",
|
||||
"\"Hello!\" - dialogue",
|
||||
"*He jumps out of the bushes.* - action",
|
||||
"",
|
||||
"#{@character.name}'s personality is below:",
|
||||
"#{open_characters_persona_params.fetch('description', '(not included)')}",
|
||||
"",
|
||||
"#{@character.name} will now respond while staying in character in an extremely descriptive manner at length, avoiding being repetitive, without advancing events by herself, avoiding implying conversations without a reply from the user first, and wait for the user's reply to advance events. Describe what #{@character.name} is feeling, saying, and doing with rich detail, but do not include any parenthetical thoughts and focus primarily on dialogue.",
|
||||
].join("\n")
|
||||
end
|
||||
|
||||
def reminder_message
|
||||
"[SYSTEM]: (Thought: I need to rememeber to be creative, descriptive, and engaging! I should work very hard to avoid being repetitive as well! Unless the user speaks to me OOC, with parentheses around their input, first, I will not say anything OOC or in parentheses. I shouldn't ignore parts of the user's post, even if they move on to a new scene. I should at least write my characters thoughts and feelings towards the prior scene before continuing with the new one. I don't need to feel the need to write a long response if the user's post is short. In that case, I can feel free to write a short response myself - making sure to not take over writing the user's character's dialogue, thoughts, or actions!)"
|
||||
end
|
||||
|
||||
def personality_for_character
|
||||
hobbies = @character.get_field_value('Nature', 'Hobbies')
|
||||
|
||||
personality_parts = [
|
||||
"a #{@character.get_field_value('Overview', 'Gender', fallback='')} #{@character.get_field_value('Overview', 'Role', fallback='character')}, age #{@character.get_field_value('Overview', 'Age', fallback='irrelevant')}."
|
||||
]
|
||||
personality_parts << "Their hobbies include #{hobbies}." if hobbies.present?
|
||||
|
||||
ContentFormatterService.plaintext_show(text: personality_parts.join(' '), viewing_user: current_user)
|
||||
end
|
||||
|
||||
def description_for_character
|
||||
occupation = @character.get_field_value('Social', 'Occupation')
|
||||
background = @character.get_field_value('History', 'Background')
|
||||
motivations = @character.get_field_value('Nature', 'Motivations')
|
||||
mannerisms = @character.get_field_value('Nature', 'Mannerisms')
|
||||
flaws = @character.get_field_value('Nature', 'Flaws')
|
||||
prejudices = @character.get_field_value('Nature', 'Prejudices')
|
||||
talents = @character.get_field_value('Nature', 'Talents')
|
||||
hobbies = @character.get_field_value('Nature', 'Hobbies')
|
||||
|
||||
description_parts = []
|
||||
description_parts.concat ["OCCUPATION", occupation, nil] if occupation.present?
|
||||
description_parts.concat ["BACKGROUND", background, nil] if background.present?
|
||||
description_parts.concat ["MOTIVATIONS", motivations, nil] if motivations.present?
|
||||
description_parts.concat ["MANNERISMS", mannerisms, nil] if mannerisms.present?
|
||||
description_parts.concat ["FLAWS", flaws, nil] if flaws.present?
|
||||
description_parts.concat ["PREJUDICES", prejudices, nil] if prejudices.present?
|
||||
description_parts.concat ["TALENTS", talents, nil] if talents.present?
|
||||
description_parts.concat ["HOBBIES", hobbies, nil] if hobbies.present?
|
||||
|
||||
ContentFormatterService.plaintext_show(text: description_parts.join("\n"), viewing_user: current_user)
|
||||
end
|
||||
|
||||
def set_character
|
||||
@character = Character.find(params[:character_id].to_i)
|
||||
end
|
||||
|
||||
def ensure_character_privacy
|
||||
unless (user_signed_in? && @character.user == current_user) || @character.privacy == 'public'
|
||||
redirect_to root_path, notice: "That character is private!"
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
def open_characters_persona_params
|
||||
params.permit(:name, :avatar, :scenario, :char_greeting, :personality, :description, :example_dialogue)
|
||||
end
|
||||
|
||||
def default_scenario
|
||||
"This character is interacting with the user within their own fictional universe. The character will respond in a way that is consistent with their personality and background."
|
||||
end
|
||||
|
||||
def default_custom_code
|
||||
""
|
||||
|
||||
# TODO maybe include our standard (whisper) formatting on messages?
|
||||
end
|
||||
|
||||
def base_open_characters_export
|
||||
{
|
||||
"name": "New character",
|
||||
"modelName": "gpt-3.5-turbo",
|
||||
"fitMessagesInContextMethod": "summarizeOld",
|
||||
"associativeMemoryMethod": "v1",
|
||||
"associativeMemoryEmbeddingModelName": "text-embedding-ada-002",
|
||||
"temperature": 0.7,
|
||||
"customCode": default_custom_code,
|
||||
"initialMessages": default_initial_messages,
|
||||
"avatar": {
|
||||
"url": "",
|
||||
"size": 1,
|
||||
"shape": "square"
|
||||
},
|
||||
"scene": {
|
||||
"background": {
|
||||
"url": ""
|
||||
},
|
||||
"music": {
|
||||
"url": ""
|
||||
}
|
||||
},
|
||||
"userCharacter": {
|
||||
"avatar": {
|
||||
"url": current_user.avatar.url,
|
||||
"size": 1,
|
||||
"shape": "circle"
|
||||
}
|
||||
},
|
||||
"streamingResponse": true
|
||||
}
|
||||
end
|
||||
|
||||
def default_initial_messages
|
||||
[
|
||||
{
|
||||
"author": "system",
|
||||
"content": "Scenario: " + (open_characters_persona_params.fetch('scenario', nil).presence || default_scenario),
|
||||
"hiddenFrom": [] # "ai", "user", "both", "neither"
|
||||
},
|
||||
{
|
||||
"author": "ai",
|
||||
"content": open_characters_persona_params.fetch('char_greeting', default_character_greeting),
|
||||
"hiddenFrom": []
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
def default_export_metadata
|
||||
{
|
||||
"version": 1,
|
||||
"created": @content.created_at.to_i,
|
||||
"modified": @content.updated_at.to_i,
|
||||
"ncid": @content.id,
|
||||
"source": "https://www.notebook.ai/plan/characters#{@content.id}",
|
||||
"tool": {
|
||||
"name": "Notebook.ai Persona Export",
|
||||
"version": "1.0.0",
|
||||
"url": "https://www.notebook.ai"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def default_character_greeting
|
||||
"Hello!"
|
||||
end
|
||||
end
|
||||
@ -1,2 +0,0 @@
|
||||
module BasilHelper
|
||||
end
|
||||
@ -300,6 +300,34 @@ module HasAttributes
|
||||
.detect { |v| v.entity_id == self.id }&.value.presence || (self.respond_to?(label.downcase) ? self.read_attribute(label.downcase) : nil)
|
||||
end
|
||||
|
||||
def get_field_value(category, field, fallback=nil)
|
||||
category = AttributeCategory.find_by(
|
||||
label: category,
|
||||
entity_type: self.class.name.downcase,
|
||||
user_id: self.user_id,
|
||||
hidden: [nil, false]
|
||||
)
|
||||
return fallback if category.nil?
|
||||
|
||||
field = AttributeField.find_by(
|
||||
label: field,
|
||||
attribute_category_id: category.id,
|
||||
user_id: self.user_id,
|
||||
hidden: [nil, false]
|
||||
)
|
||||
return fallback if field.nil?
|
||||
|
||||
answer = Attribute.find_by(
|
||||
attribute_field_id: field.id,
|
||||
entity_type: self.class.name,
|
||||
entity_id: self.id,
|
||||
user_id: self.user_id
|
||||
)
|
||||
return fallback if answer.nil?
|
||||
|
||||
answer.value
|
||||
end
|
||||
|
||||
def self.field_type_for(category, field)
|
||||
if field[:label] == 'Name' && category.name == 'overview'
|
||||
"name"
|
||||
|
||||
@ -26,7 +26,7 @@ module HasImageUploads
|
||||
|
||||
# If we don't have any uploaded images, we look for saved Basil commissions
|
||||
if result.nil? && respond_to?(:basil_commissions)
|
||||
result = basil_commissions.where.not(saved_at: nil).sample.try(:image)
|
||||
result = basil_commissions.where.not(saved_at: nil).includes([:image_attachment]).sample.try(:image)
|
||||
end
|
||||
|
||||
# Finally, if we have no image upload, we return the default image for this type
|
||||
|
||||
@ -12,7 +12,7 @@ class ContentPage < ApplicationRecord
|
||||
# TODO: this is gonna be an N+1 query any time we display a list of ContentPages with images
|
||||
def random_image_including_private(format: :small)
|
||||
ImageUpload.where(content_type: self.page_type, content_id: self.id).sample.try(:src, format) \
|
||||
|| BasilCommission.where(entity_type: self.page_type, entity_id: self.id).where.not(saved_at: nil).sample.try(:image) \
|
||||
|| BasilCommission.where(entity_type: self.page_type, entity_id: self.id).where.not(saved_at: nil).includes([:image_attachment]).sample.try(:image) \
|
||||
|| ActionController::Base.helpers.asset_path("card-headers/#{self.page_type.downcase.pluralize}.webp")
|
||||
end
|
||||
|
||||
|
||||
@ -60,7 +60,7 @@ class User < ApplicationRecord
|
||||
has_many :user_blockings, dependent: :destroy
|
||||
has_many :blocked_users, through: :user_blockings, source: :blocked_user
|
||||
def blocked_by_users
|
||||
User.where(id: UserBlocking.where(blocked_user_id: self.id).pluck(:user_id))
|
||||
@cached_blocked_by_users ||= User.where(id: UserBlocking.where(blocked_user_id: self.id).pluck(:user_id))
|
||||
end
|
||||
def blocked_by?(user)
|
||||
blocked_by_users.pluck(:id).include?(user.id)
|
||||
@ -208,7 +208,7 @@ class User < ApplicationRecord
|
||||
end
|
||||
|
||||
def image_url(size=80)
|
||||
if avatar.attached? # manually-uploaded avatar
|
||||
@cached_user_image_url ||= if avatar.attached? # manually-uploaded avatar
|
||||
Rails.application.routes.url_helpers.rails_representation_url(avatar.variant(resize_to_limit: [size, size]).processed, only_path: true)
|
||||
|
||||
else # otherwise, grab the default from Gravatar for this email address
|
||||
@ -245,7 +245,7 @@ class User < ApplicationRecord
|
||||
end
|
||||
|
||||
def active_promo_codes
|
||||
PageUnlockPromoCode.where(id: active_promotions.pluck(:page_unlock_promo_code_id))
|
||||
@cached_active_promo_codes ||= PageUnlockPromoCode.where(id: active_promotions.pluck(:page_unlock_promo_code_id))
|
||||
end
|
||||
|
||||
def initialize_stripe_customer
|
||||
|
||||
@ -21,6 +21,16 @@ class ContentFormatterService < Service
|
||||
# https://s3.amazonaws.com/raw.paste.esk.io/Llb%2F64DJHK?versionId=19Lb_TtukDbo1J_IoCpkr.d.pwpW_vmH
|
||||
VALID_LINK_CLASSES = Rails.application.config.content_type_names[:all] + %w(Timeline Document)
|
||||
|
||||
def self.plaintext_show(text:, viewing_user: User.new)
|
||||
formatted_text = markdown.render(text || '').html_safe
|
||||
|
||||
tokens_to_replace(text).each do |token|
|
||||
text.gsub!(token[:matched_string], replacement_for_token(token, viewing_user, true))
|
||||
end
|
||||
|
||||
text
|
||||
end
|
||||
|
||||
def self.show(text:, viewing_user: User.new)
|
||||
# We want to evaluate markdown first, because the markdown engine also happens
|
||||
# to strip out HTML tags. So: markdown, _then_ insert content links.
|
||||
@ -67,7 +77,7 @@ class ContentFormatterService < Service
|
||||
end
|
||||
end
|
||||
|
||||
def self.replacement_for_token(token, viewing_user)
|
||||
def self.replacement_for_token(token, viewing_user, plaintext=false)
|
||||
return unknown_link_template(token) unless token.key?(:content_type) && token.key?(:content_id)
|
||||
begin
|
||||
content_class = token[:content_type].titleize.constantize
|
||||
@ -81,9 +91,17 @@ class ContentFormatterService < Service
|
||||
return unknown_link_template(token) unless content_model.present?
|
||||
|
||||
if content_model.readable_by?(viewing_user)
|
||||
link_template(content_model)
|
||||
if plaintext
|
||||
plaintext_replacement_template(content_model)
|
||||
else
|
||||
link_template(content_model)
|
||||
end
|
||||
else
|
||||
private_link_template(content_model)
|
||||
if plaintext
|
||||
plaintext_replacement_template(content_model)
|
||||
else
|
||||
private_link_template(content_model)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -95,6 +113,10 @@ class ContentFormatterService < Service
|
||||
inline_template(content_model.class) { link_to(content_model.name, link_for(content_model), class: 'grey-text content_link disabled') }
|
||||
end
|
||||
|
||||
def self.plaintext_replacement_template(content_model)
|
||||
content_model.name
|
||||
end
|
||||
|
||||
def self.unknown_link_template(attempted_key)
|
||||
attempted_key[:matched_string]
|
||||
end
|
||||
|
||||
@ -1,20 +1,34 @@
|
||||
<%
|
||||
creating = defined?(creating) && creating
|
||||
editing = defined?(editing) && editing
|
||||
page_type_enabled = BasilService::ENABLED_PAGE_TYPES.include? content.class_name
|
||||
show_basil_tool = BasilService::ENABLED_PAGE_TYPES.include? content.class_name
|
||||
show_conversation = false && content.class_name == 'Character'
|
||||
|
||||
show_tools_menu = show_basil_tool || show_conversation
|
||||
%>
|
||||
|
||||
<% if page_type_enabled %>
|
||||
<% if show_tools_menu %>
|
||||
<ul class="collection content-tabs">
|
||||
<li class="active center grey-text uppercase">
|
||||
Tools
|
||||
</li>
|
||||
|
||||
<li class="collection-item">
|
||||
<%= link_to basil_content_path(content_type: content.class_name, id: content.id) do %>
|
||||
<i class="material-icons left">palette</i>
|
||||
Image Generation
|
||||
<% end %>
|
||||
</li>
|
||||
<% if show_basil_tool %>
|
||||
<li class="collection-item">
|
||||
<%= link_to basil_content_path(content_type: content.class_name, id: content.id) do %>
|
||||
<i class="material-icons left">palette</i>
|
||||
Image Generation
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
|
||||
<% if show_conversation %>
|
||||
<li class="collection-item">
|
||||
<%= link_to talk_path(character_id: content.id) do %>
|
||||
<i class="material-icons left">message</i>
|
||||
Talk to <%= content.name %>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
60
app/views/conversation/character_index.html.erb
Normal file
60
app/views/conversation/character_index.html.erb
Normal file
@ -0,0 +1,60 @@
|
||||
<div class="row">
|
||||
<div class="col s12">
|
||||
<h1 style="font-size: 2rem">
|
||||
<i class="material-icons float-right ">forum</i>
|
||||
Talk to your characters
|
||||
</h1>
|
||||
<p>
|
||||
There are a lot of new sites popping up for talking to AI characters. This page lets you
|
||||
export your Notebook.ai characters to those sites, starting with the open-source project
|
||||
OpenCharacters.
|
||||
</p>
|
||||
<p>
|
||||
Please note that these services aren't a part of Notebook.ai; this is just a way to get your
|
||||
Notebook.ai characters into those sites! To get started, click on any character below.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col s12">
|
||||
<% if @universe_scope %>
|
||||
<div class="card-panel <%= Universe.color %> white-text">
|
||||
<i class="material-icons left"><%= Universe.icon %></i>
|
||||
Showing <%= pluralize @characters.count, 'character' %> from <%= @universe_scope.name %>.
|
||||
<%= link_to "Show characters from all universes instead.", conversation_path(universe: "all"), class: 'purple-text text-lighten-4' %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="row">
|
||||
<% @characters.each do |content| %>
|
||||
<%= link_to talk_path(content) do %>
|
||||
<div class="col s12 m4 l3">
|
||||
<div class="hoverable card">
|
||||
<div class="card-image">
|
||||
<%= image_tag content.random_image_including_private(format: :medium), style: 'height: 200px' %>
|
||||
<span class="card-title"><%= content.name %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% if @characters.empty? %>
|
||||
<div class="center">
|
||||
<strong>You haven't created any character pages yet.</strong>
|
||||
<br /><br />
|
||||
|
||||
<%= link_to new_character_path, class: '' do %>
|
||||
<div class="hoverable card-panel <%= Character.color %> white-text" style="width: 33%; margin: 0 auto">
|
||||
<i class="material-icons left">add</i>
|
||||
Create character
|
||||
</div>
|
||||
<% end %>
|
||||
<div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
94
app/views/conversation/character_landing.html.erb
Normal file
94
app/views/conversation/character_landing.html.erb
Normal file
@ -0,0 +1,94 @@
|
||||
<div class="row">
|
||||
<div class="col s12 m4">
|
||||
<%= link_to @character do %>
|
||||
<%= image_tag @character.random_image_including_private, style: "width: 100%" %>
|
||||
<% end %>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="card-title">
|
||||
<strong>Talk to <%= @character.name %></strong>
|
||||
</div>
|
||||
<p>
|
||||
You can now export your Notebook.ai characters to the open-source project OpenCharacters
|
||||
and talk to them in real-time! This is a great way to get to know your characters a little
|
||||
better or to roleplay with them.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p style="font-size: 0.9em">
|
||||
The fields will be used to create a conversational AI persona of
|
||||
your character. You can edit these fields before exporting if you want to change how your
|
||||
character talks. If your character is public, you can also share this page with others
|
||||
to let them talk to your character, too!
|
||||
</p>
|
||||
<div>
|
||||
<%= link_to "Back to notebook page", @character %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col s12 m8">
|
||||
<%= form_tag export_character_path(@character.id), method: :post, target: '_blank' do |f| %>
|
||||
|
||||
<%= hidden_field_tag "name", @character.name %>
|
||||
<%= hidden_field_tag "avatar", @character.random_image_including_private %>
|
||||
<%# TODO background image/music/etc? %>
|
||||
|
||||
<div class="card-panel">
|
||||
<p class="center">
|
||||
<strong>Persona export for <%= @character.name %></strong>
|
||||
<% if user_signed_in? && @character.user_id == current_user.id %>
|
||||
<br />
|
||||
<em>(editable because you created <%= @character.name %>)</em>
|
||||
<% end %>
|
||||
</p>
|
||||
<br /><br />
|
||||
|
||||
<div class="input-field">
|
||||
<%= text_area_tag 'scenario', nil, disabled: false, style: 'min-height: 100px', placeholder: "Is there a specific scenario/context you want to have this conversation in?", class: 'materialize-textarea' %>
|
||||
<label for="scenario">Optional: Scenario</label>
|
||||
</div>
|
||||
|
||||
<div class="input-field">
|
||||
<%= text_area_tag 'char_greeting', @first_greeting, disabled: false, placeholder: "This will be the first thing your character says to you. It can be a simple greeting, or you can use it to set a specific topic, tone, or speaking style.", class: 'materialize-textarea' %>
|
||||
<label for="char_greeting">First greeting from <%= @character.name %></label>
|
||||
</div>
|
||||
|
||||
<div class="input-field">
|
||||
<%= text_area_tag 'personality', @personality, disabled: false, class: 'materialize-textarea' %>
|
||||
<label for="personality">Brief personality</label>
|
||||
</div>
|
||||
|
||||
<div class="input-field">
|
||||
<%= text_area_tag 'description', @description, disabled: false, class: 'materialize-textarea' %>
|
||||
<label for="description">In-depth personality</label>
|
||||
</div>
|
||||
|
||||
<div class="input-field">
|
||||
<%= text_area_tag 'example_dialogue', nil, disabled: false, style: 'min-height: 100px', placeholder: "If you have any dialogue examples, quotes, or other phrases your character says, you can use this field to include them and adjust their speaking style closer to the examples. Write as little or as much as you'd like!", class: 'materialize-textarea' %>
|
||||
<label for="example_dialogue">Optional: More dialogue examples</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-panel">
|
||||
<div class="center">
|
||||
<strong>Note: An OpenAI API key is required by OpenCharacters</strong>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
OpenCharacters uses this persona data with OpenAI's GPT models
|
||||
in a web app that runs entirely in your browser, rather than being hosted or stored on any server.
|
||||
This means that you will need a valid OpenAI key to use OpenCharacters.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<div class="center">
|
||||
<%= submit_tag "Chat with #{@character.name}", class: 'hoverable btn blue white-text' %>
|
||||
<span class="grey-text text-darken-1" style="margin-left: 0.5rem">using OpenCharacters</span>
|
||||
</div>
|
||||
|
||||
<% 10.times do %><br /><% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@ -26,6 +26,12 @@ Rails.application.routes.draw do
|
||||
get '/:content_type/:id', to: 'basil#content', as: :basil_content
|
||||
post '/:content_type/:id', to: 'basil#commission', as: :basil_commission
|
||||
end
|
||||
|
||||
scope :talk do
|
||||
get '/', to: 'conversation#character_index', as: :conversation
|
||||
get '/to/:character_id', to: 'conversation#character_landing', as: :talk
|
||||
post '/export/:character_id', to: 'conversation#export', as: :export_character
|
||||
end
|
||||
end
|
||||
|
||||
scope :stream, path: '/stream', as: :stream do
|
||||
|
||||
8
test/controllers/conversation_controller_test.rb
Normal file
8
test/controllers/conversation_controller_test.rb
Normal file
@ -0,0 +1,8 @@
|
||||
require "test_helper"
|
||||
|
||||
class ConversationControllerTest < ActionDispatch::IntegrationTest
|
||||
test "should get content" do
|
||||
get conversation_content_url
|
||||
assert_response :success
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue
Block a user