Merge branch 'master' into tailwind-redesign

This commit is contained in:
Andrew Brown 2023-06-13 19:19:07 -07:00
commit 0ff6d4dffc
27 changed files with 1174 additions and 289 deletions

View File

@ -5,30 +5,62 @@
# manually is the preferred setup for Notebook.ai instances.
# The image to build from.
FROM ruby:2.7.6
FROM ruby:3.2.1
# Properties/labels for the image.
LABEL maintainer="Notebook.ai Contributors"
# Development ENV by default, but overridable with RAILS_ENV
ARG RAILS_ENV=development
ENV RAILS_ENV=${RAILS_ENV}
# Copy the current folder into the notebookai user's home directory.
COPY . /home/notebookai
# Set the notebookai user's home directory as the working directory.
WORKDIR /home/notebookai
# Prep the image for runtime, this should all be done in one command
# to minimize image layers.
# See: https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#minimize-the-number-of-layers
# Create the notebookai user and group
RUN groupadd --system --gid 1000 notebookai && \
useradd --system --home-dir /home/notebookai --gid notebookai --uid 1000 --shell /bin/bash notebookai && \
useradd --system --home-dir /home/notebookai --gid notebookai --uid 1000 --shell /bin/bash notebookai
# Install system dependencies
RUN curl -sL https://deb.nodesource.com/setup_16.x | bash - && \
apt-get update -qq && \
apt-get install -y build-essential libpq-dev nodejs imagemagick libmagickwand-dev && \
rm --recursive --force /var/lib/apt/lists/* && \
bundle install && \
rails db:setup
apt-get install -y build-essential libpq-dev nodejs imagemagick libmagickwand-dev curl && \
rm --recursive --force /var/lib/apt/lists/*
# Install yarn
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
apt-get update && \
apt-get install -y yarn
# Install app dependencies, compile assets, and set up the database
RUN bundle install && \
yarn install && \
if [ "$RAILS_ENV" = "development" ] ; then \
echo "Starting webpack-dev-server..." && \
bin/webpack-dev-server & \
else \
echo "Compiling webpack assets..." && \
rails webpacker:compile ; \
fi && \
rails db:setup && \
rake db:migrate && \
rm -f tmp/pids/server.pid
# This image should expose the port 3000.
# This does not actually expose the port, you'll have to expose it yourself by
# using `-p 3000:3000/tcp` in Docker's CLI or `- "3000:3000"` in the in docker-compose.yml service's ports[].
# https://docs.docker.com/engine/reference/builder/#expose
#EXPOSE 3000/tcp
# This image should expose port 3000.
EXPOSE 3000/tcp
# Finally, start the server using Puma!
CMD bundle exec puma -C config/puma.rb -e ${RAILS_ENV} -b tcp://0.0.0.0:3000
# And run it with
# docker run --name nb-webserver -p 80:3000 -d notebookai

View File

@ -115,7 +115,7 @@ group :production do
end
group :test, :production do
gem 'pg', '~> 1.4'
gem 'pg', '~> 1.5'
gem "mini_racer", "~> 0.6.3" # TODO: audit whether we can remove this
end

View File

@ -133,7 +133,7 @@ GEM
autoprefixer-rails (10.4.7.0)
execjs (~> 2)
aws-eventstream (1.2.0)
aws-partitions (1.746.0)
aws-partitions (1.760.0)
aws-sdk (3.1.0)
aws-sdk-resources (~> 3)
aws-sdk-accessanalyzer (1.23.0)
@ -331,7 +331,7 @@ GEM
aws-sdk-connectparticipant (1.14.0)
aws-sdk-core (~> 3, >= 3.120.0)
aws-sigv4 (~> 1.1)
aws-sdk-core (3.171.0)
aws-sdk-core (3.171.1)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
@ -594,7 +594,7 @@ GEM
aws-sdk-kinesisvideosignalingchannels (1.13.0)
aws-sdk-core (~> 3, >= 3.120.0)
aws-sigv4 (~> 1.1)
aws-sdk-kms (1.63.0)
aws-sdk-kms (1.64.0)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sigv4 (~> 1.1)
aws-sdk-lakeformation (1.17.0)
@ -1102,7 +1102,7 @@ GEM
aws-sdk-route53resolver (1.30.0)
aws-sdk-core (~> 3, >= 3.120.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.120.1)
aws-sdk-s3 (1.122.0)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
@ -1280,7 +1280,7 @@ GEM
bundler-audit (0.9.1)
bundler (>= 1.2.0, < 3)
thor (~> 1.0)
chartkick (4.1.3)
chartkick (5.0.2)
climate_control (0.2.0)
cocoon (1.2.15)
codeclimate-test-reporter (1.0.9)
@ -1475,7 +1475,7 @@ GEM
net-protocol
netrc (0.11.0)
newrelic_rpm (9.2.0)
nio4r (2.5.8)
nio4r (2.5.9)
nokogiri (1.14.3)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
@ -1499,7 +1499,7 @@ GEM
faraday (~> 1.4)
faraday_middleware (~> 1)
paypalhttp (1.0.1)
pg (1.4.6)
pg (1.5.3)
pkg-config (1.5.1)
pry (0.14.2)
coderay (~> 1.1)
@ -1543,7 +1543,7 @@ GEM
rails (>= 3.2)
rails-ujs (0.1.0)
railties (>= 3.1)
rails_admin (3.1.1)
rails_admin (3.1.2)
activemodel-serializers-xml (>= 1.0)
kaminari (>= 0.14, < 2.0)
nested_form (~> 0.3)
@ -1598,7 +1598,7 @@ GEM
sprockets (> 3.0)
sprockets-rails
tilt
selenium-webdriver (4.8.0)
selenium-webdriver (4.9.1)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
@ -1635,7 +1635,7 @@ GEM
stripe (>= 2.8, < 9)
terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0)
terser (1.1.13)
terser (1.1.15)
execjs (>= 0.3.0, < 3)
text-hyphen (1.4.1)
textstat (0.1.8)
@ -1643,9 +1643,9 @@ GEM
thor (1.2.1)
tilt (2.0.11)
timeago_js (3.0.2.2)
timeout (0.3.1)
timeout (0.3.2)
tribute (3.6.0.0)
turbo-rails (1.3.3)
turbo-rails (1.4.0)
actionpack (>= 6.0.0)
activejob (>= 6.0.0)
railties (>= 6.0.0)
@ -1724,7 +1724,7 @@ DEPENDENCIES
paranoia
paypal-checkout-sdk
paypal_client
pg (~> 1.4)
pg (~> 1.5)
pry
puma (~> 5.6)
puma-heroku

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

View File

@ -1,3 +1,47 @@
// Place all the styles related to the Basil controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: https://sass-lang.com/
$small-screen-up: 601px !default;
$medium-screen-up: 993px !default;
$large-screen-up: 1201px !default;
$small-screen: 600px !default;
$medium-screen: 992px !default;
$large-screen: 1200px !default;
$medium-and-up: "only screen and (min-width : #{$small-screen-up})" !default;
$large-and-up: "only screen and (min-width : #{$medium-screen-up})" !default;
$small-and-down: "only screen and (max-width : #{$small-screen})" !default;
$medium-and-down: "only screen and (max-width : #{$medium-screen})" !default;
$medium-only: "only screen and (min-width : #{$small-screen-up}) and (max-width : #{$medium-screen})" !default;
@mixin column-count($ct) {
-webkit-column-count: $ct;
-moz-column-count: $ct;
column-count: $ct;
}
.cards-container {
column-break-inside: avoid;
.card {
display: inline-block;
overflow: visible;
}
}
@media #{$small-and-down} {
.cards-container {
@include column-count(1);
}
}
@media #{$medium-and-up} {
.cards-container {
@include column-count(2);
}
}
@media #{$large-and-up} {
.cards-container {
@include column-count(3);
}
}
.text-center {
text-align: center;
}

View File

@ -1,5 +1,5 @@
class BasilController < ApplicationController
before_action :authenticate_user!, except: [:complete_commission, :about, :stats]
before_action :authenticate_user!, except: [:complete_commission, :about, :stats, :jam, :queue_jam_job, :commission_info]
before_action :require_admin_access, only: [:review], unless: -> { Rails.env.development? }
@ -210,6 +210,51 @@ class BasilController < ApplicationController
def about
end
def jam
@recent_commissions = BasilCommission.where(entity_id: nil).order('id DESC').limit(24)
@total_count = BasilCommission.where(entity_id: nil).count
# For generating pie charts
@all_commissions = BasilCommission.where(entity_id: nil)
end
def queue_jam_job
created_prompt = [
jam_params[:age],
*jam_params[:features]
].compact.join(', ')
# Create our commission, then redirect back to preview it
BasilCommission.create!(
user: current_user,
entity: nil,
prompt: created_prompt,
job_id: SecureRandom.uuid,
style: ["realistic", "realistic2", "realistic3"].sample,
final_settings: jam_params
)
redirect_back(fallback_location: basil_jam_path, notice: "#{jam_params.fetch(:name, '').presence || 'Your character'} will be visualized shortly. Find them on this page!")
end
def commission_info
@commission = BasilCommission.find_by(job_id: params[:jobid])
raise "No BasilCommission with ID #{params[:jobid]}" if @commission.nil?
render json: {
image_url: @commission.image.url,
user_id: @commission.user_id,
prompt: @commission.prompt,
job_id: @commission.job_id,
created_at: @commission.created_at,
updated_at: @commission.updated_at,
completed_at: @commission.completed_at,
style: @commission.style,
final_settings: @commission.final_settings,
cached_seconds_taken: @commission.cached_seconds_taken,
}
end
def stats
@commissions = BasilCommission.all.with_deleted
@ -509,10 +554,11 @@ class BasilController < ApplicationController
def complete_commission
commission = BasilCommission.find_by(job_id: params[:jobid])
return if commission.nil?
commission.update(completed_at: DateTime.current,
final_settings: JSON.parse(params[:settings]))
raise "Tried to complete commission with invalid job ID #{params[:jobid]}" if commission.nil?
merged_settings = commission.final_settings || {}
commission.update!(completed_at: DateTime.current,
final_settings: merged_settings.merge(JSON.parse(params.fetch(:settings, "{}"))))
# Attach the image in S3 to our `image` ActiveStorage relation
key = "job-#{params[:jobid]}.png"
@ -554,6 +600,7 @@ class BasilController < ApplicationController
@commissions = BasilCommission.where(id: @reviewed_commission_ids.pluck(:basil_commission_id))
.where.not(completed_at: nil)
.where(user: current_user)
.where.not(entity_type: nil, entity_id: nil)
.order(created_at: :desc)
.limit(50)
.includes(:entity)
@ -565,6 +612,7 @@ class BasilController < ApplicationController
@commissions = BasilCommission.where.not(id: @reviewed_commission_ids)
.where.not(completed_at: nil)
.where(user: current_user)
.where.not(entity_type: nil, entity_id: nil)
.order(created_at: :desc)
.limit(50)
.includes(:entity)
@ -595,4 +643,8 @@ class BasilController < ApplicationController
def commission_params
params.require(:basil_commission).permit(:style, :entity_type, :entity_id, field: {})
end
def jam_params
params.require(:commission).permit(:name, :age, features: [])
end
end

View File

@ -168,6 +168,12 @@ class ContentController < ApplicationController
if user_signed_in? && current_user.can_create?(@content.class) \
|| PermissionService.user_has_active_promotion_for_this_content_type(user: current_user, content_type: @content.class.name)
# For users who are creating premium content in a collaborated universe without premium of their own
# we want to default that content into one of their collaborated unvierses.
if !current_user.on_premium_plan? && Rails.application.config.content_types[:premium].map(&:name).include?(@content.class.name)
@content.universe_id = current_user.contributable_universes.first.try(:id)
end
if params.key?(:document_entity)
entity = DocumentEntity.find_by(id: params.fetch(:document_entity).to_i)
if entity.document_owner == current_user

View File

@ -1,7 +1,7 @@
class CollaborationMailer < ApplicationMailer
default from: "collaboration@notebook.ai"
def contributor_invitation(inviter:, invite_email:, universe:)
def contributor_invitation(inviter, invite_email, universe)
@inviter = inviter
@universe = universe

View File

@ -1,8 +1,8 @@
class BasilCommission < ApplicationRecord
acts_as_paranoid
belongs_to :user
belongs_to :entity, polymorphic: true
belongs_to :user, optional: true
belongs_to :entity, polymorphic: true, optional: true
has_one_attached :image,
service: :amazon_basil,
@ -18,8 +18,7 @@ class BasilCommission < ApplicationRecord
# TODO clean this up and put it in a service
sts_client = Aws::STS::Client.new(region: region)
queue_url = 'https://sqs.' + region + '.amazonaws.com/' +
sts_client.get_caller_identity.account + '/' + queue_name
queue_url = 'https://sqs.' + region + '.amazonaws.com/' + sts_client.get_caller_identity.account + '/' + queue_name
sqs_client = Aws::SQS::Client.new(region: region)
message_body = {

View File

@ -1,5 +1,5 @@
class ContributorService < Service
def self.invite_contributor_to_universe universe:, email:
def self.invite_contributor_to_universe(universe:, email:)
# First, look up whether a user already exists for this invite
related_user = User.find_by(email: email.downcase)
@ -31,19 +31,19 @@ class ContributorService < Service
end
end
def self.send_invite_email_to inviter:, email:, universe:
def self.send_invite_email_to(inviter:, email:, universe:)
CollaborationMailer.contributor_invitation(
inviter: inviter,
invite_email: email,
universe: universe
inviter,
email,
universe
).deliver_now! if Rails.env.production?
end
def self.send_contributor_notice_to inviter:, email:, universe:
def self.send_contributor_notice_to(inviter:, email:, universe:)
CollaborationMailer.contributor_invitation(
inviter: inviter,
invite_email: email,
universe: universe
inviter,
email,
universe
).deliver_now! if Rails.env.production?
end
end

View File

@ -61,8 +61,8 @@ class ExportService < Service
if value_for_this_field.present?
json_list = JSON.parse(value_for_this_field.value)
formatted_names = json_list.map do |link_code|
query_link_code_with_cache(*link_code.split('-')).name
end
query_link_code_with_cache(*link_code.split('-')).try(:name)
end.compact
export_text << " #{field.label}: #{formatted_names.to_sentence}\n"
else
@ -310,8 +310,8 @@ class ExportService < Service
# TODO: we should probably whitelist content_type from valid page types here
# If there's no cache, we unfortunately need to do a query to resolve the link code
content = class_from_name(content_type_name).find(content_id)
@content_cache[cache_key] = content
content = class_from_name(content_type_name).find_by(id: content_id)
@content_cache[cache_key] = content if content.present?
return content
end

View File

@ -0,0 +1,478 @@
<!--
Partial included for all the character fields/etc for the next character vizjam
-->
<!--
<div class="row">
<div class="col s12 m12">
<h1 class="text-center" style="font-size: 2rem">
<i class="material-icons <%= Character.text_color %>"><%= Character.icon %></i>
Welcome to our Character VizJam!
</h1>
<div class="center">
<%= image_tag 'basil/character-jam.png', style: 'width: 600px;' %>
<br />
Come back to this page on June 8th to start visualizing your characters!
</div>
</div>
</div>
-->
<%= content_for :full_width_page_header do %>
<div class="row">
<div class="col s12 m12 l6">
<h1 style="font-size: 2rem; padding: 0 1em">
<i class="material-icons <%= Character.text_color %>"><%= Character.icon %></i>
Welcome to our Character VizJam!
</h1>
<ul class="collapsible" style="margin: 0 2em">
<li class="active">
<div class="collapsible-header">
<i class="material-icons pink-text">palette</i>
<strong>Visualize your character</strong>
</div>
<div class="collapsible-body">
<%= form_for basil_jam_submit_path do |f| %>
<div class="input-field">
<input placeholder="Nameless character" id="name" name="commission[name]" type="text">
<label for="name">Name your character, then select their traits from the options below.</label>
</div>
<!-- Age radio -->
<div style="margin-bottom: 1em">
<div x-data="{ selectedTag: '' }">
<strong style="margin-right: 1em">Age</strong>
<% options = ['Infant', 'Child', 'Teenager', 'Young Adult', 'Adult', 'Old', 'Very Old'] %>
<% options.each do |option| %>
<label>
<input type="radio" name="commission[age]" value="<%= option %>" />
<span class="chip">
<%= option %>
</span>
</label>
<% end %>
</div>
</div>
<!-- Gender radio -->
<div style="margin-bottom: 1em">
<strong style="margin-right: 1em">Gender</strong>
<% options = ['Male', 'Female', 'Ambiguous', 'Transgender', 'Non-binary', 'Agender', 'Androgenous', 'Genderqueer'] %>
<% options.each do |option| %>
<label>
<input type="checkbox" name="commission[features][]" value="<%= option %>" />
<span class="chip">
<%= option %>
</span>
</label>
<% end %>
</div>
<!-- Build checkboxes -->
<div style="margin-bottom: 1em">
<strong style="margin-right: 1em">Body type</strong>
<% options = ['Frail', 'Lean', 'Thin', 'Athletic', 'Hourglass', 'Rectangular', 'Muscular', 'Big-boned', 'Petite', 'Round', 'Pear-shaped', 'Curvy', 'Overweight', 'Underweight'] %>
<% options.each do |option| %>
<label>
<input type="checkbox" name="commission[features][]" value="<%= option %> body" />
<span class="chip">
<%= option %>
</span>
</label>
<% end %>
</div>
<!-- Hair color checkboxes -->
<div style="margin-bottom: 1em">
<strong style="margin-right: 1em">Hair color</strong>
<% options = ['Blonde', 'Black', 'Brown', 'Red', 'White', 'Grey', 'Greying', 'Bald', 'Bleached', 'Blue', 'Green', 'Purple', 'Pink', 'Orange', 'Auburn', 'Rainbow'] %>
<% options.each do |option| %>
<label>
<input type="checkbox" name="commission[features][]" value="<%= option %> hair color" />
<span class="chip">
<%= option %>
</span>
</label>
<% end %>
</div>
<!-- Hair style checkboxes -->
<div style="margin-bottom: 1em">
<strong style="margin-right: 1em">Hair style</strong>
<% options = ['Long', 'Medium', 'Short', 'Wavy', 'Straight', 'Curly', 'Afro', 'Bald', 'Balding', 'Bangs', 'Bob cut', 'Bowl cut', 'Bouffant', 'Braided', 'Bun', 'Buzzcut', 'Chignon', 'Combover', 'Cornrows', 'Crewcut', 'Dreadlocks', 'Emo', 'Feathered', 'Flattop', 'Fringe', 'Mop-top', 'Parted', 'Pigtails', 'Pixie', 'Pompadour', 'Ponytail', 'Rat-tail', 'Rocker', 'Slicked back', 'Spiked'] %>
<% options.each do |option| %>
<label>
<input type="checkbox" name="commission[features][]" value="<%= option %> hair style" />
<span class="chip">
<%= option %>
</span>
</label>
<% end %>
</div>
<!-- Eye color checkboxes -->
<div style="margin-bottom: 1em">
<strong style="margin-right: 1em">Eye color</strong>
<% options = ['Amber', 'Blue', 'Brown', 'Topaz', 'Grey', 'Green', 'Hazel', 'Amethyst', 'Indigo', 'Violet', 'Red', 'Black', 'White'] %>
<% options.each do |option| %>
<label>
<input type="checkbox" name="commission[features][]" value="<%= option %> eye color" />
<span class="chip">
<%= option %>
</span>
</label>
<% end %>
</div>
<!-- Skin tone checkboxes -->
<div style="margin-bottom: 1em">
<strong style="margin-right: 1em">Skin tone</strong>
<% options = ['Light', 'Medium', 'Dark', 'Pale', 'Fair', 'Tan', 'White', 'Brown', 'Black', 'Olive', 'Albino', 'Chocolate', 'Grey', 'Green', 'Blue', 'Red', 'Pink', 'Orange', 'Silver', 'Gold', 'Yellow', 'Purple', 'Freckled', 'Speckled'] %>
<% options.each do |option| %>
<label>
<input type="checkbox" name="commission[features][]" value="<%= option %> skin tone" />
<span class="chip">
<%= option %>
</span>
</label>
<% end %>
</div>
<!-- Facial hair checkboxes -->
<div style="margin-bottom: 1em">
<strong style="margin-right: 1em">Facial hair</strong>
<% options = ['Stubble', 'Patchy', 'Beard', 'Chin curtain', 'Chinstrap', 'Fu Manchu', 'Goatee', 'Mustache', 'Handlebar mustache', 'Horseshoe mustache', 'Mutton chops', 'Neckbeard', 'Sideburns', 'Soul patch'] %>
<% options.each do |option| %>
<label>
<input type="checkbox" name="commission[features][]" value="<%= option %> facial hair" />
<span class="chip">
<%= option %>
</span>
</label>
<% end %>
</div>
<!-- Race checkboxes -->
<div style="margin-bottom: 1em">
<strong style="margin-right: 1em">Alternate Race</strong>
<% options = AutocompleteService.for_field_label(content_model: Character, label: 'Race') - ['Human', 'Dark Elf', 'Construct', 'Half-Elf', 'Half-Dwarf', 'Half-Orc', 'Spirit'] %>
<% options.each do |option| %>
<label>
<input type="checkbox" name="commission[features][]" value="<%= option %> race" />
<span class="chip">
<%= option %>
</span>
</label>
<% end %>
</div>
<div class="center">
<br />
<%= f.submit 'Visualize this character', class: 'btn white-text pink' %>
</div>
<% end %>
</div>
</li>
<li>
<div class="collapsible-header">
<i class="material-icons">help</i>
How do I save my images?
</div>
<div class="collapsible-body">
<p>
To save any image, simply right click on it (or long-press if you're on mobile) and click "Save as..." to save
it to your computer.
</p>
<p>
Feel free to upload your images to their character pages on Notebook.ai if you want to show them off in a gallery
alongside any other information you have about your character!
<% unless user_signed_in? %>
<%= link_to 'You can sign up for a free account here.', new_registration_path(User) %>
<% end %>
</p>
</div>
</li>
<li>
<div class="collapsible-header">
<i class="material-icons">help</i>
Who can see the images I generate?
</div>
<div class="collapsible-body">
<p>
All visualizer images are typically private by default when generated from Notebook.ai, but any images generated from this page
for the VizJam will be public by default (and visible from this page!). The jam is meant to introduce our creatives to
the new kinds of tools out there available for visualizing your ideas, and making everything public is a great way to
learn what's possible from each other. If you want to make private images of your characters, you can always use
<%= link_to "Notebook.ai's standard visualization feature", basil_path %>.
</p>
<p>
Only the most recent 20 generated images are shown on this page, so make sure you save any images you want to keep! After they fall
off the list, you won't see them again!
</p>
</div>
</li>
<li>
<div class="collapsible-header">
<i class="material-icons">help</i>
What if I want an option that isn't available?
</div>
<div class="collapsible-body">
<p>
Come <%= link_to 'join us on Discord', 'https://discord.gg/bDE8g5YRzp' %>
and request it! I'll be adding more character options throughout the day based on your feedback. :)
</p>
</div>
</li>
<li>
<div class="collapsible-header">
<i class="material-icons">help</i>
How is this different from the normal visualization features in Notebook.ai?
</div>
<div class="collapsible-body">
<p>
Here are the big differences:
<table>
<th>
<td><strong>VizJam</strong></td>
<td><strong>Notebook.ai's Visualizer</strong></td>
</th>
<tr>
<td><strong>Price</strong></td>
<td>Free to use</td>
<td>Available with Premium ($7-9/mo)</td>
</tr>
<tr>
<td><strong>Privacy</strong></td>
<td>Public</td>
<td>Private</td>
</tr>
<tr>
<td><strong>Available Styles</strong></td>
<td>Realistic</td>
<td>Realistic & 11 other styles</td>
</tr>
<tr>
<td><strong>Content</strong></td>
<td>Characters only</td>
<td><%= BasilService::ENABLED_PAGE_TYPES.map(&:pluralize).to_sentence %></td>
</tr>
<tr>
<td><strong>Control</strong></td>
<td>Simple checkbox options</td>
<td>Unlimited, freeform text</td>
</tr>
</table>
</p>
</div>
</li>
<li>
<div class="collapsible-header">
<i class="material-icons">help</i>
How long will the VizJam last?
</div>
<div class="collapsible-body">
<p>
This VizJam runs from <strong>June 8rd, 2023</strong> to <strong>June 13th, 2023</strong>. You can follow
<%= link_to '@IndentLabs on Twitter', 'https://www.twitter.com/IndentLabs', target: '_blank' %>
or
<%= link_to '@IndentLabs on Medium', 'https://medium.com/indent-labs', target: '_blank' %>
to know when the next VizJam will be!
</p>
</div>
</li>
</ul>
</div>
<div class="col s12 m12 l6">
<h2 style="font-size: 1.4rem">
Recent visualizations <small>(click one to see their traits, refresh for more)</small>
<span class="right badge red white-text tooltipped" data-tooltip="<%= @total_count %> characters visualized!">
<%= number_with_delimiter @total_count %>
</span>
</h2>
<div class="row">
<div class="col s12 cards-container">
<% @recent_commissions.each do |commission| %>
<div class="hoverable card" id='card-<%= commission.job_id %>' data-complete="<%= commission.complete? %>">
<div class="card-image">
<%= link_to "#details-#{commission.job_id}", class: 'modal-trigger waves-effect waves-light' do %>
<% if commission.complete? %>
<%= image_tag commission.image, class: 'commission-image' %>
<% else %>
<%= image_tag image_path("placeholders/loading.gif"), class: 'commission-image', style: 'background: #2196F3' %>
<% end %>
<span class="card-title" style="background: black; opacity: 0.75; padding: 4px">
<%= commission.final_settings&.fetch('name', '').presence || 'No name' %>
</span>
<% end %>
</div>
</div>
<% if commission.completed_at.nil? %>
<script>
document.addEventListener('DOMContentLoaded', (event) => {
let jobId = '<%= commission.job_id %>';
let card = document.getElementById(`card-${jobId}`);
let modal = document.getElementById(`details-${jobId}`);
let complete = card.getAttribute('data-complete') === 'true';
if (!complete) {
console.log('job id ' + jobId + ' is not complete, queueing polling');
let interval = setInterval(() => {
console.log('polling for', jobId);
fetch('<%= basil_commission_info_path(commission.job_id) %>')
.then(response => {
if(!response.ok) {
throw new Error("HTTP error " + response.status);
}
return response.json();
})
.then(data => {
if (data.completed_at) {
console.log('job id ' + jobId + ' is complete, updating image');
complete = true;
card.setAttribute('data-complete', 'true');
cardImage = card.querySelector('.commission-image');
cardImage.src = data.image_url;
modalImage = modal.querySelector('.commission-image');
modalImage.src = data.image_url;
clearInterval(interval);
} else {
console.log('job id ' + jobId + ' is not complete, continuing polling');
}
})
.catch(error => {
console.log("Fetch error: " + error);
});
}, 5000);
}
});
</script>
<% end %>
<div id="details-<%= commission.job_id %>" class="modal modal-fixed-footer">
<div class="modal-content">
<h4>
<i class="material-icons <%= Character.text_color %>"><%= Character.icon %></i>
<%= commission.final_settings&.fetch('name', '').presence || 'Nameless character' %>
</h4>
<div class="row">
<div class="col s12 m6">
<% if commission.complete? %>
<%= link_to commission.image, target: '_blank' do %>
<%= image_tag commission.image, class: 'commission-image', style: 'width: 100%' %>
<% end %>
<div class="text-center" style="font-size: 0.8em">
Click the image to see it full-size and/or download it.
</div>
<% else %>
<%= image_tag image_path("placeholders/loading.gif"), class: 'commission-image', style: 'width: 100%' %>
<div class="text-center" style="font-size: 0.8em">
This image is still generating...
</div>
<% end %>
</div>
<div class="col s12 m6">
<ul style="margin: 0 8px">
<li>
<strong class="grey-text">Traits:</strong>
<div>
<% commission.prompt.split(',').map do |tag| %>
<span class="red white-text" style="padding: 1.5px 10px; white-space: nowrap;"><%= tag.strip %></span>
<% end %>
</div>
<%# link_to 'Select these traits in my form', '#' %>
</li>
<li style="padding-top: 1em">
<strong class="grey-text">Generated AI Prompt:</strong>
<div class="card-panel blue lighten-5">
The raw AI parameters that were used are listed below. You can use these in other image generation tools.
If they are blank, you may need to refresh the page to see them.
</div>
<div style="font-size: 0.8em">
Sampler: <strong><%= commission.final_settings.fetch('sampler', '') %></strong>
</div>
<div style="font-size: 0.8em">
Steps: <strong><%= commission.final_settings.fetch('steps', '') %></strong>
</div>
<div style="font-size: 0.8em">
CFG scale: <strong><%= commission.final_settings.fetch('cfg_scale', '') %></strong>
</div>
<div style="font-size: 0.8em">
Face restoration: <strong><%= commission.final_settings.fetch('face_restoration_model', '') %></strong>
</div>
<div>
<div style="font-size: 0.8em">Positive prompt:</span>
<blockquote style="margin: 5px 0">
<%= commission.final_settings.fetch('prompt', '').gsub('ANAD2', 'person') %>
</blockquote>
</div>
<div>
<div style="font-size: 0.8em">Negative prompt:</span>
<blockquote style="margin: 5px 0">
<%= commission.final_settings.fetch('negative_prompt', '') %>
</blockquote>
</div>
<div style="font-size: 0.8em">
Notebook.ai style: <strong><code><%= commission.style %></code></strong>
</div>
</li>
<li>
<small class="grey-text">Generation ID: <%= commission.job_id %></small>
</li>
</ul>
</div>
</div>
</div>
<div class="modal-footer">
<a href="#!" class="modal-close waves-effect waves-green btn-flat">Close</a>
</div>
</div>
<% end %>
</div>
</div>
</div>
</div>
<% end %>
<script>
function pollingData() {
return {
image: 'images/sample-1.jpg',
jobId: '123', // Job id should be dynamic
pollingInterval: null,
init() {
this.pollingInterval = setInterval(this.poll.bind(this), 5000); // Poll every 5 seconds
},
poll() {
fetch(`/poll/${this.jobId}`)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
if (data.image) {
this.image = data.image;
clearInterval(this.pollingInterval); // Stop polling if image is returned
}
})
.catch(error => {
console.error('Error:', error);
});
},
}
}
</script>

View File

@ -1,224 +0,0 @@
<script type="text/javascript">
function commission_basil(style) {
// Set style hidden value to our selected style
$('#basil_commission_style').val(style);
// Submit form to start the commission
$('#new_basil_commission').submit();
}
</script>
<div class="row">
<%= form_for BasilCommission.new, url: basil_character_path(@character) do |f| %>
<%= f.hidden_field :style, value: 'realistic' %>
<%= f.hidden_field :entity_type, value: 'Character' %>
<%= f.hidden_field :entity_id, value: @character.id %>
<div class="col s12 m4">
<div style="margin: 1em 0">
<%= link_to basil_path, class: 'grey-text text-darken-2' do %>
<i class="material-icons left">chevron_left</i>
Back to my character list
<% end %>
</div>
<div class="clearfix">
<%= image_tag @character.random_image_including_private(format: :medium), style: 'width: 100%' %>
<h1 style="font-size: 2rem; margin-bottom: 0"><%= link_to @character.name, @character %></h1>
<%= link_to 'Edit this character page', edit_polymorphic_path(@character), class: 'grey-text text-darken-2' %>
</div>
<ul>
<% if @gender_field && @gender_value %>
<li style="margin-bottom: 1em">
<div class="grey-text text-darken-3" style="font-weight: bold; font-size: 0.8em">
Gender
<%= range_field_tag "field[#{@gender_field.id}]", @guidance.fetch(@gender_field.id.to_s, 1), { min: 0, max: 1.3, step: 0.1, style: 'width: 50%', class: 'js-importance-slider hide' } %>
</div>
<div>
<%= @gender_value %>
</div>
</li>
<% end %>
<% if @age_field && @gender_value %>
<li style="margin-bottom: 1em">
<div class="grey-text text-darken-3" style="font-weight: bold; font-size: 0.8em">
Age
<%= range_field_tag "field[#{@age_field.id}]", @guidance.fetch(@age_field.id.to_s, 1), { min: 0, max: 1.3, step: 0.1, style: 'width: 50%', class: 'js-importance-slider hide' } %>
</div>
<div>
<%= @age_value %>
</div>
</li>
<% end %>
<% shown_any_value = false %>
<% @appearance_fields.each do |field| %>
<%
value = @attributes.detect { |attr| attr.attribute_field_id == field.id }.try(:value)
next if value.nil? || value.blank?
shown_any_value = true
%>
<li style="margin-bottom: 1em;">
<div class="grey-text text-darken-3" style="font-weight: bold; font-size: 0.8em">
<%= field.label %>
<%= range_field_tag "field[#{field.id}]", @guidance.fetch(field.id.to_s, 1), { min: 0, max: 1.3, step: 0.1, style: 'width: 50%', class: 'js-importance-slider hide' } %>
</div>
<div>
<%= value %>
</div>
</li>
<% end %>
</ul>
<% if !shown_any_value %>
<div class="red card-panel lighten-3">
Basil works best with guidance! Please fill out some fields in the "Looks" category for this character
before requesting an image.
</div>
<% end %>
<div>
<% if @can_request_another %>
<%= link_to 'Customize per-field importance', "javascript:var sliders = document.getElementsByClassName('js-importance-slider'); for(var i = 0; i < sliders.length; i++) sliders.item(i).classList.remove('hide')" %>
<% end %>
<div class="card-panel js-importance-slider hide" style="margin-right: 1em">
<strong>How to customize per-field importance</strong>
<br /><br />
This allows you to tell Basil which fields are more or less important to you. For example, if Basil isn't
getting a character's hair color right, you can increase the importance of the "Hair Color" field.
<br /><br />
You can also tell Basil to ignore a field entirely by dragging the slider all the way to the left.
</div>
</div>
<div class="card-panel" style="margin-top: 2em">
<p>
This is still a work in progress and very much a beta that will change a lot before releasing publicly,
but feel free to use it as much as you'd like to provide feedback!
</p>
<p>
If you run into any issues, please let me know <%# in the %>
<%# link_to 'Site Support forums', 'https://www.notebook.ai/forum/site-support' %>
<%# or %> <%= link_to 'on Discord', 'https://discord.gg/7WCuGxY3AW' %>.
</p>
</div>
</div>
<div class="col s12 m2" style="margin-top: 1rem">
<% if @can_request_another && shown_any_value %>
<div class="grey-text center"><strong>Available styles</strong></div>
<% %w(realistic painting sketch digital anime abstract).each do |style| %>
<%= link_to "javascript:commission_basil('#{style}')" do %>
<div class="hoverable card-panel purple white-text">
<%= style.humanize %>
<i class="material-icons right">chevron_right</i>
</div>
<% end %>
<% end %>
<% end %>
<% if @can_request_another && shown_any_value %>
<div class="grey-text center"><strong>Experimental styles</strong></div>
<% %w(painting2 horror watercolor).each do |style| %>
<%= link_to "javascript:commission_basil('#{style}')" do %>
<div class="hoverable card-panel purple white-text">
<%= style.humanize %>
<i class="material-icons right">chevron_right</i>
</div>
<% end %>
<% end %>
<% end %>
<% if !@can_request_another %>
<div class="card-panel purple white-text">
Basil is working on your <%= pluralize @in_progress_commissions.count, 'requested commission' %>.
<br /><br />
As soon as he completes one, you'll be able to request another.
</div>
<% end %>
</div>
<% end %>
<div class="col s12 m6" style="margin-top: 1rem">
<% @commissions.each do |commission| %>
<div>
<% if commission.complete? %>
<%# image_tag commission.image, style: 'width: 100%' %>
<%
s3 = Aws::S3::Resource.new(region: "us-east-1")
obj = s3.bucket(commission.s3_bucket).object("job-#{commission.job_id}.png")
%>
<div class="card horizontal">
<div class="card-image">
<%= link_to obj.presigned_url(:get) do %>
<%= image_tag obj.presigned_url(:get) %>
<% end %>
</div>
<div class="card-stacked">
<div class="card-content">
<div>
<strong><%= @character.name %></strong>
<% if commission.style? %>
<em>(<%= commission.style.humanize %>)</em>
<% end %>
</div>
<div class="grey-text text-darken-2">
<span style="font-size: 0.8em">Completed <%= time_ago_in_words commission.completed_at %> ago</span>
&middot;<br />
<span style="font-size: 0.8em">Took <%= distance_of_time_in_words commission.completed_at - commission.created_at %></span>
</div>
<div style="margin-top: 1em">
<div class="center" style="font-size: 0.9em"><strong>Feedback for Basil</strong></div>
<div class="row">
<%= form_for commission.basil_feedbacks.find_or_initialize_by(user: current_user), url: basil_feedback_path(commission.job_id), method: :POST, remote: true do |f| %>
<div class="col s3 red lighten-3">
<label>
<%= f.radio_button :score_adjustment, '-2', { class: 'autosave-closest-form-on-change' } %>
<span class="black-text">:'(</span>
</label>
</div>
<div class="col s3 orange lighten-3">
<label>
<%= f.radio_button :score_adjustment, '-1', { class: 'autosave-closest-form-on-change' } %>
<span class="black-text">:(</span>
</label>
</div>
<div class="col s3 green lighten-3">
<label>
<%= f.radio_button :score_adjustment, '1', { class: 'autosave-closest-form-on-change' } %>
<span class="black-text">:)</span>
</label>
</div>
<div class="col s3 blue lighten-3">
<label>
<%= f.radio_button :score_adjustment, '2', { class: 'autosave-closest-form-on-change' } %>
<span class="black-text">:D</span>
</label>
</div>
<% end %>
</div>
</div>
</div>
<!--
<div class="card-action">
<%= link_to "Save to #{@character.name}", '#', class: 'purple-text' %>
<%= link_to "Delete", '#', class: 'red-text right' %>
</div>
-->
</div>
</div>
<% else %>
<div class="card-panel green white-text darken-4">
Basil is still working on this commission... (style: <%= commission.style %>)
<div style="font-size: 0.8em">
(Requested <%= time_ago_in_words(commission.created_at) %> ago)
</div>
</div>
<% end %>
</div>
<% end %>
</div>
</div>

View File

@ -0,0 +1,444 @@
<div class="row">
<div class="col s12 m12 l6">
<h1 class="text-center" style="font-size: 2rem">
<i class="material-icons <%= Character.text_color %>"><%= Character.icon %></i>
The Character VizJam has concluded!
</h1>
<div class="center" style="padding: 10px">
<%= link_to asset_path('basil/character-jam.png') do %>
<%= image_tag 'basil/character-jam.png', style: 'width: 100%' %>
<% end %>
<br />
Thank you for participating!
</div>
</div>
<div class="col s12 m12 l6">
<h2 style="font-size: 1.6rem; margin-top: 3rem">Together, we visualized 1,611 characters!</h2>
<p>
In the spirit of AI transparency, I've compiled some aggregate visualizations of the kinds of characters
that were most &mdash; and least &mdash; visualized during this VizJam.
</p>
<ul class="collapsible">
<li>
<div class="collapsible-header"><i class="material-icons">bar_chart</i> Age</div>
<div class="collapsible-body white">
<%= column_chart(
{ "Infant" => 36, "Child" => 33, "Teenager" => 275, "Young Adult" => 467, "Adult" => 751, "Old" => 146, "Very Old" => 24 },
colors: ['#2196F3'],
max: 760,
label: 'Visualizations'
)
%>
</div>
</li>
<li>
<div class="collapsible-header"><i class="material-icons">pie_chart</i> Gender</div>
<div class="collapsible-body white">
<%= pie_chart(
{ "Male" => 508, "Female" => 628, "Ambiguous" => 92, "Transgender" => 63, "Non-binary" => 81, "Agender" => 33, "Androgenous" => 146, "Genderqueer" => 28 },
legend: 'right',
suffix: ' visualizations'
)
%>
</div>
</li>
<li>
<div class="collapsible-header"><i class="material-icons">bar_chart</i> Skin tone</div>
<div class="collapsible-body white">
<%= bar_chart(
{"Light"=>348,
"Pale"=>314,
"Olive"=>233,
"Black"=>227,
"Brown"=>188,
"White"=>185,
"Fair"=>169,
"Dark"=>165,
"Tan"=>153,
"Medium"=>151,
"Freckled"=>99,
"Blue"=>42,
"Grey"=>29,
"Silver"=>27,
"Chocolate"=>27,
"Gold"=>25,
"Green"=>24,
"Speckled"=>22,
"Albino"=>21,
"Purple"=>19,
"Pink"=>17,
"Red"=>15,
"Yellow"=>11,
"Orange"=>10},
colors: ['#2196F3'],
label: 'Visualizations',
height: '600px',
max: 350
)
%>
</div>
</li>
<li>
<div class="collapsible-header"><i class="material-icons">bar_chart</i> Face</div>
<div class="collapsible-body white">
<%= bar_chart(
{"Brown"=>285,
"Blue"=>218,
"Green"=>161,
"Amber"=>96,
"Grey"=>90,
"Hazel"=>87,
"Topaz"=>66,
"Black"=>61,
"Amethyst"=>50,
"Red"=>43,
"Violet"=>43,
"White"=>28,
"Indigo"=>25},
colors: ['#2196F3'],
title: 'Eye color',
height: '500px',
label: 'Visualizations'
)
%>
<br />
<%= bar_chart({
"Stubble"=>168,
"Beard"=>125,
"Chinstrap"=>114,
"Mustache"=>113,
"Goatee"=>61,
"Patchy"=>40,
"Sideburns"=>28,
"Mutton chops"=>16,
"Soul patch"=>9,
"Neckbeard"=>7,
"Horseshoe mustache"=>5,
"Fu Manchu"=>5,
"Handlebar mustache"=>4,
"Chin curtain"=>3},
title: 'Facial hair (if any)',
colors: ['#2196F3'],
max: 170,
height: '500px',
label: 'Visualizations'
)
%>
</div>
</li>
<li>
<div class="collapsible-header"><i class="material-icons">bar_chart</i> Hair</div>
<div class="collapsible-body white">
<%= bar_chart({
"Black"=>375,
"Brown"=>359,
"Blonde"=>216,
"Red"=>93,
"Blue"=>88,
"White"=>79,
"Auburn"=>68,
"Orange"=>54,
"Pink"=>53,
"Purple"=>48,
"Greying"=>40,
"Bleached"=>39,
"Grey"=>32,
"Rainbow"=>30,
"Green"=>29,
"Bald"=>7},
colors: ['#2196F3'],
title: 'Colors',
height: '500px',
label: 'Visualizations')
%>
<br />
<%=
bar_chart({
"Long"=>617,
"Medium"=>394,
"Wavy"=>288,
"Straight"=>242,
"Short"=>231,
"Curly"=>186,
"Ponytail"=>111,
"Bangs"=>99,
"Braided"=>97,
"Parted"=>88,
"Fringe"=>70,
"Bob cut"=>59,
"Emo"=>48,
"Slicked back"=>46,
"Dreadlocks"=>46,
"Pixie"=>41,
"Bun"=>40,
"Feathered"=>39,
"Afro"=>36,
"Rocker"=>31,
"Spiked"=>29,
"Pigtails"=>22,
"Mop-top"=>20,
"Buzzcut"=>20,
"Bald"=>19,
"Crewcut"=>18,
"Balding"=>13,
"Cornrows"=>11,
"Flattop"=>10,
"Chignon"=>9,
"Bowl cut"=>9,
"Rat-tail"=>8,
"Pompadour"=>7,
"Bouffant"=>7,
"Combover"=>6},
colors: ['#2196F3'],
max: 620,
height: '900px',
title: 'Styles',
label: 'Visualizations'
)
%>
</div>
</li>
<li>
<div class="collapsible-header"><i class="material-icons">bar_chart</i> Body</div>
<div class="collapsible-body white">
<%=
bar_chart({
"Lean"=>395,
"Thin"=>336,
"Athletic"=>329,
"Curvy"=>188,
"Hourglass"=>149,
"Muscular"=>144,
"Rectangular"=>130,
"Petite"=>106,
"Round"=>77,
"Frail"=>74,
"Big-boned"=>73,
"Pear-shaped"=>73,
"Underweight"=>66,
"Overweight"=>60
},
colors: ['#2196F3'],
height: '500px',
label: 'Visualizations')
%>
</div>
</li>
<li>
<div class="collapsible-header"><i class="material-icons">bar_chart</i> Non-human races</div>
<div class="collapsible-body white">
<%= bar_chart(
{"Angel"=>69,
"Fey"=>67,
"Fairy"=>59,
"Vampire"=>54,
"Elf"=>52,
"Animal"=>51,
"Reptilian"=>48,
"Werewolf"=>44,
"Elemental"=>33,
"Robot"=>29,
"Android"=>26,
"Bird"=>25,
"Insectoid"=>18,
"Genie"=>15,
"Orc"=>13,
"Halfling"=>13,
"Dwarf"=>11,
"Gnome"=>10,
"Arachnoid"=>10},
colors: ['#2196F3'],
max: 70,
height: '500px',
label: 'Visualizations'
)
%>
</div>
</li>
</ul>
<p style="font-size: 0.8em">
If you noticed that any particular age, gender, race, or other trait produced
lower-quality images than others, please <%= link_to 'let me know', 'https://discord.gg/bDE8g5YRzp' %>
so I can continue to make our AI models work better for <em>all</em> kinds of characters!
</p>
<br />
<div class="grey lighten-4">
<%= area_chart({
"2023-06-08"=>208,
"2023-06-09"=>515,
"2023-06-10"=>255,
"2023-06-11"=>128,
"2023-06-12"=>167,
"2023-06-13"=>338}, title: 'New visualizations per day', colors: ['#E91E63'], max: 550, height: '200px')
%>
</div>
</div>
</div>
<%
@content_type = Character
singular_class_name = @content_type.name
pluralized_class_name = @content_type.name.pluralize
premium_page = !User.new.can_create?(@content_type)
%>
<div class="row">
<div class="col s12">
<h1 class="text-center" style="font-size: 2rem">
You can still create characters and visualize them on Notebook.ai!
</h1>
</div>
<div class="col s12">
<div class="card horizontal">
<div class="card-image">
<%= image_tag "card-headers/#{pluralized_class_name.downcase}.webp", class: 'materialboxed tooltipped', alt: "The default image used for all #{pluralized_class_name.downcase} on Notebook.ai, but you can replace it with your own uploads.", data: { tooltip: "The default image used for all #{pluralized_class_name.downcase} on Notebook.ai, but you can replace it with your own uploads."} %>
</div>
<div class="card-stacked">
<div class="card-content spaced-paragraphs">
<h1 class="card-title <%= @content_type.text_color %>">Creating <%= pluralized_class_name %> on Notebook.ai</h1>
<p><em>
<%= t("content_descriptions.#{singular_class_name.downcase}") %></em>
</p>
<p>
Creating <%= pluralized_class_name.downcase %> on Notebook.ai is easy.
</p>
<p>
To get started, just click <strong><%= pluralized_class_name %></strong> under the "Worldbuilding" header in the site sidebar.
You'll be able to see or edit all of your existing <%= singular_class_name.downcase %> pages and create new ones at any time.
</p>
</div>
</div>
</div>
</div>
<div class="col s12 m12 l5">
<div class="card">
<div class="card-content">
<h2 class="card-title">
<i class="material-icons <%= @content_type.text_color %> left">
<%= @content_type.icon %>
</i>
Get a head start with a rich
<span class="<%= @content_type.text_color %>"><%= singular_class_name.downcase %></span>
template
</h2>
<br />
<div class="spaced-paragraphs">
<p>
Templates on Notebook.ai are what help our unique worldbuilding system better understand your world.
</p>
<p>
You can fill out as little or as much as you'd like on every new <%= singular_class_name.downcase %>. You'll see
progress indicators every time you edit it to show where you can make progress on, and our system will
intelligently generate questions for you around the site that will automatically save your answers
to the proper place on your <%= singular_class_name.downcase %> page.
</p>
<p>
Templates are also fully customizable across every <%= singular_class_name.downcase %> in your notebook.
</p>
<p>
You can browse the default template for <%= pluralized_class_name.downcase %> here; click any category
to see its questions.
</p>
</div>
</div>
</div>
</div>
<div class="col s12 m12 l7">
<ul class="collapsible popout">
<%
YAML.load_file(Rails.root.join('config', 'attributes', "#{singular_class_name.downcase}.yml")).map do |category_name, category_details|
%>
<% next if category_name == :contributors %>
<li>
<div class="collapsible-header <%= @content_type.color %> darken-3 white-text">
<i class="material-icons"><%= category_details[:icon] %></i>
<%= category_details[:label] %>
</div>
<div class="collapsible-body">
<% if category_name == :gallery %>
<p>
This category lets you upload images to this <%= singular_class_name.downcase %>'s notebook page.
</p>
<br />
<p>
It's great if you have sketches or artwork for your <%= singular_class_name.downcase %>,
but also works well for collecting visual inspiration, too!
</p>
<% end %>
<% category_details.fetch(:attributes, []).each do |field| %>
<div>
<strong><%= field[:label] %></strong>
</div>
<div>
<% if field[:field_type] == 'link' || field[:field_type] == 'universe' %>
This field allows you to link your other Notebook.ai pages to this <%= singular_class_name.downcase %>.
<% elsif field[:field_type] == 'tags' %>
This field lets you add clickable tags to your <%= pluralized_class_name.downcase %>.
<% else %>
<%=
I18n.translate "attributes.#{singular_class_name.downcase}.#{field[:label].downcase.gsub(/\s/, '_')}",
scope: :serendipitous_questions,
name: "this #{singular_class_name.downcase}",
default: 'Write as little or as much as you want!'
%>
<br />
<span class="grey-text"><%= field[:description].try(:html_safe) %></span>
<% end %>
</div>
<br />
<% end %>
</div>
</li>
<%
end
%>
</ul>
</div>
<div class="col s12">
<div class="card horizontal">
<div class="card-image">
<%= image_tag 'basil/portrait.png', style: 'width: 420px' %>
</div>
<div class="card-stacked">
<div class="card-content">
<div class="card-title">
<i class="pink-text material-icons left">palette</i>
Visualize your characters
</div>
<p>
After you've created a character on Notebook.ai, visualizing them is as easy as picking an image style and clicking a button.
Everything you've written about what they look like on their notebook page is automatically included.
</p>
<br />
<p>
Image visualization is a Premium feature, but you can generate up to 100 images for free to try it out for yourself.
</p>
</div>
</div>
</div>
</div>
</div>
<% if user_signed_in? %>
<div class="text-center">
<div>Already logged in? Great!</div>
<%= link_to 'Visualize your ideas', basil_path, class: 'btn btn-large hoverable blue white-text' %>
</div>
<% else %>
<div class="text-center">
<div>Want to keep visualizing your ideas?</div>
<%= link_to 'Get started with Notebook.ai', new_registration_path(User), class: 'btn btn-large hoverable blue white-text' %>
</div>
<% end %>
<% 10.times do %><br /><% end %>

View File

@ -20,7 +20,7 @@
<% @current_queue_items.each do |commission| %>
<li>
<%= commission.entity_type %>-<%= commission.entity_id %> (<%= commission.style %>)
for U-<%= commission.user_id %>
for U-<%= commission.user_id %> (#<%= commission.id %>)
</li>
<% end %>
</ol>
@ -29,15 +29,10 @@
<% @recent_commissions.each do |commission| %>
<div>
<% if commission.complete? %>
<%# image_tag commission.image, style: 'width: 100%' %>
<%
s3 = Aws::S3::Resource.new(region: "us-east-1")
obj = s3.bucket(commission.s3_bucket).object("job-#{commission.job_id}.png")
%>
<div class="card horizontal">
<div class="card-image">
<%= link_to obj.presigned_url(:get) do %>
<%= image_tag obj.presigned_url(:get) %>
<%= link_to commission.image do %>
<%= image_tag commission.image %>
<% end %>
</div>
<div class="card-stacked">
@ -45,12 +40,17 @@
<div>
<%= commission.id %>.
<% if commission.entity.present? %>
<i class="material-icons right <%= content_class_from_name(commission.entity_type).text_color %>"><%= content_class_from_name(commission.entity_type).icon %></i>
<strong><%= link_to commission.entity.name, commission.entity %></strong>
<% end %>
<% if commission.style? %>
<em>(<%= commission.style.humanize %>)</em>
<% end %>
by <%= link_to commission.user.name, commission.user %>
<% if commission.user_id %>
by <%= link_to commission.user.name, commission.user %>
<% else %>
anonymous generation
<% end %>
<div style="font-size: 0.5em"><%= commission.job_id %></div>
</div>
<ul>

View File

@ -42,7 +42,7 @@
</div>
<div class="col s12 m8 l9 grey lighten-3">
<%=
area_chart @commissions.group_by_day(:created_at).map { |date, count| [date.to_date, count] },
area_chart @commissions.where('created_at > ?', 30.days.ago.beginning_of_month).group_by_day(:created_at).map { |date, count| [date.to_date, count] },
colors: ['#9C27B0', '#2196F3'],
title: 'Images created per day',
suffix: ' images'

View File

@ -345,6 +345,12 @@
</div>
</div>
<div class="modal-footer">
<% if current_user.can_delete?(document) %>
<%= link_to document_path(document), method: :delete, class: 'red white-text btn hoverable' do %>
<i class="material-icons left">delete</i>
Delete
<% end %>
<% end %>
<% if current_user.can_read?(document) %>
<%= link_to polymorphic_path(document), class: 'blue white-text text-lighten-1 btn hoverable' do %>
<i class="material-icons left"><%= content_type.icon %></i>

View File

@ -9,7 +9,7 @@
material for your ideas.
</p>
<p>
To generate an image of this <%= @content.try(:page_type) || 'page' %>,
To generate an image of this <%= @content.try(:page_type).try(:downcase) || 'page' %>,
simply click a style below. Once the image has generated, you can move it
to your <%= link_to @content.try(:name), @content.view_path %> page by clicking "Save" on any image.
</p>

View File

@ -0,0 +1,18 @@
<div class="col s12 m5 l4">
<div class="grey-text uppercase center">
Happening now
</div>
<%= link_to jam_path do %>
<div class="card-panel hoverable pink white-text" style="margin-bottom: 0">
<div class="valign-wrapper">
<i class="material-icons" class="left" style="font-size: 3em; margin-right: 0.3em;">face_2</i>
<div>
Join our free Character VizJam event until June 13th!
</div>
</div>
</div>
<% end %>
<div style="margin-bottom: 2em;">
<%= link_to 'dismiss message', notice_dismissal_dismiss_path(notice_id: 21), class: 'right' %>
</div>
</div>

View File

@ -208,6 +208,7 @@ en:
- Delicate
- Flat
- Fragile
- Frail
- Lean
- Lightly muscled
- Small-shouldered
@ -221,6 +222,9 @@ en:
- Big-boned
- Round physique
- Pear-shaped
- Curvy
- Overweight
- Underweight
bow_types:
- Longbow
@ -1859,6 +1863,11 @@ en:
- Red
- Jet black
- Raven black
- Orange
- Silver
- Gold
- Yellow
- Purple
sword_types:
- Cutlass

View File

@ -19,6 +19,11 @@ Rails.application.routes.draw do
post '/feedback/:jobid', to: 'basil#feedback', as: :basil_feedback
post '/:id/save', to: 'basil#save', as: :basil_save
delete '/:id/delete', to: 'basil#delete', as: :basil_delete
get '/commission/:jobid', to: 'basil#commission_info', as: :basil_commission_info
# Landing pages
get '/jam', to: 'basil#jam', as: :basil_jam
post '/jam', to: 'basil#queue_jam_job', as: :basil_jam_submit
# Standard generation flow for users
get '/', to: 'basil#index', as: :basil
@ -34,6 +39,9 @@ Rails.application.routes.draw do
end
end
# Temporary landing path for jams (nice URL)
get '/jam', to: 'basil#jam', as: :jam
scope :stream, path: '/stream', as: :stream do
get '/', to: 'stream#index'
get 'world', to: 'stream#global'

View File

@ -0,0 +1,13 @@
class DropNotNullConstraintFromBasilCommissions < ActiveRecord::Migration[6.1]
def up
change_column_null :basil_commissions, :user_id, true
change_column_null :basil_commissions, :entity_type, true
change_column_null :basil_commissions, :entity_id, true
end
def down
change_column_null :basil_commissions, :user_id, false
change_column_null :basil_commissions, :entity_type, false
change_column_null :basil_commissions, :entity_id, false
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2023_04_14_183606) do
ActiveRecord::Schema.define(version: 2023_05_12_222601) do
create_table "active_storage_attachments", force: :cascade do |t|
t.string "name", null: false
@ -187,9 +187,9 @@ ActiveRecord::Schema.define(version: 2023_04_14_183606) do
end
create_table "basil_commissions", force: :cascade do |t|
t.integer "user_id", null: false
t.string "entity_type", null: false
t.integer "entity_id", null: false
t.integer "user_id"
t.string "entity_type"
t.integer "entity_id"
t.string "prompt"
t.string "job_id"
t.datetime "created_at", precision: 6, null: false

View File

@ -4,6 +4,6 @@ services:
build: .
ports:
- "3000:3000/tcp"
command: sh -c "rake db:migrate && rm -f tmp/pids/server.pid && exec bundle exec rails server -b 0.0.0.0"
command: sh -c "rake db:migrate && rm -f tmp/pids/server.pid && exec rails server -b 0.0.0.0"
volumes:
- "./:/notebook-ai"