Models hosting v1

Seto Elkahfi committed Jun 17, 2026 at 10:52 UTC 7734fcb6fed6ce5a0c3a9a4166baa0620190ae4c
25 files changed +1383 -45
.gitignore
+5
index 0fdde5a..da4a3f4 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,8 @@ # Ignore local git repos for development environment .local-repos/* !.local-repos/.keep + +# Ignore the bare git repositories the app hosts on disk (runtime data: +# user repos, seeded models, and their LFS-backed weights). Keep the folder. +/repos/* +!/repos/.keep
app/controllers/models_controller.rb
+67
new file mode 100644 index 0000000..338ca3b --- /dev/null +++ b/app/controllers/models_controller.rb @@ -0,0 +1,67 @@ +# Hugging Face-style discovery page for open-weights model repositories. +class ModelsController < ApplicationController + def index + @query = params[:q].to_s.strip + @pipeline_tag = params[:pipeline_tag].presence + @library = params[:library].presence + @sort = params.fetch(:sort, "trending") + + repos = Repository.models + .visible_to(current_user) + .includes(:user) + .to_a + + # Decorate each repo with its parsed model card once, then filter/sort in + # Ruby — the card metadata lives in git, not the database. + cards = repos.map { |repo| [ repo, repo.model_card ] } + + if @query.present? + q = @query.downcase + cards.select! do |repo, card| + repo.full_name.downcase.include?(q) || + repo.description.to_s.downcase.include?(q) || + card.tags.any? { |t| t.downcase.include?(q) } + end + end + + @facets = build_facets(cards) + + cards.select! { |_repo, card| card.pipeline_tag == @pipeline_tag } if @pipeline_tag + cards.select! { |_repo, card| card.library_name == @library } if @library + + @models = sort_cards(cards) + end + + private + + def build_facets(cards) + { + pipeline_tags: tally(cards) { |_repo, card| card.pipeline_tag }, + libraries: tally(cards) { |_repo, card| card.library_name } + } + end + + def tally(cards) + cards.filter_map { |pair| yield(*pair) } + .tally + .sort_by { |label, count| [ -count, label ] } + end + + def sort_cards(cards) + case @sort + when "downloads" + cards.sort_by { |repo, _| -repo.downloads_count } + when "likes" + cards.sort_by { |repo, _| -repo.stars_count } + when "recent" + cards.sort_by { |repo, _| repo.updated_at }.reverse + else # "trending" — a blend of likes, downloads and recency + cards.sort_by { |repo, _| -trending_score(repo) } + end + end + + def trending_score(repo) + age_days = [ (Time.current - repo.updated_at) / 1.day, 0.5 ].max + ((repo.stars_count * 3) + Math.log10(repo.downloads_count + 1) * 10) / age_days + end +end
app/controllers/repositories_controller.rb
+75 -5
index 3f31efa..973b2c6 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -4,8 +4,27 @@ class RepositoriesController < ApplicationController before_action :load_repository, only: [ :show, :tree, :cicd ] before_action :ensure_can_read!, only: [ :show, :tree, :cicd ] + # Discovery page for public code repositories — the code-side counterpart to + # the model library at /models. + def explore + @query = params[:q].to_s.strip + @sort = params.fetch(:sort, "recent") + + repos = Repository.code.visible_to(current_user).includes(:user) + if @query.present? + like = "%#{@query.downcase}%" + repos = repos.where("LOWER(repositories.name) LIKE :q OR LOWER(repositories.description) LIKE :q", q: like) + end + + @repositories = case @sort + when "stars" then repos.order(stars_count: :desc, updated_at: :desc) + when "name" then repos.order(Arel.sql("LOWER(repositories.name) ASC")) + else repos.order(updated_at: :desc) + end.to_a + end + def new - @repository = Repository.new + @repository = Repository.new(kind: params[:kind] == "model" ? "model" : "code") end def create @@ -18,10 +37,11 @@ class RepositoriesController < ApplicationController GitRepositoryService.create_bare_repo(current_user.username, @repository.name) GitRepositoryService.initialize_with_readme( current_user.username, @repository.name, - default_branch: @repository.default_branch + default_branch: @repository.default_branch, + readme: initial_readme(@repository) ) redirect_to repository_path(current_user.username, @repository.name), - notice: "Repository created successfully." + notice: "#{@repository.model? ? 'Model' : 'Repository'} created successfully." else render :new, status: :unprocessable_entity end @@ -38,7 +58,11 @@ class RepositoriesController < ApplicationController unless @repository.initialized? && GitRepositoryService.branch_exists?(@repository.disk_path, branch) @empty = true - return + render(@repository.model? ? :model : :show) and return + end + + if @repository.model? + return show_model(branch) end @tree = GitRepositoryService.list_tree(@repository.disk_path, branch) @@ -106,8 +130,54 @@ class RepositoriesController < ApplicationController private + # Renders the Hugging Face-style model page: parsed model card + a + # "Files and versions" listing with LFS-resolved sizes, all on one page with + # client-side tab switching. + def show_model(branch) + @card = @repository.model_card(branch: branch) + @card_html = render_markdown(@card.body) if @card.body.present? + @files = GitRepositoryService.model_files(@repository.disk_path, branch) + @total_size = @files.filter_map { |f| f[:size] }.sum + @commit_count = GitRepositoryService.commit_count(@repository.disk_path, branch) + @recent_commits = GitRepositoryService.commits(@repository.disk_path, branch, limit: 1) + render :model + end + def repository_params - params.require(:repository).permit(:name, :description, :default_branch, :is_private) + permitted = params.require(:repository).permit(:name, :description, :default_branch, :is_private, :kind) + permitted[:kind] = "model" if permitted[:kind] == "model" + permitted[:kind] = "code" unless permitted[:kind] == "model" + permitted + end + + # Seeds a fresh repo's README — a starter model card with YAML front-matter + # for models, a plain heading for code repos. + def initial_readme(repository) + return nil unless repository.model? + + <<~MARKDOWN + --- + license: apache-2.0 + pipeline_tag: text-generation + library_name: transformers + tags: + - #{repository.name} + --- + + # #{repository.name} + + Describe your model here. Document its architecture, training data, + intended use, limitations, and how to load the weights. + + Push your weight files (`*.safetensors`, `*.gguf`, `*.bin`) with Git LFS: + + ```bash + git lfs install + git lfs track "*.safetensors" "*.gguf" + git add .gitattributes + git push origin #{repository.default_branch} + ``` + MARKDOWN end def load_owner
app/controllers/stars_controller.rb
+28
new file mode 100644 index 0000000..2976772 --- /dev/null +++ b/app/controllers/stars_controller.rb @@ -0,0 +1,28 @@ +# Toggles a star (a "like" for models, a star for code repos) for the signed-in +# user on a repository. One endpoint flips the state in both directions. +class StarsController < ApplicationController + before_action :require_sign_in! + before_action :load_repository + + def create + star = current_user.stars.find_or_initialize_by(repository: @repository) + if star.persisted? + star.destroy + else + star.save + end + redirect_back fallback_location: repository_path(@repository.user.username, @repository.name) + end + + private + + def load_repository + owner = User.find_by!(username: params[:username]) + @repository = owner.repositories.find_by!(name: params[:repository]) + unless !@repository.is_private || current_user == owner + render file: Rails.public_path.join("404.html"), status: :not_found, layout: false + end + rescue ActiveRecord::RecordNotFound + render file: Rails.public_path.join("404.html"), status: :not_found, layout: false + end +end
app/controllers/users_controller.rb
+6 -1
index d24e0f8..24c2959 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -11,7 +11,12 @@ class UsersController < ApplicationController @profile_user.repositories.order(updated_at: :desc) else @profile_user.repositories.where(is_private: false).order(updated_at: :desc) - end + end.to_a + + # The profile mirrors the site nav: code repositories and models are shown + # on separate tabs, so split the owner's repos by kind. + @code_repositories = @repositories.select(&:code?) + @model_repositories = @repositories.select(&:model?) if @profile_user.username == DEMO_FEED_USERNAME # Curated showcase feed for the demo profile.
app/helpers/models_helper.rb
+53
new file mode 100644 index 0000000..73fd291 --- /dev/null +++ b/app/helpers/models_helper.rb @@ -0,0 +1,53 @@ +module ModelsHelper + # A small curated map of Hugging Face pipeline tags to human labels. Unknown + # tags fall back to a titleized version of the raw value. + PIPELINE_LABELS = { + "text-generation" => "Text Generation", + "text2text-generation" => "Text-to-Text", + "text-classification" => "Text Classification", + "token-classification" => "Token Classification", + "question-answering" => "Question Answering", + "summarization" => "Summarization", + "translation" => "Translation", + "feature-extraction" => "Feature Extraction", + "sentence-similarity" => "Sentence Similarity", + "fill-mask" => "Fill-Mask", + "image-classification" => "Image Classification", + "image-to-text" => "Image-to-Text", + "text-to-image" => "Text-to-Image", + "automatic-speech-recognition" => "Speech Recognition", + "text-to-speech" => "Text-to-Speech", + "audio-classification" => "Audio Classification" + }.freeze + + def pipeline_label(tag) + return nil if tag.blank? + PIPELINE_LABELS[tag] || tag.tr("-_", " ").split.map(&:capitalize).join(" ") + end + + # Compact human download count, e.g. 1240 → "1.2k", 3_400_000 → "3.4M". + def humanize_count(n) + n = n.to_i + return n.to_s if n < 1000 + number_to_human(n, units: { thousand: "k", million: "M", billion: "B" }, + format: "%n%u", precision: 2, strip_insignificant_zeros: true) + end + + # Maps a weight-file extension to a short format badge label. + WEIGHT_FORMATS = { + "safetensors" => "Safetensors", + "gguf" => "GGUF", + "bin" => "PyTorch", + "pt" => "PyTorch", + "pth" => "PyTorch", + "onnx" => "ONNX", + "ckpt" => "Checkpoint", + "h5" => "Keras", + "msgpack" => "Flax", + "tflite" => "TFLite" + }.freeze + + def weight_format(filename) + WEIGHT_FORMATS[File.extname(filename).delete_prefix(".").downcase] + end +end
app/models/repository.rb
+35
index 8fc44b9..00e941e 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1,5 +1,15 @@ class Repository < ApplicationRecord + KINDS = %w[code model].freeze + belongs_to :user + has_many :stars, dependent: :destroy + has_many :stargazers, through: :stars, source: :user + + scope :models, -> { where(kind: "model") } + scope :code, -> { where(kind: "code") } + scope :visible_to, ->(user) { + user ? where("is_private = ? OR user_id = ?", false, user.id) : where(is_private: false) + } validates :name, presence: true, format: { @@ -8,6 +18,7 @@ class Repository < ApplicationRecord }, uniqueness: { scope: :user_id, case_sensitive: false } validates :default_branch, presence: true + validates :kind, inclusion: { in: KINDS } validate :no_consecutive_dots_in_name validate :no_leading_hyphen_in_name @@ -29,6 +40,30 @@ class Repository < ApplicationRecord Dir.exist?(git_path) && File.exist?(File.join(git_path, "HEAD")) end + def model? + kind == "model" + end + + def code? + kind != "model" + end + + def starred_by?(user) + return false unless user + stars.exists?(user_id: user.id) + end + + # Parses the Hugging Face-style YAML front-matter from the README (the "model + # card") on the default branch. Memoized per instance; returns a ModelCard + # even when there is no front-matter so callers can use it unconditionally. + def model_card(branch: nil) + @model_card ||= begin + branch ||= default_branch + readme = (GitRepositoryService.readme_content(disk_path, branch) if initialized?) + ModelCard.parse(readme&.dig(:content)) + end + end + private def no_consecutive_dots_in_name
app/models/star.rb
+6
new file mode 100644 index 0000000..1c14f3f --- /dev/null +++ b/app/models/star.rb @@ -0,0 +1,6 @@ +class Star < ApplicationRecord + belongs_to :user + belongs_to :repository, counter_cache: :stars_count + + validates :user_id, uniqueness: { scope: :repository_id } +end
app/models/user.rb
+2
index 16d6bbc..a8c2fd1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -3,6 +3,8 @@ class User < ApplicationRecord has_many :repositories, dependent: :destroy has_many :ssh_keys, dependent: :destroy + has_many :stars, dependent: :destroy + has_many :starred_repositories, through: :stars, source: :repository validates :smbcloud_id, presence: true,
app/services/git_repository_service.rb
+38 -7
index 41e046b..edb4479 100644 --- a/app/services/git_repository_service.rb +++ b/app/services/git_repository_service.rb @@ -16,14 +16,14 @@ class GitRepositoryService path end - def self.initialize_with_readme(username, reponame, default_branch: "main") + def self.initialize_with_readme(username, reponame, default_branch: "main", readme: nil) bare_path = repo_path(username, reponame) Dir.mktmpdir do |tmpdir| system("git", "-C", tmpdir, "init", "-b", default_branch, exception: true) system("git", "-C", tmpdir, "config", "user.email", "sigitsi@localhost", exception: true) system("git", "-C", tmpdir, "config", "user.name", "siGit", exception: true) - readme = File.join(tmpdir, "README.md") - File.write(readme, "# #{reponame}\n\nWelcome to #{reponame}.\n") + readme_path = File.join(tmpdir, "README.md") + File.write(readme_path, readme || "# #{reponame}\n\nWelcome to #{reponame}.\n") system("git", "-C", tmpdir, "add", ".", exception: true) system("git", "-C", tmpdir, "commit", "-m", "Initial commit", exception: true) system("git", "-C", tmpdir, "remote", "add", "origin", bare_path, exception: true) @@ -43,13 +43,44 @@ class GitRepositoryService out, _err, status = Open3.capture3(*args) return [] unless status.success? out.lines.filter_map do |line| - parts = line.split - next unless parts.length >= 5 - mode, type, _sha, _size, name = parts[0], parts[1], parts[2], parts[3], parts[4..]&.join(" ")&.strip - { mode: mode, type: type, name: name, path: prefix + name } + # `ls-tree --long` columns: <mode> <type> <sha> <size> <tab> <name> + meta, name = line.split("\t", 2) + next if name.nil? + mode, type, _sha, size = meta.split + { mode: mode, type: type, name: name.strip, + path: prefix + name.strip, + size: (size == "-" ? nil : size.to_i) } end.sort_by { |e| [e[:type] == "tree" ? 0 : 1, e[:name].downcase] } end + # Git LFS / Xet pointer files are tiny text stubs that stand in for large + # binary weights stored out-of-band. Recognise them so the UI can show the + # real on-disk size and an "LFS" badge instead of the ~130-byte pointer. + LFS_POINTER_RE = %r{\Aversion https://(?:git-lfs\.github\.com|hf\.co)/spec/v1} + LFS_SIZE_RE = /^size (\d+)$/ + POINTER_MAX = 1024 + + # Root-level file listing for a model repository's "Files and versions" tab, + # with LFS pointer sizes resolved. Folders are included but not recursed. + def self.model_files(path, branch) + list_tree(path, branch).map do |entry| + next entry unless entry[:type] == "blob" + resolve_lfs(path, branch, entry) + end + end + + # If +entry+ is a small blob that turns out to be an LFS pointer, replace its + # size with the pointer's declared size and flag it. Otherwise return as-is. + def self.resolve_lfs(path, branch, entry) + return entry if entry[:size].nil? || entry[:size] > POINTER_MAX + + content = file_content(path, branch, entry[:path]) + return entry unless content&.match?(LFS_POINTER_RE) + + size = content[LFS_SIZE_RE, 1]&.to_i + entry.merge(lfs: true, size: size || entry[:size]) + end + def self.file_content(path, branch, file_path) out, _err, status = Open3.capture3("git", "--git-dir", path, "show", "#{branch}:#{file_path}") return nil unless status.success?
app/services/model_card.rb
+86
new file mode 100644 index 0000000..f47ee73 --- /dev/null +++ b/app/services/model_card.rb @@ -0,0 +1,86 @@ +require "yaml" + +# Parses a Hugging Face-style "model card": a Markdown README whose leading +# section is a YAML front-matter block delimited by `---` lines, e.g. +# +# --- +# license: apache-2.0 +# base_model: Qwen/Qwen2.5-3B-Instruct +# pipeline_tag: text-generation +# library_name: gguf +# tags: +# - text-generation +# - gguf +# --- +# # Qwen2.5-3B-Instruct-GGUF +# ...markdown body... +# +# The metadata drives the model page sidebar and the discovery-page facets; the +# body is the rendered card content with the front-matter stripped out. +class ModelCard + FRONT_MATTER = /\A---\s*\n(.*?\n)---\s*\n?(.*)\z/m + + attr_reader :metadata, :body + + def self.parse(content) + new(content) + end + + def initialize(content) + content = content.to_s + if (m = content.match(FRONT_MATTER)) + @metadata = safe_yaml(m[1]) + @body = m[2].to_s + else + @metadata = {} + @body = content + end + end + + def license + value("license") + end + + def pipeline_tag + value("pipeline_tag") + end + + def library_name + value("library_name") || value("library") + end + + def base_model + Array(metadata["base_model"]).flatten.compact.first.presence + end + + def language + Array(metadata["language"]).flatten.compact.map(&:to_s) + end + + def datasets + Array(metadata["datasets"]).flatten.compact.map(&:to_s) + end + + def tags + Array(metadata["tags"]).flatten.compact.map(&:to_s).uniq + end + + # True when there is any structured metadata worth showing in the sidebar. + def present? + metadata.present? + end + + private + + def value(key) + v = metadata[key] + v.is_a?(Array) ? v.first : v + end + + def safe_yaml(raw) + parsed = YAML.safe_load(raw, permitted_classes: [ Date, Time ], aliases: false) + parsed.is_a?(Hash) ? parsed : {} + rescue Psych::SyntaxError + {} + end +end
app/views/models/index.html.erb
+144
new file mode 100644 index 0000000..6411433 --- /dev/null +++ b/app/views/models/index.html.erb @@ -0,0 +1,144 @@ +<% content_for :title, "Models" %> + +<div class="max-w-6xl mx-auto px-4 sm:px-6 py-8"> + + <div class="flex items-end justify-between gap-4 mb-6 flex-wrap"> + <div> + <h1 class="text-2xl font-semibold text-gray-100 flex items-center gap-2"> + <svg class="w-6 h-6 text-brand-500" viewBox="0 0 24 24" fill="none" + stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"> + <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/> + <path d="m3.27 6.96 8.73 5.05 8.73-5.05M12 22.08V12"/> + </svg> + Models + <span class="badge-gray text-sm align-middle"><%= number_with_delimiter(@models.size) %></span> + </h1> + <p class="text-sm text-gray-400 mt-1">Open-weights models hosted on siGit, ready to download and run.</p> + </div> + <% if signed_in? %> + <%= link_to new_repository_path(kind: "model"), class: "btn-primary text-xs py-2 px-4" do %> + <svg class="w-3.5 h-3.5" viewBox="0 0 16 16" fill="currentColor"><path d="M7.75 2a.75.75 0 0 1 .75.75V7h4.25a.75.75 0 0 1 0 1.5H8.5v4.25a.75.75 0 0 1-1.5 0V8.5H2.75a.75.75 0 0 1 0-1.5H7V2.75A.75.75 0 0 1 7.75 2Z"/></svg> + New model + <% end %> + <% end %> + </div> + + <div class="grid grid-cols-1 lg:grid-cols-4 gap-6"> + + <%# ── Filter sidebar ── %> + <aside class="lg:col-span-1 space-y-6"> + <% if @facets[:pipeline_tags].any? %> + <div> + <h2 class="text-xs uppercase tracking-wider text-gray-500 font-semibold mb-2">Tasks</h2> + <ul class="space-y-0.5"> + <% @facets[:pipeline_tags].each do |tag, count| %> + <li> + <%= link_to models_path(request.query_parameters.merge(pipeline_tag: (@pipeline_tag == tag ? nil : tag)).compact), + class: "flex items-center justify-between px-2 py-1 rounded text-sm #{@pipeline_tag == tag ? 'bg-brand-500/10 text-brand-400' : 'text-gray-400 hover:bg-surface-700'}" do %> + <span class="truncate"><%= pipeline_label(tag) %></span> + <span class="text-xs text-gray-500"><%= count %></span> + <% end %> + </li> + <% end %> + </ul> + </div> + <% end %> + + <% if @facets[:libraries].any? %> + <div> + <h2 class="text-xs uppercase tracking-wider text-gray-500 font-semibold mb-2">Libraries</h2> + <ul class="space-y-0.5"> + <% @facets[:libraries].each do |lib, count| %> + <li> + <%= link_to models_path(request.query_parameters.merge(library: (@library == lib ? nil : lib)).compact), + class: "flex items-center justify-between px-2 py-1 rounded text-sm #{@library == lib ? 'bg-brand-500/10 text-brand-400' : 'text-gray-400 hover:bg-surface-700'}" do %> + <span class="truncate"><%= lib %></span> + <span class="text-xs text-gray-500"><%= count %></span> + <% end %> + </li> + <% end %> + </ul> + </div> + <% end %> + + <% if @pipeline_tag || @library || @query.present? %> + <%= link_to "Clear filters", models_path, class: "text-xs text-brand-500 hover:underline" %> + <% end %> + </aside> + + <%# ── Results ── %> + <main class="lg:col-span-3"> + <div class="flex items-center gap-3 mb-4"> + <%= form_with url: models_path, method: :get, class: "flex-1" do %> + <% if @pipeline_tag %><%= hidden_field_tag :pipeline_tag, @pipeline_tag %><% end %> + <% if @library %><%= hidden_field_tag :library, @library %><% end %> + <%= hidden_field_tag :sort, @sort %> + <div class="relative"> + <svg class="w-4 h-4 text-gray-500 absolute left-3 top-1/2 -translate-y-1/2" viewBox="0 0 16 16" fill="currentColor"><path d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-1.06 1.06ZM11.5 7a4.5 4.5 0 1 0-9 0 4.5 4.5 0 0 0 9 0Z"/></svg> + <%= text_field_tag :q, @query, placeholder: "Search models by name or tag…", + class: "form-input pl-9", autocomplete: "off" %> + </div> + <% end %> + <%= form_with url: models_path, method: :get, class: "shrink-0", id: "sort-form" do %> + <% if @query.present? %><%= hidden_field_tag :q, @query %><% end %> + <% if @pipeline_tag %><%= hidden_field_tag :pipeline_tag, @pipeline_tag %><% end %> + <% if @library %><%= hidden_field_tag :library, @library %><% end %> + <%= select_tag :sort, + options_for_select([["Trending", "trending"], ["Most likes", "likes"], ["Most downloads", "downloads"], ["Recently updated", "recent"]], @sort), + class: "form-input py-2 pr-8 text-sm", onchange: "this.form.submit()" %> + <% end %> + </div> + + <% if @models.any? %> + <div class="card divide-y divide-surface-600"> + <% @models.each do |repo, card| %> + <%= link_to repository_path(repo.user.username, repo.name), + class: "block px-4 py-3 hover:bg-surface-600 transition-colors" do %> + <div class="flex items-center justify-between gap-3"> + <div class="min-w-0"> + <div class="flex items-center gap-2"> + <svg class="w-4 h-4 text-brand-500 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> + <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/> + <path d="m3.27 6.96 8.73 5.05 8.73-5.05M12 22.08V12"/> + </svg> + <span class="font-medium text-gray-100 truncate"><%= repo.full_name %></span> + <% if repo.is_private %><span class="badge-gray">Private</span><% end %> + </div> + <div class="flex items-center gap-2 mt-1 text-xs text-gray-500 flex-wrap"> + <% if (label = pipeline_label(card.pipeline_tag)) %> + <span class="text-brand-400"><%= label %></span> + <span>&middot;</span> + <% end %> + <% if card.library_name.present? %> + <span><%= card.library_name %></span> + <span>&middot;</span> + <% end %> + <span>Updated <%= time_ago_in_words(repo.updated_at) %> ago</span> + </div> + </div> + <div class="flex items-center gap-4 text-xs text-gray-500 shrink-0"> + <span class="flex items-center gap-1" title="Downloads"> + <svg class="w-3.5 h-3.5" viewBox="0 0 16 16" fill="currentColor"><path d="M7.47 10.78a.75.75 0 0 0 1.06 0l3.75-3.75a.75.75 0 0 0-1.06-1.06L8.75 8.44V1.75a.75.75 0 0 0-1.5 0v6.69L4.78 5.97a.75.75 0 0 0-1.06 1.06ZM3.75 13a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5Z"/></svg> + <%= humanize_count(repo.downloads_count) %> + </span> + <span class="flex items-center gap-1" title="Likes"> + <svg class="w-3.5 h-3.5" viewBox="0 0 16 16" fill="currentColor"><path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z"/></svg> + <%= humanize_count(repo.stars_count) %> + </span> + </div> + </div> + <% end %> + <% end %> + </div> + <% else %> + <div class="card px-6 py-16 text-center"> + <p class="text-sm text-gray-400 mb-1">No models found.</p> + <p class="text-xs text-gray-500">Try clearing filters or + <%= link_to "publish the first one", new_repository_path(kind: "model"), class: "text-brand-500 hover:underline" %>. + </p> + </div> + <% end %> + </main> + + </div> +</div>
app/views/pages/home.html.erb
+59 -12
index 768d9a9..094e4ab 100644 --- a/app/views/pages/home.html.erb +++ b/app/views/pages/home.html.erb @@ -5,18 +5,24 @@ <div class="max-w-6xl mx-auto px-4 sm:px-6 py-20 sm:py-28"> <div class="max-w-3xl"> - <h1 class="max-w-2xl text-4xl font-semibold tracking-tight text-gray-100 sm:text-6xl"> - Git hosting that stays out of the way. + + <h1 class="mt-6 max-w-2xl text-4xl font-semibold tracking-tight text-gray-100 sm:text-6xl"> + Code and models, hosted side by side. </h1> <p class="mt-6 max-w-xl text-base leading-7 text-gray-400 sm:text-lg"> - siGit gives you repositories, history, diffs, and a calm place to read code. - Nothing loud. Nothing extra. + siGit is git hosting built for the AI era. Version your repositories, publish + open-weights models with full model cards and LFS-backed files, and hand the keys + to your AI agent over the Agent Client Protocol. </p> <div class="mt-10 flex flex-col gap-3 sm:flex-row"> - <%= link_to "Sign in", signin_path, class: "btn-primary px-6 py-3 text-sm justify-center" %> - <a href="#features" class="btn-secondary px-6 py-3 text-sm justify-center">See what it does</a> + <%= link_to "Explore models", models_path, class: "btn-primary px-6 py-3 text-sm justify-center" %> + <% if signed_in? %> + <%= link_to "Browse repositories", repos_path, class: "btn-secondary px-6 py-3 text-sm justify-center" %> + <% else %> + <%= link_to "Sign in", signin_path, class: "btn-secondary px-6 py-3 text-sm justify-center" %> + <% end %> </div> <p class="mt-10 text-sm text-gray-500">or check what <a href="/sigit" class="text-gray-400 underline underline-offset-2 hover:text-gray-200 transition-colors">@sigit</a> is doing</p> @@ -36,15 +42,54 @@ </div> </section> + <%# ── Two pillars: code + models ── %> + <section class="max-w-6xl mx-auto px-4 sm:px-6 pt-16 sm:pt-20"> + <div class="grid grid-cols-1 gap-4 sm:grid-cols-2"> + <%= link_to repos_path, class: "card p-8 hover:border-surface-400 transition-colors group" do %> + <div class="flex items-center gap-3 mb-4"> + <span class="flex h-10 w-10 items-center justify-center rounded-sm border border-surface-500 bg-surface-800 text-gray-300"> + <svg class="w-5 h-5" viewBox="0 0 16 16" fill="currentColor"><path d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8Z"/></svg> + </span> + <h2 class="text-lg font-semibold text-gray-100">Code repositories</h2> + </div> + <p class="text-sm leading-6 text-gray-400"> + Fast diffs, deep file browsing, instant repos. Public or private, just git push. + </p> + <span class="mt-4 inline-flex items-center gap-1 text-sm text-brand-500 group-hover:gap-2 transition-all"> + Browse repositories + <svg class="w-3.5 h-3.5" viewBox="0 0 16 16" fill="currentColor"><path d="M8.22 2.97a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L10.94 8.75H2.75a.75.75 0 0 1 0-1.5h8.19L8.22 4.03a.75.75 0 0 1 0-1.06Z"/></svg> + </span> + <% end %> + + <%= link_to models_path, class: "card p-8 hover:border-surface-400 transition-colors group" do %> + <div class="flex items-center gap-3 mb-4"> + <span class="flex h-10 w-10 items-center justify-center rounded-sm border border-surface-500 bg-surface-800 text-brand-500"> + <svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.27 6.96 8.73 5.05 8.73-5.05M12 22.08V12"/></svg> + </span> + <h2 class="text-lg font-semibold text-gray-100">Model library</h2> + </div> + <p class="text-sm leading-6 text-gray-400"> + A home for open weights. Model cards, quantized GGUF and safetensors, downloads and likes. + </p> + <span class="mt-4 inline-flex items-center gap-1 text-sm text-brand-500 group-hover:gap-2 transition-all"> + Explore models + <svg class="w-3.5 h-3.5" viewBox="0 0 16 16" fill="currentColor"><path d="M8.22 2.97a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L10.94 8.75H2.75a.75.75 0 0 1 0-1.5h8.19L8.22 4.03a.75.75 0 0 1 0-1.06Z"/></svg> + </span> + <% end %> + </div> + </section> + + <%# ── Why siGit ── %> <section id="features" class="max-w-6xl mx-auto px-4 sm:px-6 py-16 sm:py-20"> <div class="grid grid-cols-1 gap-px overflow-hidden rounded-sm border border-surface-500 bg-surface-500 sm:grid-cols-3"> <div class="bg-surface-800 p-8 sm:p-10"> <div class="mb-6 flex h-10 w-10 items-center justify-center rounded-sm border border-surface-500 bg-surface-700 text-brand-500"> <span class="text-lg font-semibold">01</span> </div> - <h2 class="text-lg font-semibold text-gray-100">Code Residency</h2> + <h2 class="text-lg font-semibold text-gray-100">One platform for both</h2> <p class="mt-3 text-sm leading-6 text-gray-400"> - Instant repo creation. Public or private. No friction, just git push. + Source code and open-weights models share the same git storage and the same calm UI. + No second tool to learn, no weights scattered across services. </p> </div> @@ -52,9 +97,10 @@ <div class="mb-6 flex h-10 w-10 items-center justify-center rounded-sm border border-surface-500 bg-surface-700 text-brand-500"> <span class="text-lg font-semibold">02</span> </div> - <h2 class="text-lg font-semibold text-gray-100">Native Inspection</h2> + <h2 class="text-lg font-semibold text-gray-100">Built for big files</h2> <p class="mt-3 text-sm leading-6 text-gray-400"> - A UI that gets out of the way. Blazing fast diffs and deep file browsing. + Model cards parsed straight from your README, Git LFS for multi-gigabyte weights, and + file sizes, formats, and quants surfaced at a glance. </p> </div> @@ -62,9 +108,10 @@ <div class="mb-6 flex h-10 w-10 items-center justify-center rounded-sm border border-surface-500 bg-surface-700 text-brand-500"> <span class="text-lg font-semibold">03</span> </div> - <h2 class="text-lg font-semibold text-gray-100">The Dark Room</h2> + <h2 class="text-lg font-semibold text-gray-100">Agent-ready</h2> <p class="mt-3 text-sm leading-6 text-gray-400"> - A low-latency, high-contrast workspace designed for the 2 AM flow state. + siGit speaks the Agent Client Protocol, so your AI coding agent can read, review, and + ship straight from the editor. Low latency, high contrast, made for the 2 AM flow state. </p> </div> </div>
app/views/repositories/_model_use.html.erb
+34
new file mode 100644 index 0000000..a28b73c --- /dev/null +++ b/app/views/repositories/_model_use.html.erb @@ -0,0 +1,34 @@ +<%# + Sidebar "Use this model" panel — clone/download snippets plus a + library-specific load example. Locals: repository, owner, card. +%> +<% full = "#{owner.username}/#{repository.name}" %> +<% lib = card.library_name.to_s.downcase %> + +<div class="card p-4"> + <h3 class="text-xs uppercase tracking-wider text-gray-500 font-semibold mb-3 flex items-center gap-1.5"> + <svg class="w-3.5 h-3.5" viewBox="0 0 16 16" fill="currentColor"><path d="M7.47 10.78a.75.75 0 0 0 1.06 0l3.75-3.75a.75.75 0 0 0-1.06-1.06L8.75 8.44V1.75a.75.75 0 0 0-1.5 0v6.69L4.78 5.97a.75.75 0 0 0-1.06 1.06ZM3.75 13a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5Z"/></svg> + Use this model + </h3> + + <div class="code-container p-3 space-y-0.5 mb-3"> + <p class="text-gray-500"># Download with the siGit CLI</p> + <p class="text-gray-200">sigit download <%= full %></p> + </div> + + <div class="code-container p-3 space-y-0.5"> + <% if lib.include?("gguf") || card.tags.any? { |t| t.downcase.include?("gguf") } %> + <p class="text-gray-500"># Run with llama.cpp</p> + <p class="text-gray-200">llama-cli -hf <%= full %></p> + <% elsif lib.include?("transformers") || lib.empty? %> + <p class="text-gray-500"># Load with Transformers</p> + <p class="text-gray-200">from transformers import AutoModel</p> + <p class="text-gray-200">m = AutoModel.from_pretrained(</p> + <p class="text-gray-200">&nbsp;&nbsp;"<%= full %>",</p> + <p class="text-gray-200">&nbsp;&nbsp;revision="<%= repository.default_branch %>")</p> + <% else %> + <p class="text-gray-500"># Clone the repository</p> + <p class="text-gray-200">git clone https://sigit.si/<%= full %></p> + <% end %> + </div> +</div>
app/views/repositories/explore.html.erb
+76
new file mode 100644 index 0000000..68b386e --- /dev/null +++ b/app/views/repositories/explore.html.erb @@ -0,0 +1,76 @@ +<% content_for :title, "Repositories" %> + +<div class="max-w-6xl mx-auto px-4 sm:px-6 py-8"> + + <div class="flex items-end justify-between gap-4 mb-6 flex-wrap"> + <div> + <h1 class="text-2xl font-semibold text-gray-100 flex items-center gap-2"> + <svg class="w-6 h-6 text-brand-500" viewBox="0 0 16 16" fill="currentColor"> + <path d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8Z"/> + </svg> + Repositories + <span class="badge-gray text-sm align-middle"><%= number_with_delimiter(@repositories.size) %></span> + </h1> + <p class="text-sm text-gray-400 mt-1">Source code hosted on siGit — browse, clone, and build on it.</p> + </div> + <% if signed_in? %> + <%= link_to new_repository_path, class: "btn-primary text-xs py-2 px-4" do %> + <svg class="w-3.5 h-3.5" viewBox="0 0 16 16" fill="currentColor"><path d="M7.75 2a.75.75 0 0 1 .75.75V7h4.25a.75.75 0 0 1 0 1.5H8.5v4.25a.75.75 0 0 1-1.5 0V8.5H2.75a.75.75 0 0 1 0-1.5H7V2.75A.75.75 0 0 1 7.75 2Z"/></svg> + New repository + <% end %> + <% end %> + </div> + + <div class="flex items-center gap-3 mb-4"> + <%= form_with url: repos_path, method: :get, class: "flex-1" do %> + <%= hidden_field_tag :sort, @sort %> + <div class="relative"> + <svg class="w-4 h-4 text-gray-500 absolute left-3 top-1/2 -translate-y-1/2" viewBox="0 0 16 16" fill="currentColor"><path d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-1.06 1.06ZM11.5 7a4.5 4.5 0 1 0-9 0 4.5 4.5 0 0 0 9 0Z"/></svg> + <%= text_field_tag :q, @query, placeholder: "Search repositories…", + class: "form-input pl-9", autocomplete: "off" %> + </div> + <% end %> + <%= form_with url: repos_path, method: :get, class: "shrink-0" do %> + <% if @query.present? %><%= hidden_field_tag :q, @query %><% end %> + <%= select_tag :sort, + options_for_select([["Recently updated", "recent"], ["Most stars", "stars"], ["Name", "name"]], @sort), + class: "form-input py-2 pr-8 text-sm", onchange: "this.form.submit()" %> + <% end %> + </div> + + <% if @repositories.any? %> + <div class="card divide-y divide-surface-600"> + <% @repositories.each do |repo| %> + <%= link_to repository_path(repo.user.username, repo.name), + class: "block px-4 py-3 hover:bg-surface-600 transition-colors" do %> + <div class="flex items-center justify-between gap-3"> + <div class="min-w-0"> + <div class="flex items-center gap-2"> + <svg class="w-4 h-4 text-gray-500 shrink-0" viewBox="0 0 16 16" fill="currentColor"> + <path d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8Z"/> + </svg> + <span class="font-medium text-gray-100 truncate"><%= repo.full_name %></span> + <% if repo.is_private %><span class="badge-gray">Private</span><% end %> + </div> + <% if repo.description.present? %> + <p class="text-xs text-gray-400 mt-1 truncate max-w-xl"><%= repo.description %></p> + <% end %> + <p class="text-xs text-gray-500 mt-1">Updated <%= time_ago_in_words(repo.updated_at) %> ago</p> + </div> + <div class="flex items-center gap-1 text-xs text-gray-500 shrink-0" title="Stars"> + <svg class="w-3.5 h-3.5" viewBox="0 0 16 16" fill="currentColor"><path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z"/></svg> + <%= number_with_delimiter(repo.stars_count) %> + </div> + </div> + <% end %> + <% end %> + </div> + <% else %> + <div class="card px-6 py-16 text-center"> + <p class="text-sm text-gray-400 mb-1">No repositories found.</p> + <% if signed_in? %> + <p class="text-xs text-gray-500"><%= link_to "Create one", new_repository_path, class: "text-brand-500 hover:underline" %> to get started.</p> + <% end %> + </div> + <% end %> +</div>
app/views/repositories/model.html.erb
+217
new file mode 100644 index 0000000..eea3c2f --- /dev/null +++ b/app/views/repositories/model.html.erb @@ -0,0 +1,217 @@ +<% content_for :title, @repository.full_name %> +<% card = @card || @repository.model_card %> + +<div data-controller="tabs" data-tabs-active-value="<%= params[:tab] || 'card' %>"> + +<%# ── Model header band ── %> +<div class="border-b border-surface-600 bg-surface-800"> + <div class="max-w-6xl mx-auto px-4 sm:px-6 pt-6 pb-0"> + + <div class="flex items-start justify-between gap-4 mb-4"> + <div class="min-w-0"> + <div class="flex items-center gap-2 text-sm flex-wrap"> + <%# cube / weights glyph %> + <svg class="w-5 h-5 text-brand-500 shrink-0" viewBox="0 0 24 24" fill="none" + stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> + <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/> + <path d="m3.27 6.96 8.73 5.05 8.73-5.05M12 22.08V12"/> + </svg> + <%= link_to "@#{@owner.username}", user_profile_path(@owner.username), + class: "text-brand-500 hover:underline font-medium" %> + <span class="text-gray-500">/</span> + <%= link_to @repository.name, repository_path(@owner.username, @repository.name), + class: "text-gray-100 hover:underline font-semibold text-base" %> + <% if @repository.is_private %> + <span class="badge-gray ml-1">Private</span> + <% end %> + </div> + + <% if @repository.description.present? %> + <p class="text-sm text-gray-400 mt-2 max-w-2xl"><%= @repository.description %></p> + <% end %> + + <%# ── Metadata pills ── %> + <div class="flex items-center gap-2 flex-wrap mt-3 text-xs"> + <% if (label = pipeline_label(card.pipeline_tag)) %> + <%= link_to label, models_path(pipeline_tag: card.pipeline_tag), + class: "badge-blue hover:bg-brand-500/20" %> + <% end %> + <% if card.library_name.present? %> + <%= link_to models_path(library: card.library_name), + class: "badge-gray hover:bg-surface-500 flex items-center gap-1" do %> + <svg class="w-3 h-3" viewBox="0 0 16 16" fill="currentColor"><path d="M3 2.75A.75.75 0 0 1 3.75 2h8.5a.75.75 0 0 1 .75.75v10.5a.75.75 0 0 1-.75.75h-8.5a.75.75 0 0 1-.75-.75Zm1.5.75v9h7v-9Z"/></svg> + <%= card.library_name %> + <% end %> + <% end %> + <% if card.license.present? %> + <span class="badge-gray">License: <%= card.license %></span> + <% end %> + <% card.tags.first(6).each do |tag| %> + <span class="badge-gray text-gray-400"><%= tag %></span> + <% end %> + </div> + </div> + + <%# ── Like / download stats ── %> + <div class="flex items-center gap-2 shrink-0"> + <%= button_to repository_star_path(@owner.username, @repository.name), + method: :post, + class: "btn-secondary text-xs py-1.5 px-3 #{'text-amber-300 border-amber-500/40' if @repository.starred_by?(current_user)}" do %> + <svg class="w-3.5 h-3.5" viewBox="0 0 16 16" + fill="<%= @repository.starred_by?(current_user) ? 'currentColor' : 'none' %>" + stroke="currentColor" stroke-width="1.5"> + <path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z"/> + </svg> + <span>Like</span> + <span class="badge-gray ml-0.5"><%= number_with_delimiter(@repository.stars_count) %></span> + <% end %> + </div> + </div> + + <%# ── Tabs ── %> + <div class="flex items-center gap-0 -mb-px"> + <button data-tabs-target="tab" data-tabs-key="card" data-action="click->tabs#switch" + class="tab-item active flex items-center gap-1.5"> + <svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor"> + <path d="M0 1.75A.75.75 0 0 1 .75 1h4.253c1.227 0 2.317.59 3 1.501A3.743 3.743 0 0 1 11.006 1h4.245a.75.75 0 0 1 .75.75v10.5a.75.75 0 0 1-.75.75h-4.507a2.25 2.25 0 0 0-1.591.659l-.622.621a.75.75 0 0 1-1.06 0l-.622-.621A2.25 2.25 0 0 0 5.258 13H.75a.75.75 0 0 1-.75-.75Z"/> + </svg> + Model card + </button> + <button data-tabs-target="tab" data-tabs-key="files" data-action="click->tabs#switch" + class="tab-item flex items-center gap-1.5"> + <svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor"> + <path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Z"/> + </svg> + Files and versions + <% if @files&.any? %><span class="badge-gray text-xs"><%= @files.size %></span><% end %> + </button> + </div> + </div> +</div> + +<%# ── Body ── %> +<div class="max-w-6xl mx-auto px-4 sm:px-6 py-6"> + + <% if @empty %> + <div class="card p-8 text-center max-w-2xl mx-auto"> + <h2 class="text-lg font-semibold text-gray-100 mb-2">This model has no files yet</h2> + <p class="text-sm text-gray-400 mb-6">Push your model card and weights to get started.</p> + <div class="code-container text-left p-4 max-w-lg mx-auto text-xs font-mono space-y-0.5"> + <p class="text-gray-500 mb-1"># Push weights with Git LFS</p> + <p class="text-gray-300">git clone git@sigitsi.com:<%= @owner.username %>/<%= @repository.name %></p> + <p class="text-gray-300">cd <%= @repository.name %></p> + <p class="text-gray-300">git lfs install &amp;&amp; git lfs track "*.safetensors" "*.gguf"</p> + <p class="text-gray-300">git add . &amp;&amp; git commit -m "Add weights"</p> + <p class="text-gray-300">git push origin <%= @repository.default_branch %></p> + </div> + </div> + <% else %> + <div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> + + <%# ── Main column ── %> + <div class="lg:col-span-2 min-w-0"> + <%# Model card panel %> + <div data-tabs-target="panel" data-tabs-key="card"> + <% if @card_html.present? %> + <div class="card"> + <div class="px-6 py-6 prose-readme"><%= @card_html %></div> + </div> + <% else %> + <div class="card px-6 py-12 text-center text-sm text-gray-500"> + No model card yet. Add a <code>README.md</code> to describe this model. + </div> + <% end %> + </div> + + <%# Files and versions panel %> + <div data-tabs-target="panel" data-tabs-key="files" class="hidden"> + <div class="card rounded-b-none border-b-0 px-4 py-2.5 flex items-center justify-between gap-4 bg-surface-600"> + <span class="text-xs text-gray-400 flex items-center gap-2"> + <span class="btn-secondary py-1 px-2.5 text-xs font-mono cursor-default"><%= @branch %></span> + <%= pluralize(@files.size, "file") %> + </span> + <span class="text-xs text-gray-500"> + Total: <span class="text-gray-300"><%= number_to_human_size(@total_size) %></span> + </span> + </div> + <div class="card rounded-t-none"> + <% @files.each do |entry| %> + <div class="file-row"> + <% if entry[:type] == "tree" %> + <svg class="w-4 h-4 text-brand-400 shrink-0" viewBox="0 0 16 16" fill="currentColor"> + <path d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1Z"/> + </svg> + <%= link_to entry[:name], + repository_tree_path(@owner.username, @repository.name, @branch, entry[:path]), + class: "text-brand-500 hover:underline font-medium flex-1" %> + <% else %> + <svg class="w-4 h-4 text-gray-500 shrink-0" viewBox="0 0 16 16" fill="currentColor"> + <path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Z"/> + </svg> + <%= link_to entry[:name], + repository_blob_path(@owner.username, @repository.name, @branch, entry[:path]), + class: "text-gray-200 hover:text-brand-500 hover:underline flex-1 truncate" %> + <div class="flex items-center gap-2 shrink-0"> + <% if (fmt = weight_format(entry[:name])) %> + <span class="badge-blue text-[10px]"><%= fmt %></span> + <% end %> + <% if entry[:lfs] %> + <span class="badge-gray text-[10px] text-gray-400" title="Stored with Git LFS">LFS</span> + <% end %> + <% if entry[:size] %> + <span class="text-xs text-gray-500 font-mono w-20 text-right"><%= number_to_human_size(entry[:size]) %></span> + <% end %> + <%= link_to repository_blob_raw_path(@owner.username, @repository.name, @branch, entry[:path]), + class: "text-gray-500 hover:text-brand-500", title: "Download" do %> + <svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor"><path d="M7.47 10.78a.75.75 0 0 0 1.06 0l3.75-3.75a.75.75 0 0 0-1.06-1.06L8.75 8.44V1.75a.75.75 0 0 0-1.5 0v6.69L4.78 5.97a.75.75 0 0 0-1.06 1.06ZM3.75 13a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5Z"/></svg> + <% end %> + </div> + <% end %> + </div> + <% end %> + </div> + </div> + </div> + + <%# ── Sidebar ── %> + <aside class="lg:col-span-1 space-y-4"> + <%= render "repositories/model_use", repository: @repository, owner: @owner, card: card %> + + <div class="card p-4 space-y-3 text-sm"> + <h3 class="text-xs uppercase tracking-wider text-gray-500 font-semibold">Model details</h3> + <% if card.base_model.present? %> + <div class="flex items-center justify-between gap-2"> + <span class="text-gray-500">Base model</span> + <span class="text-gray-300 truncate font-mono text-xs"><%= card.base_model %></span> + </div> + <% end %> + <% if card.library_name.present? %> + <div class="flex items-center justify-between"><span class="text-gray-500">Library</span><span class="text-gray-300"><%= card.library_name %></span></div> + <% end %> + <% if card.license.present? %> + <div class="flex items-center justify-between"><span class="text-gray-500">License</span><span class="text-gray-300"><%= card.license %></span></div> + <% end %> + <% if card.language.any? %> + <div class="flex items-center justify-between"><span class="text-gray-500">Languages</span><span class="text-gray-300"><%= card.language.first(4).join(", ") %></span></div> + <% end %> + <div class="flex items-center justify-between"><span class="text-gray-500">Downloads</span><span class="text-gray-300"><%= number_with_delimiter(@repository.downloads_count) %></span></div> + <div class="flex items-center justify-between"><span class="text-gray-500">Likes</span><span class="text-gray-300"><%= number_with_delimiter(@repository.stars_count) %></span></div> + <div class="flex items-center justify-between"><span class="text-gray-500">Updated</span><span class="text-gray-300"><%= time_ago_in_words(@repository.updated_at) %> ago</span></div> + </div> + + <% if card.tags.any? %> + <div class="card p-4"> + <h3 class="text-xs uppercase tracking-wider text-gray-500 font-semibold mb-3">Tags</h3> + <div class="flex flex-wrap gap-1.5"> + <% card.tags.each do |tag| %> + <span class="badge-gray text-[11px] text-gray-400"><%= tag %></span> + <% end %> + </div> + </div> + <% end %> + </aside> + + </div> + <% end %> +</div> +</div>
app/views/repositories/new.html.erb
+27
index 1b6a33f..1f50a66 100644 --- a/app/views/repositories/new.html.erb +++ b/app/views/repositories/new.html.erb @@ -5,6 +5,33 @@ <p class="text-sm text-gray-400 mb-8">All your files and their history, in one place.</p> <%= form_with model: @repository, url: "/new", method: :post, class: "space-y-6" do |f| %> + <%# ── Repository type ── %> + <div> + <label class="form-label mb-2">Type</label> + <div class="grid grid-cols-2 gap-3"> + <label class="card px-4 py-3 cursor-pointer hover:border-surface-400 flex items-start gap-3"> + <%= f.radio_button :kind, "code", class: "mt-0.5", checked: @repository.code? %> + <div> + <div class="flex items-center gap-1.5 text-sm font-medium text-gray-100"> + <svg class="w-4 h-4 text-gray-400" viewBox="0 0 16 16" fill="currentColor"><path d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Z"/></svg> + Code + </div> + <p class="text-xs text-gray-400 mt-0.5">Source code with full git history.</p> + </div> + </label> + <label class="card px-4 py-3 cursor-pointer hover:border-surface-400 flex items-start gap-3"> + <%= f.radio_button :kind, "model", class: "mt-0.5", checked: @repository.model? %> + <div> + <div class="flex items-center gap-1.5 text-sm font-medium text-gray-100"> + <svg class="w-4 h-4 text-brand-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.27 6.96 8.73 5.05 8.73-5.05M12 22.08V12"/></svg> + Model + </div> + <p class="text-xs text-gray-400 mt-0.5">Open weights with a model card &amp; LFS files.</p> + </div> + </label> + </div> + </div> + <% if @repository.errors.any? %> <div class="border border-red-800/40 bg-red-900/20 rounded px-4 py-3"> <p class="text-sm font-medium text-red-300 mb-1">Please fix the following errors:</p>
app/views/shared/_navbar.html.erb
+4 -5
index ba64dbb..f419f4c 100644 --- a/app/views/shared/_navbar.html.erb +++ b/app/views/shared/_navbar.html.erb @@ -7,11 +7,10 @@ <span class="font-semibold text-base tracking-tight">Code &amp; Deploy</span> <% end %> - <% if signed_in? %> - <div class="hidden sm:flex items-center gap-1"> - <%= link_to "Explore", "#", class: "nav-link px-2 py-1 rounded hover:bg-surface-600" %> - </div> - <% end %> + <div class="hidden sm:flex items-center gap-1"> + <%= link_to "Models", models_path, class: "nav-link px-2 py-1 rounded hover:bg-surface-600" %> + <%= link_to "Repos", repos_path, class: "nav-link px-2 py-1 rounded hover:bg-surface-600" %> + </div> </div> <div class="flex items-center gap-3">
app/views/users/_activity_item.html.erb
+1 -1
index 5b5861f..9d226b0 100644 --- a/app/views/users/_activity_item.html.erb +++ b/app/views/users/_activity_item.html.erb @@ -35,7 +35,7 @@ <div class="card px-4 py-3"> <p class="text-xs text-gray-500 mb-2"><%= time_ago_in_words(item[:at]) %> ago</p> <p class="text-sm"> - <span class="text-gray-400">Created repository </span> + <span class="text-gray-400">Created <%= item[:repo].model? ? "model" : "repository" %> </span> <%= link_to "@#{profile_user.username}/#{item[:repo].name}", repository_path(profile_user.username, item[:repo].name), class: "text-brand-500 hover:underline font-medium" %> </p> </div>
app/views/users/show.html.erb
+86 -6
index 986f3d4..5b2c91d 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -122,7 +122,19 @@ <path d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8Z"/> </svg> Repositories - <span class="badge-gray ml-1.5 align-middle"><%= @repositories.count %></span> + <span class="badge-gray ml-1.5 align-middle"><%= @code_repositories.count %></span> + </button> + <button data-tabs-target="tab" + data-tabs-key="models" + data-action="click->tabs#switch" + class="tab-item"> + <svg class="inline-block w-3.5 h-3.5 mr-1.5 -mt-0.5" viewBox="0 0 24 24" fill="none" + stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> + <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/> + <path d="m3.27 6.96 8.73 5.05 8.73-5.05M12 22.08V12"/> + </svg> + Models + <span class="badge-gray ml-1.5 align-middle"><%= @model_repositories.count %></span> </button> </div> @@ -230,7 +242,7 @@ <p class="text-xs text-gray-500 mb-2">2 weeks ago</p> <p class="text-sm"> <span class="text-gray-400">Starred </span> - <%= link_to "bartowski/Qwen2.5-3B-Instruct-GGUF", "https://huggingface.co/bartowski/Qwen2.5-3B-Instruct-GGUF", class: "text-brand-500 hover:underline font-medium", target: "_blank", rel: "noopener noreferrer" %> + <%= link_to "bartowski/Qwen2.5-3B-Instruct-GGUF", repository_path("bartowski", "Qwen2.5-3B-Instruct-GGUF"), class: "text-brand-500 hover:underline font-medium" %> </p> </div> </li> @@ -247,21 +259,21 @@ <% end %> </div> - <%# ── Repositories panel ── %> + <%# ── Repositories panel (code) ── %> <div data-tabs-target="panel" data-tabs-key="repositories" class="hidden"> <div class="flex items-center justify-between mb-4"> <h2 class="text-base font-semibold text-gray-100"> Repositories - <span class="badge-gray ml-1 align-middle"><%= @repositories.count %></span> + <span class="badge-gray ml-1 align-middle"><%= @code_repositories.count %></span> </h2> <% if signed_in? && current_user == @profile_user %> <%= link_to new_repository_path, class: "btn-secondary text-xs" do %>New<% end %> <% end %> </div> - <% if @repositories.any? %> + <% if @code_repositories.any? %> <div class="space-y-3"> - <% @repositories.each do |repo| %> + <% @code_repositories.each do |repo| %> <div class="card px-5 py-4 hover:border-surface-400 transition-colors"> <div class="flex items-start justify-between gap-4"> <div class="min-w-0 flex-1"> @@ -291,6 +303,74 @@ <% end %> </div> + <%# ── Models panel ── %> + <div data-tabs-target="panel" data-tabs-key="models" class="hidden"> + <div class="flex items-center justify-between mb-4"> + <h2 class="text-base font-semibold text-gray-100"> + Models + <span class="badge-gray ml-1 align-middle"><%= @model_repositories.count %></span> + </h2> + <% if signed_in? && current_user == @profile_user %> + <%= link_to new_repository_path(kind: "model"), class: "btn-secondary text-xs" do %>New<% end %> + <% end %> + </div> + + <% if @model_repositories.any? %> + <div class="space-y-3"> + <% @model_repositories.each do |repo| %> + <% card = repo.model_card %> + <div class="card px-5 py-4 hover:border-surface-400 transition-colors"> + <div class="flex items-start justify-between gap-4"> + <div class="min-w-0 flex-1"> + <div class="flex items-center gap-2 mb-1"> + <svg class="w-4 h-4 text-brand-500 shrink-0" viewBox="0 0 24 24" fill="none" + stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> + <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/> + <path d="m3.27 6.96 8.73 5.05 8.73-5.05M12 22.08V12"/> + </svg> + <%= link_to repo.name, + repository_path(@profile_user.username, repo.name), + class: "font-semibold text-brand-500 hover:underline" %> + <% if repo.is_private %> + <span class="badge-gray">Private</span> + <% end %> + </div> + <div class="flex items-center gap-2 text-xs text-gray-500 flex-wrap"> + <% if (label = pipeline_label(card.pipeline_tag)) %> + <span class="text-brand-400"><%= label %></span> + <span>&middot;</span> + <% end %> + <% if card.library_name.present? %> + <span><%= card.library_name %></span> + <span>&middot;</span> + <% end %> + <span class="flex items-center gap-1"> + <svg class="w-3 h-3" viewBox="0 0 16 16" fill="currentColor"><path d="M7.47 10.78a.75.75 0 0 0 1.06 0l3.75-3.75a.75.75 0 0 0-1.06-1.06L8.75 8.44V1.75a.75.75 0 0 0-1.5 0v6.69L4.78 5.97a.75.75 0 0 0-1.06 1.06ZM3.75 13a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5Z"/></svg> + <%= humanize_count(repo.downloads_count) %> + </span> + <span class="flex items-center gap-1"> + <svg class="w-3 h-3" viewBox="0 0 16 16" fill="currentColor"><path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z"/></svg> + <%= humanize_count(repo.stars_count) %> + </span> + </div> + </div> + <div class="text-xs text-gray-500 shrink-0"> + <%= time_ago_in_words(repo.updated_at) %> ago + </div> + </div> + </div> + <% end %> + </div> + <% else %> + <div class="card px-6 py-12 text-center"> + <p class="text-sm text-gray-500 mb-1">No models yet.</p> + <% if signed_in? && current_user == @profile_user %> + <p class="text-xs text-gray-500"><%= link_to "Publish a model", new_repository_path(kind: "model"), class: "text-brand-500 hover:underline" %> to share open weights.</p> + <% end %> + </div> + <% end %> + </div> + </main> </div> </div>
config/routes.rb
+11 -1
index 66b7b9b..e0d304a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -22,6 +22,11 @@ Rails.application.routes.draw do get "/auth/confirmation/resend", to: "confirmations#new", as: :resend_confirmation post "/auth/confirmation/resend", to: "confirmations#create" + # Discovery surfaces — declared before the "/:username" matcher so they + # aren't read as profiles. + get "/models", to: "models#index", as: :models # open-weights model library + get "/repos", to: "repositories#explore", as: :repos # public code repositories + # Repository creation get "/new", to: "repositories#new", as: :new_repository post "/new", to: "repositories#create" @@ -56,7 +61,12 @@ Rails.application.routes.draw do get "/:username/:repository/raw/:branch/*path", to: "blobs#raw", as: :repository_blob_raw, format: false get "/:username/:repository/tree/:branch/*path", to: "repositories#tree", as: :repository_tree get "/:username/:repository/tree/:branch", to: "repositories#tree", as: :repository_branch - get "/:username/:repository", to: "repositories#show", as: :repository + post "/:username/:repository/star", to: "stars#create", as: :repository_star, + constraints: { repository: /[^\/.][^\/]*/ }, format: false + # `format: false` keeps dotted names (e.g. "Qwen2.5-3B-Instruct-GGUF") intact + # rather than treating the dot as a response-format separator. + get "/:username/:repository", to: "repositories#show", as: :repository, + constraints: { repository: /[^\/.][^\/]*/ }, format: false # Health check get "up" => "rails/health#show", as: :rails_health_check
db/migrate/20250101000004_add_model_support_and_stars.rb
+22
new file mode 100644 index 0000000..7cb2975 --- /dev/null +++ b/db/migrate/20250101000004_add_model_support_and_stars.rb @@ -0,0 +1,22 @@ +class AddModelSupportAndStars < ActiveRecord::Migration[8.1] + def change + # A repository is either regular source code ("code") or a published set of + # open weights ("model") — the Hugging Face-style model library lives on the + # same git storage as everything else, distinguished only by this kind. + add_column :repositories, :kind, :string, null: false, default: "code" + add_index :repositories, :kind + + # Cached counters so the discovery index can sort/show without extra queries. + add_column :repositories, :stars_count, :integer, null: false, default: 0 + add_column :repositories, :downloads_count, :integer, null: false, default: 0 + + # Stars double as Hugging Face "likes" for models and GitHub-style stars for + # code repos — one row per (user, repository). + create_table :stars do |t| + t.references :user, null: false, foreign_key: true + t.references :repository, null: false, foreign_key: true + t.timestamps + end + add_index :stars, [ :user_id, :repository_id ], unique: true + end +end
db/schema.rb
+17 -1
index 64c28ec..057d71f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2025_01_01_000003) do +ActiveRecord::Schema[8.1].define(version: 2025_01_01_000004) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -19,10 +19,14 @@ ActiveRecord::Schema[8.1].define(version: 2025_01_01_000003) do t.string "default_branch", default: "main", null: false t.text "description" t.string "disk_path", null: false + t.integer "downloads_count", default: 0, null: false t.boolean "is_private", default: false, null: false + t.string "kind", default: "code", null: false t.string "name", null: false + t.integer "stars_count", default: 0, null: false t.datetime "updated_at", null: false t.bigint "user_id", null: false + t.index ["kind"], name: "index_repositories_on_kind" t.index ["user_id", "name"], name: "index_repositories_on_user_id_and_name", unique: true t.index ["user_id"], name: "index_repositories_on_user_id" end @@ -37,6 +41,16 @@ ActiveRecord::Schema[8.1].define(version: 2025_01_01_000003) do t.index ["user_id"], name: "index_ssh_keys_on_user_id" end + create_table "stars", force: :cascade do |t| + t.datetime "created_at", null: false + t.bigint "repository_id", null: false + t.datetime "updated_at", null: false + t.bigint "user_id", null: false + t.index ["repository_id"], name: "index_stars_on_repository_id" + t.index ["user_id", "repository_id"], name: "index_stars_on_user_id_and_repository_id", unique: true + t.index ["user_id"], name: "index_stars_on_user_id" + end + create_table "users", force: :cascade do |t| t.string "access_token" t.string "avatar_url" @@ -53,4 +67,6 @@ ActiveRecord::Schema[8.1].define(version: 2025_01_01_000003) do add_foreign_key "repositories", "users" add_foreign_key "ssh_keys", "users" + add_foreign_key "stars", "repositories" + add_foreign_key "stars", "users" end
db/seeds.rb
+284 -6
index 4fbd6ed..811db75 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,9 +1,287 @@ # This file should ensure the existence of records required to run the application in every environment (production, # development, test). The code here should be idempotent so that it can be executed at any point in every environment. # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). -# -# Example: -# -# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| -# MovieGenre.find_or_create_by!(name: genre_name) -# end + +require "digest" +require "open3" +require "fileutils" +require "tmpdir" + +# --------------------------------------------------------------------------- +# Demo model library — seeds a handful of Hugging Face-style model repositories +# so the /models discovery page and model pages work out of the box. +# --------------------------------------------------------------------------- + +# A Git LFS pointer file: the tiny text stub that stands in for a large weight +# file. The UI resolves the real `size` and shows an "LFS" badge. +def lfs_pointer(size_bytes, seed) + oid = Digest::SHA256.hexdigest("#{seed}-#{size_bytes}") + "version https://git-lfs.github.com/spec/v1\noid sha256:#{oid}\nsize #{size_bytes}\n" +end + +GB = 1024 * 1024 * 1024 +MB = 1024 * 1024 + +# Pushes README + files into the model's bare repo (idempotent: skips if the +# default branch already has commits). +def seed_model_files(repo, card:, files:) + path = repo.disk_path + GitRepositoryService.create_bare_repo(repo.user.username, repo.name) unless repo.initialized? + return if GitRepositoryService.branch_exists?(path, repo.default_branch) + + Dir.mktmpdir do |dir| + system("git", "-C", dir, "init", "-b", repo.default_branch, exception: true) + system("git", "-C", dir, "config", "user.email", "sigitsi@localhost", exception: true) + system("git", "-C", dir, "config", "user.name", "siGit", exception: true) + # The seed writes Git LFS *pointer* text directly — we do not want the local + # git-lfs filter (if installed) to try to clean/upload them as real objects. + # Neutralise the LFS filters so the pointer bytes are committed verbatim. + %w[clean smudge].each { |f| system("git", "-C", dir, "config", "filter.lfs.#{f}", "cat", exception: true) } + system("git", "-C", dir, "config", "filter.lfs.process", "", exception: true) + system("git", "-C", dir, "config", "filter.lfs.required", "false", exception: true) + + File.write(File.join(dir, "README.md"), card) + File.write(File.join(dir, ".gitattributes"), + "*.gguf filter=lfs diff=lfs merge=lfs -text\n*.safetensors filter=lfs diff=lfs merge=lfs -text\n") + files.each do |f| + full = File.join(dir, f[:path]) + FileUtils.mkdir_p(File.dirname(full)) + File.write(full, f[:content]) + end + + system("git", "-C", dir, "add", ".", exception: true) + system("git", "-C", dir, "commit", "-m", "Upload model", exception: true) + system("git", "-C", dir, "remote", "add", "origin", path, exception: true) + system("git", "-C", dir, "push", "origin", repo.default_branch, exception: true) + end +end + +def seed_model(username:, email:, smbcloud_id:, name:, description:, card:, files:, downloads:, likes:, updated_at: Time.current) + user = User.find_or_create_by!(smbcloud_id: smbcloud_id) do |u| + u.username = username + u.email = email + end + + repo = user.repositories.find_or_initialize_by(name: name) + repo.assign_attributes( + kind: "model", + description: description, + default_branch: "main", + disk_path: GitRepositoryService.repo_path(username, name), + downloads_count: downloads, + stars_count: likes, + updated_at: updated_at + ) + repo.save! + + seed_model_files(repo, card: card, files: files) + repo +rescue => e + warn " ! Skipped #{username}/#{name}: #{e.message}" + nil +end + +# ── bartowski/Qwen2.5-3B-Instruct-GGUF (the showcase model) ── +qwen_card = <<~MD + --- + license: apache-2.0 + base_model: Qwen/Qwen2.5-3B-Instruct + pipeline_tag: text-generation + library_name: gguf + language: + - en + tags: + - text-generation + - gguf + - quantized + - qwen2.5 + --- + + # Qwen2.5-3B-Instruct-GGUF + + GGUF quantizations of [Qwen/Qwen2.5-3B-Instruct](https://sigit.si/qwen/Qwen2.5-3B-Instruct), + produced with `llama.cpp`. Pick a quant that fits your hardware — higher bits + mean better quality, lower bits mean a smaller file and faster inference. + + ## Quant table + + | File | Quant | Size | Notes | + | ---- | ----- | ---- | ----- | + | `Qwen2.5-3B-Instruct-Q8_0.gguf` | Q8_0 | 3.3 GB | Near-lossless, recommended for quality | + | `Qwen2.5-3B-Instruct-Q5_K_M.gguf` | Q5_K_M | 2.2 GB | Balanced quality / size | + | `Qwen2.5-3B-Instruct-Q4_K_M.gguf` | Q4_K_M | 1.9 GB | Good default for most setups | + | `Qwen2.5-3B-Instruct-Q3_K_M.gguf` | Q3_K_M | 1.6 GB | Smallest, some quality loss | + + ## Run it + + ```bash + llama-cli -hf bartowski/Qwen2.5-3B-Instruct-GGUF -p "Hello!" + ``` + + ## Prompt format + + ``` + <|im_start|>system + You are a helpful assistant.<|im_end|> + <|im_start|>user + {prompt}<|im_end|> + <|im_start|>assistant + ``` +MD + +qwen_files = [ + { path: "Qwen2.5-3B-Instruct-Q8_0.gguf", content: lfs_pointer((3.3 * GB).to_i, "q8") }, + { path: "Qwen2.5-3B-Instruct-Q5_K_M.gguf", content: lfs_pointer((2.2 * GB).to_i, "q5") }, + { path: "Qwen2.5-3B-Instruct-Q4_K_M.gguf", content: lfs_pointer((1.9 * GB).to_i, "q4") }, + { path: "Qwen2.5-3B-Instruct-Q3_K_M.gguf", content: lfs_pointer((1.6 * GB).to_i, "q3") }, + { path: "config.json", content: %({\n "model_type": "qwen2",\n "quantized_by": "bartowski"\n}\n) } +] + +seed_model( + username: "bartowski", email: "bartowski@example.com", smbcloud_id: 900_001, + name: "Qwen2.5-3B-Instruct-GGUF", + description: "GGUF quants of Qwen2.5-3B-Instruct for llama.cpp.", + card: qwen_card, files: qwen_files, + downloads: 1_284_530, likes: 412, updated_at: 2.weeks.ago +) + +# ── meta-llama/Llama-3.2-1B-Instruct (safetensors / transformers) ── +llama_card = <<~MD + --- + license: llama3.2 + base_model: meta-llama/Llama-3.2-1B + pipeline_tag: text-generation + library_name: transformers + language: + - en + tags: + - text-generation + - llama + - instruct + --- + + # Llama-3.2-1B-Instruct + + A compact 1B instruction-tuned model. Load it with 🤗 Transformers: + + ```python + from transformers import AutoModelForCausalLM, AutoTokenizer + + model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3.2-1B-Instruct") + tok = AutoTokenizer.from_pretrained("meta-llama/Llama-3.2-1B-Instruct") + ``` +MD + +llama_files = [ + { path: "model.safetensors", content: lfs_pointer((2.5 * GB).to_i, "llama-st") }, + { path: "config.json", content: %({\n "model_type": "llama",\n "hidden_size": 2048\n}\n) }, + { path: "tokenizer.json", content: lfs_pointer((9 * MB).to_i, "llama-tok") }, + { path: "generation_config.json", content: %({\n "temperature": 0.6,\n "top_p": 0.9\n}\n) } +] + +seed_model( + username: "meta-llama", email: "meta-llama@example.com", smbcloud_id: 900_002, + name: "Llama-3.2-1B-Instruct", + description: "Lightweight 1B instruction-tuned text model.", + card: llama_card, files: llama_files, + downloads: 3_902_117, likes: 1_287, updated_at: 5.days.ago +) + +# ── sentence-transformers/all-MiniLM-L6-v2 (embeddings) ── +minilm_card = <<~MD + --- + license: apache-2.0 + pipeline_tag: sentence-similarity + library_name: sentence-transformers + tags: + - sentence-similarity + - feature-extraction + - embeddings + --- + + # all-MiniLM-L6-v2 + + Maps sentences and paragraphs to a 384-dimensional dense vector space for + semantic search, clustering, and retrieval. + + ```python + from sentence_transformers import SentenceTransformer + model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2") + emb = model.encode(["A sentence to embed."]) + ``` +MD + +minilm_files = [ + { path: "model.safetensors", content: lfs_pointer((90 * MB).to_i, "minilm-st") }, + { path: "config.json", content: %({\n "model_type": "bert",\n "hidden_size": 384\n}\n) }, + { path: "tokenizer.json", content: lfs_pointer((700 * 1024).to_i, "minilm-tok") } +] + +seed_model( + username: "sentence-transformers", email: "st@example.com", smbcloud_id: 900_003, + name: "all-MiniLM-L6-v2", + description: "384-dim sentence embeddings for semantic search.", + card: minilm_card, files: minilm_files, + downloads: 8_450_990, likes: 2_034, updated_at: 1.month.ago +) + +# ── A model published by the demo @sigit profile, so its Models tab has content ── +# Attaches to the existing "sigit" user rather than creating one. On-brand: a +# small code-completion model the platform ships itself. +if (sigit = User.find_by(username: "sigit")) + sigit_card = <<~MD + --- + license: apache-2.0 + base_model: Qwen/Qwen2.5-Coder-1.5B + pipeline_tag: text-generation + library_name: gguf + language: + - en + tags: + - text-generation + - code + - code-completion + - gguf + --- + + # SiGit-Coder-1.5B-GGUF + + A small, fast code-completion model that powers inline suggestions in the + siGit Code & Deploy editor. GGUF quantizations run locally with `llama.cpp`, + so completions stay on your machine. + + ## Quant table + + | File | Quant | Size | Notes | + | ---- | ----- | ---- | ----- | + | `SiGit-Coder-1.5B-Q8_0.gguf` | Q8_0 | 1.6 GB | Best quality | + | `SiGit-Coder-1.5B-Q5_K_M.gguf` | Q5_K_M | 1.1 GB | Balanced | + | `SiGit-Coder-1.5B-Q4_K_M.gguf` | Q4_K_M | 1.0 GB | Recommended default | + + ## Run it + + ```bash + llama-cli -hf sigit/SiGit-Coder-1.5B-GGUF -p "def fib(n):" + ``` + MD + + sigit_files = [ + { path: "SiGit-Coder-1.5B-Q8_0.gguf", content: lfs_pointer((1.6 * GB).to_i, "sigit-q8") }, + { path: "SiGit-Coder-1.5B-Q5_K_M.gguf", content: lfs_pointer((1.1 * GB).to_i, "sigit-q5") }, + { path: "SiGit-Coder-1.5B-Q4_K_M.gguf", content: lfs_pointer((1.0 * GB).to_i, "sigit-q4") }, + { path: "config.json", content: %({\n "model_type": "qwen2",\n "quantized_by": "sigit"\n}\n) } + ] + + repo = sigit.repositories.find_or_initialize_by(name: "SiGit-Coder-1.5B-GGUF") + repo.assign_attributes( + kind: "model", + description: "Local code-completion model behind the siGit editor's inline suggestions.", + default_branch: "main", + disk_path: GitRepositoryService.repo_path(sigit.username, "SiGit-Coder-1.5B-GGUF"), + downloads_count: 47_213, stars_count: 96, updated_at: 4.days.ago + ) + repo.save! + seed_model_files(repo, card: sigit_card, files: sigit_files) +end + +puts "Seeded #{Repository.models.count} model repositories."
repos/.keep
new file mode 100644 index 0000000..e69de29