htm 0.0.15 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +1 -0
  3. data/CHANGELOG.md +67 -0
  4. data/README.md +97 -1592
  5. data/bin/htm_mcp +31 -0
  6. data/config/database.yml +7 -4
  7. data/docs/getting-started/installation.md +31 -11
  8. data/docs/guides/mcp-server.md +456 -21
  9. data/docs/multi_framework_support.md +2 -2
  10. data/examples/mcp_client.rb +2 -2
  11. data/examples/rails_app/.gitignore +2 -0
  12. data/examples/rails_app/Gemfile +22 -0
  13. data/examples/rails_app/Gemfile.lock +438 -0
  14. data/examples/rails_app/Procfile.dev +1 -0
  15. data/examples/rails_app/README.md +98 -0
  16. data/examples/rails_app/Rakefile +5 -0
  17. data/examples/rails_app/app/assets/stylesheets/application.css +83 -0
  18. data/examples/rails_app/app/assets/stylesheets/inter-font.css +6 -0
  19. data/examples/rails_app/app/controllers/application_controller.rb +19 -0
  20. data/examples/rails_app/app/controllers/dashboard_controller.rb +27 -0
  21. data/examples/rails_app/app/controllers/files_controller.rb +205 -0
  22. data/examples/rails_app/app/controllers/memories_controller.rb +102 -0
  23. data/examples/rails_app/app/controllers/robots_controller.rb +44 -0
  24. data/examples/rails_app/app/controllers/search_controller.rb +46 -0
  25. data/examples/rails_app/app/controllers/tags_controller.rb +30 -0
  26. data/examples/rails_app/app/javascript/application.js +4 -0
  27. data/examples/rails_app/app/javascript/controllers/application.js +9 -0
  28. data/examples/rails_app/app/javascript/controllers/index.js +6 -0
  29. data/examples/rails_app/app/views/dashboard/index.html.erb +123 -0
  30. data/examples/rails_app/app/views/files/index.html.erb +108 -0
  31. data/examples/rails_app/app/views/files/new.html.erb +321 -0
  32. data/examples/rails_app/app/views/files/show.html.erb +130 -0
  33. data/examples/rails_app/app/views/layouts/application.html.erb +124 -0
  34. data/examples/rails_app/app/views/memories/_memory_card.html.erb +51 -0
  35. data/examples/rails_app/app/views/memories/deleted.html.erb +62 -0
  36. data/examples/rails_app/app/views/memories/edit.html.erb +35 -0
  37. data/examples/rails_app/app/views/memories/index.html.erb +81 -0
  38. data/examples/rails_app/app/views/memories/new.html.erb +71 -0
  39. data/examples/rails_app/app/views/memories/show.html.erb +126 -0
  40. data/examples/rails_app/app/views/robots/index.html.erb +106 -0
  41. data/examples/rails_app/app/views/robots/new.html.erb +36 -0
  42. data/examples/rails_app/app/views/robots/show.html.erb +79 -0
  43. data/examples/rails_app/app/views/search/index.html.erb +184 -0
  44. data/examples/rails_app/app/views/shared/_navbar.html.erb +52 -0
  45. data/examples/rails_app/app/views/shared/_stat_card.html.erb +52 -0
  46. data/examples/rails_app/app/views/tags/index.html.erb +131 -0
  47. data/examples/rails_app/app/views/tags/show.html.erb +67 -0
  48. data/examples/rails_app/bin/dev +8 -0
  49. data/examples/rails_app/bin/rails +4 -0
  50. data/examples/rails_app/bin/rake +4 -0
  51. data/examples/rails_app/config/application.rb +33 -0
  52. data/examples/rails_app/config/boot.rb +5 -0
  53. data/examples/rails_app/config/database.yml +15 -0
  54. data/examples/rails_app/config/environment.rb +5 -0
  55. data/examples/rails_app/config/importmap.rb +7 -0
  56. data/examples/rails_app/config/routes.rb +38 -0
  57. data/examples/rails_app/config/tailwind.config.js +35 -0
  58. data/examples/rails_app/config.ru +5 -0
  59. data/examples/rails_app/log/.keep +0 -0
  60. data/examples/rails_app/tmp/local_secret.txt +1 -0
  61. data/lib/htm/active_record_config.rb +2 -5
  62. data/lib/htm/configuration.rb +35 -2
  63. data/lib/htm/database.rb +3 -6
  64. data/lib/htm/mcp/cli.rb +333 -0
  65. data/lib/htm/mcp/group_tools.rb +476 -0
  66. data/lib/htm/mcp/resources.rb +89 -0
  67. data/lib/htm/mcp/server.rb +98 -0
  68. data/lib/htm/mcp/tools.rb +488 -0
  69. data/lib/htm/models/file_source.rb +5 -3
  70. data/lib/htm/railtie.rb +0 -4
  71. data/lib/htm/tasks.rb +7 -4
  72. data/lib/htm/version.rb +1 -1
  73. data/lib/tasks/htm.rake +6 -9
  74. metadata +59 -4
  75. data/bin/htm_mcp.rb +0 -621
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationController < ActionController::Base
4
+ # HTM instance for the current request
5
+ def htm
6
+ @htm ||= HTM.new(robot_name: current_robot_name)
7
+ end
8
+ helper_method :htm
9
+
10
+ # Allow switching robots via session
11
+ def current_robot_name
12
+ session[:robot_name] || 'explorer'
13
+ end
14
+ helper_method :current_robot_name
15
+
16
+ def current_robot_name=(name)
17
+ session[:robot_name] = name
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DashboardController < ApplicationController
4
+ def index
5
+ # Note: HTM::Models::Node has a default_scope that excludes deleted nodes
6
+ # so we don't need to call .active explicitly
7
+ @stats = {
8
+ total_nodes: HTM::Models::Node.count,
9
+ nodes_with_embeddings: HTM::Models::Node.with_embeddings.count,
10
+ deleted_nodes: HTM::Models::Node.deleted.count,
11
+ total_tags: HTM::Models::Tag.count,
12
+ total_robots: HTM::Models::Robot.count,
13
+ total_file_sources: HTM::Models::FileSource.count
14
+ }
15
+
16
+ @recent_memories = HTM::Models::Node.recent.limit(5)
17
+
18
+ @top_tags = HTM::Models::Tag
19
+ .joins(:nodes)
20
+ .group('tags.id')
21
+ .order('COUNT(nodes.id) DESC')
22
+ .limit(10)
23
+ .select('tags.*, COUNT(nodes.id) as node_count')
24
+
25
+ @robots = HTM::Models::Robot.order(created_at: :desc).limit(5)
26
+ end
27
+ end
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FilesController < ApplicationController
4
+ def index
5
+ @file_sources = HTM::Models::FileSource.order(updated_at: :desc)
6
+ end
7
+
8
+ def show
9
+ @file_source = HTM::Models::FileSource.find(params[:id])
10
+ @chunks = @file_source.chunks.order(:id)
11
+ end
12
+
13
+ def new
14
+ end
15
+
16
+ def create
17
+ path = params[:path]&.strip
18
+
19
+ if path.blank?
20
+ flash[:alert] = 'File path is required'
21
+ redirect_to new_file_path
22
+ return
23
+ end
24
+
25
+ unless File.exist?(path)
26
+ flash[:alert] = "File not found: #{path}"
27
+ redirect_to new_file_path
28
+ return
29
+ end
30
+
31
+ force = params[:force] == 'true'
32
+
33
+ begin
34
+ result = htm.load_file(path, force: force)
35
+ flash[:notice] = "File loaded: #{result[:chunks_created]} chunks created, #{result[:chunks_updated]} updated, #{result[:chunks_deleted]} deleted"
36
+ redirect_to file_path(result[:file_source_id])
37
+ rescue StandardError => e
38
+ flash[:alert] = "Error loading file: #{e.message}"
39
+ redirect_to new_file_path
40
+ end
41
+ end
42
+
43
+ def load_directory
44
+ path = params[:path]&.strip
45
+ pattern = params[:pattern] || '**/*.md'
46
+
47
+ if path.blank?
48
+ flash[:alert] = 'Directory path is required'
49
+ redirect_to new_file_path
50
+ return
51
+ end
52
+
53
+ unless Dir.exist?(path)
54
+ flash[:alert] = "Directory not found: #{path}"
55
+ redirect_to new_file_path
56
+ return
57
+ end
58
+
59
+ begin
60
+ results = htm.load_directory(path, pattern: pattern)
61
+ total_chunks = results.sum { |r| r[:chunks_created] }
62
+ flash[:notice] = "Loaded #{results.length} files with #{total_chunks} total chunks"
63
+ redirect_to files_path
64
+ rescue StandardError => e
65
+ flash[:alert] = "Error loading directory: #{e.message}"
66
+ redirect_to new_file_path
67
+ end
68
+ end
69
+
70
+ def upload
71
+ files = params[:files]
72
+ force = params[:force] == 'true'
73
+
74
+ if files.blank?
75
+ flash[:alert] = 'Please select at least one file'
76
+ redirect_to new_file_path
77
+ return
78
+ end
79
+
80
+ results = process_uploaded_files(files, force: force)
81
+
82
+ if results[:errors].any?
83
+ flash[:alert] = "Loaded #{results[:success_count]} files with #{results[:errors].length} errors: #{results[:errors].first}"
84
+ else
85
+ flash[:notice] = "Loaded #{results[:success_count]} files with #{results[:total_chunks]} total chunks"
86
+ end
87
+
88
+ if results[:last_file_source_id] && results[:success_count] == 1
89
+ redirect_to file_path(results[:last_file_source_id])
90
+ else
91
+ redirect_to files_path
92
+ end
93
+ end
94
+
95
+ def upload_directory
96
+ files = params[:files]
97
+ extension = params[:extension]
98
+
99
+ if files.blank?
100
+ flash[:alert] = 'Please select a directory'
101
+ redirect_to new_file_path
102
+ return
103
+ end
104
+
105
+ # Filter files by extension if specified
106
+ filtered_files = if extension.present?
107
+ files.select { |f| f.original_filename.end_with?(extension) }
108
+ else
109
+ files.select { |f| f.original_filename.match?(/\.(md|markdown|txt)$/i) }
110
+ end
111
+
112
+ if filtered_files.empty?
113
+ flash[:alert] = 'No matching files found in the selected directory'
114
+ redirect_to new_file_path
115
+ return
116
+ end
117
+
118
+ results = process_uploaded_files(filtered_files, force: false)
119
+
120
+ if results[:errors].any?
121
+ flash[:alert] = "Loaded #{results[:success_count]} files with #{results[:errors].length} errors"
122
+ else
123
+ flash[:notice] = "Loaded #{results[:success_count]} files with #{results[:total_chunks]} total chunks"
124
+ end
125
+
126
+ redirect_to files_path
127
+ end
128
+
129
+ def sync
130
+ @file_source = HTM::Models::FileSource.find(params[:id])
131
+
132
+ begin
133
+ result = htm.load_file(@file_source.file_path, force: true)
134
+ flash[:notice] = "File synced: #{result[:chunks_created]} created, #{result[:chunks_updated]} updated, #{result[:chunks_deleted]} deleted"
135
+ rescue StandardError => e
136
+ flash[:alert] = "Error syncing file: #{e.message}"
137
+ end
138
+
139
+ redirect_to file_path(@file_source)
140
+ end
141
+
142
+ def destroy
143
+ @file_source = HTM::Models::FileSource.find(params[:id])
144
+
145
+ begin
146
+ htm.unload_file(@file_source.file_path)
147
+ flash[:notice] = 'File unloaded successfully'
148
+ rescue StandardError => e
149
+ flash[:alert] = "Error unloading file: #{e.message}"
150
+ end
151
+
152
+ redirect_to files_path
153
+ end
154
+
155
+ def sync_all
156
+ synced = 0
157
+ errors = 0
158
+
159
+ HTM::Models::FileSource.find_each do |source|
160
+ if source.needs_sync?
161
+ begin
162
+ htm.load_file(source.file_path, force: true)
163
+ synced += 1
164
+ rescue StandardError
165
+ errors += 1
166
+ end
167
+ end
168
+ end
169
+
170
+ if errors.zero?
171
+ flash[:notice] = "Synced #{synced} files"
172
+ else
173
+ flash[:alert] = "Synced #{synced} files with #{errors} errors"
174
+ end
175
+
176
+ redirect_to files_path
177
+ end
178
+
179
+ private
180
+
181
+ def process_uploaded_files(files, force: false)
182
+ results = { success_count: 0, total_chunks: 0, errors: [], last_file_source_id: nil }
183
+
184
+ # Create uploads directory if it doesn't exist
185
+ uploads_dir = Rails.root.join('tmp', 'uploads')
186
+ FileUtils.mkdir_p(uploads_dir)
187
+
188
+ files.each do |file|
189
+ # Save uploaded file to temp location
190
+ temp_path = uploads_dir.join(file.original_filename)
191
+ File.open(temp_path, 'wb') { |f| f.write(file.read) }
192
+
193
+ begin
194
+ result = htm.load_file(temp_path.to_s, force: force)
195
+ results[:success_count] += 1
196
+ results[:total_chunks] += result[:chunks_created]
197
+ results[:last_file_source_id] = result[:file_source_id]
198
+ rescue StandardError => e
199
+ results[:errors] << "#{file.original_filename}: #{e.message}"
200
+ end
201
+ end
202
+
203
+ results
204
+ end
205
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MemoriesController < ApplicationController
4
+ before_action :set_memory, only: [:show, :edit, :update, :destroy, :restore]
5
+
6
+ def index
7
+ # Default scope excludes deleted nodes, so no .active needed
8
+ @memories = HTM::Models::Node.includes(:tags)
9
+ .order(created_at: :desc)
10
+
11
+ if params[:tag].present?
12
+ @memories = @memories.joins(:tags).where(tags: { name: params[:tag] })
13
+ end
14
+
15
+ if params[:search].present?
16
+ @memories = @memories.where('content ILIKE ?', "%#{params[:search]}%")
17
+ end
18
+
19
+ # Simple pagination without Kaminari
20
+ @page = (params[:page] || 1).to_i
21
+ @per_page = 20
22
+ @total_count = @memories.count
23
+ @total_pages = (@total_count.to_f / @per_page).ceil
24
+ @memories = @memories.offset((@page - 1) * @per_page).limit(@per_page)
25
+ end
26
+
27
+ def show
28
+ @related = htm.recall(@memory.content, limit: 5, strategy: :vector, raw: true)
29
+ .reject { |m| m['id'] == @memory.id }
30
+ end
31
+
32
+ def new
33
+ @memory = HTM::Models::Node.new
34
+ end
35
+
36
+ def create
37
+ content = params[:content] || params.dig(:node, :content)
38
+ tags = params[:tags] || params.dig(:node, :tags)
39
+ metadata = params[:metadata] || params.dig(:node, :metadata)
40
+
41
+ if content.blank?
42
+ flash[:alert] = 'Content is required'
43
+ redirect_to new_memory_path
44
+ return
45
+ end
46
+
47
+ tag_array = tags.present? ? tags.split(',').map(&:strip) : []
48
+ metadata_hash = metadata.present? ? JSON.parse(metadata) : {}
49
+
50
+ node_id = htm.remember(content, tags: tag_array, metadata: metadata_hash)
51
+ flash[:notice] = 'Memory stored successfully'
52
+ redirect_to memory_path(node_id)
53
+ rescue JSON::ParserError
54
+ flash[:alert] = 'Invalid metadata JSON format'
55
+ redirect_to new_memory_path
56
+ end
57
+
58
+ def edit
59
+ end
60
+
61
+ def update
62
+ content = params[:content] || params.dig(:node, :content)
63
+
64
+ if content.blank?
65
+ flash[:alert] = 'Content is required'
66
+ redirect_to edit_memory_path(@memory)
67
+ return
68
+ end
69
+
70
+ @memory.update!(content: content)
71
+ flash[:notice] = 'Memory updated successfully'
72
+ redirect_to memory_path(@memory)
73
+ end
74
+
75
+ def destroy
76
+ soft = params[:permanent] != 'true'
77
+ if soft
78
+ htm.forget(@memory.id)
79
+ flash[:notice] = 'Memory moved to trash. You can restore it later.'
80
+ else
81
+ htm.forget(@memory.id, soft: false, confirm: :confirmed)
82
+ flash[:notice] = 'Memory permanently deleted.'
83
+ end
84
+ redirect_to memories_path
85
+ end
86
+
87
+ def restore
88
+ htm.restore(@memory.id)
89
+ flash[:notice] = 'Memory restored successfully.'
90
+ redirect_to memory_path(@memory)
91
+ end
92
+
93
+ def deleted
94
+ @memories = HTM::Models::Node.deleted.order(deleted_at: :desc)
95
+ end
96
+
97
+ private
98
+
99
+ def set_memory
100
+ @memory = HTM::Models::Node.with_deleted.find(params[:id])
101
+ end
102
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RobotsController < ApplicationController
4
+ def index
5
+ @robots = HTM::Models::Robot.order(created_at: :desc)
6
+ end
7
+
8
+ def show
9
+ @robot = HTM::Models::Robot.find(params[:id])
10
+ # Default scope already excludes deleted nodes, so no .active needed
11
+ @memory_count = @robot.nodes.count
12
+ @recent_memories = @robot.nodes.order(created_at: :desc).limit(10)
13
+ end
14
+
15
+ def new
16
+ end
17
+
18
+ def create
19
+ name = params[:name]&.strip
20
+
21
+ if name.blank?
22
+ flash[:alert] = 'Robot name is required'
23
+ redirect_to new_robot_path
24
+ return
25
+ end
26
+
27
+ if HTM::Models::Robot.exists?(name: name)
28
+ flash[:alert] = 'A robot with that name already exists'
29
+ redirect_to new_robot_path
30
+ return
31
+ end
32
+
33
+ robot = HTM::Models::Robot.create!(name: name, metadata: {})
34
+ flash[:notice] = "Robot '#{name}' created successfully"
35
+ redirect_to robot_path(robot)
36
+ end
37
+
38
+ def switch
39
+ robot = HTM::Models::Robot.find(params[:id])
40
+ self.current_robot_name = robot.name
41
+ flash[:notice] = "Switched to robot '#{robot.name}'"
42
+ redirect_to root_path
43
+ end
44
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SearchController < ApplicationController
4
+ def index
5
+ @query = params[:query]
6
+ @strategies = %i[vector fulltext hybrid]
7
+ @selected_strategy = (params[:strategy] || 'hybrid').to_sym
8
+ @limit = (params[:limit] || 10).to_i
9
+ @timeframe = params[:timeframe].presence || 'all time'
10
+
11
+ @results = {}
12
+ @errors = {}
13
+
14
+ return unless @query.present?
15
+
16
+ # Convert "all time" to nil for HTM (no timeframe filter)
17
+ timeframe_param = @timeframe == 'all time' ? nil : @timeframe
18
+
19
+ if params[:compare] == 'true'
20
+ # Compare all strategies
21
+ @strategies.each do |strategy|
22
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
23
+ begin
24
+ @results[strategy] = htm.recall(@query, limit: @limit, strategy: strategy, timeframe: timeframe_param, raw: true)
25
+ rescue StandardError => e
26
+ @results[strategy] = []
27
+ @errors[strategy] = e.message
28
+ end
29
+ end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
30
+ @results["#{strategy}_time".to_sym] = ((end_time - start_time) * 1000).round(2)
31
+ end
32
+ else
33
+ # Single strategy search
34
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
35
+ begin
36
+ @results[@selected_strategy] = htm.recall(@query, limit: @limit, strategy: @selected_strategy, timeframe: timeframe_param, raw: true)
37
+ rescue StandardError => e
38
+ @results[@selected_strategy] = []
39
+ @errors[@selected_strategy] = e.message
40
+ flash.now[:alert] = "Search error: #{e.message}"
41
+ end
42
+ end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
43
+ @results["#{@selected_strategy}_time".to_sym] = ((end_time - start_time) * 1000).round(2)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TagsController < ApplicationController
4
+ def index
5
+ @view_type = params[:view] || 'list'
6
+
7
+ case @view_type
8
+ when 'tree'
9
+ @tree_string = HTM::Models::Tag.all.tree_string
10
+ @tree_svg = HTM::Models::Tag.all.tree_svg(title: 'HTM Tag Hierarchy')
11
+ when 'mermaid'
12
+ @tree_mermaid = HTM::Models::Tag.all.tree_mermaid
13
+ else
14
+ @tags = HTM::Models::Tag
15
+ .left_joins(:nodes)
16
+ .group('tags.id')
17
+ .select('tags.*, COUNT(nodes.id) as node_count')
18
+ .order(:name)
19
+
20
+ if params[:prefix].present?
21
+ @tags = @tags.where('tags.name LIKE ?', "#{params[:prefix]}%")
22
+ end
23
+ end
24
+ end
25
+
26
+ def show
27
+ @tag = HTM::Models::Tag.find(params[:id])
28
+ @memories = @tag.nodes.active.includes(:tags).order(created_at: :desc)
29
+ end
30
+ end
@@ -0,0 +1,4 @@
1
+ // Configure your import map in config/importmap.rb
2
+
3
+ import "@hotwired/turbo-rails"
4
+ import "controllers"
@@ -0,0 +1,9 @@
1
+ import { Application } from "@hotwired/stimulus"
2
+
3
+ const application = Application.start()
4
+
5
+ // Configure Stimulus development experience
6
+ application.debug = false
7
+ window.Stimulus = application
8
+
9
+ export { application }
@@ -0,0 +1,6 @@
1
+ // Import and register all your controllers from the importmap via controllers/**/*_controller
2
+ import { application } from "controllers/application"
3
+
4
+ // Eager load all controllers defined in the import map under controllers/**/*_controller
5
+ import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
6
+ eagerLoadControllersFrom("controllers", application)
@@ -0,0 +1,123 @@
1
+ <div class="space-y-8">
2
+ <!-- Header -->
3
+ <div>
4
+ <h1 class="text-2xl font-bold text-white">HTM Memory Explorer</h1>
5
+ <p class="mt-1 text-sm text-gray-400">Monitor and explore your Hierarchical Temporal Memory system</p>
6
+ </div>
7
+
8
+ <!-- Stats Grid -->
9
+ <div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
10
+ <%= render 'shared/stat_card', title: 'Total Memories', value: @stats[:total_nodes], icon: 'memory', color: 'indigo' %>
11
+ <%= render 'shared/stat_card', title: 'With Embeddings', value: @stats[:nodes_with_embeddings], icon: 'vector', color: 'green' %>
12
+ <%= render 'shared/stat_card', title: 'Deleted', value: @stats[:deleted_nodes], icon: 'trash', color: 'red' %>
13
+ <%= render 'shared/stat_card', title: 'Tags', value: @stats[:total_tags], icon: 'tag', color: 'yellow' %>
14
+ <%= render 'shared/stat_card', title: 'Robots', value: @stats[:total_robots], icon: 'robot', color: 'purple' %>
15
+ <%= render 'shared/stat_card', title: 'File Sources', value: @stats[:total_file_sources], icon: 'file', color: 'blue' %>
16
+ </div>
17
+
18
+ <!-- Two Column Layout -->
19
+ <div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
20
+ <!-- Recent Memories -->
21
+ <div class="rounded-lg bg-gray-800 border border-gray-700 p-6">
22
+ <div class="flex items-center justify-between mb-4">
23
+ <h2 class="text-lg font-medium text-white">Recent Memories</h2>
24
+ <%= link_to 'View All', memories_path, class: 'text-sm text-indigo-400 hover:text-indigo-300' %>
25
+ </div>
26
+ <% if @recent_memories.any? %>
27
+ <ul class="space-y-3">
28
+ <% @recent_memories.each do |memory| %>
29
+ <li class="rounded-md bg-gray-700/50 p-3">
30
+ <p class="text-sm text-gray-300 line-clamp-2"><%= truncate(memory.content, length: 120) %></p>
31
+ <div class="mt-2 flex items-center gap-2 text-xs text-gray-500">
32
+ <span><%= time_ago_in_words(memory.created_at) %> ago</span>
33
+ <% if memory.embedding.present? %>
34
+ <span class="inline-flex items-center rounded-full bg-green-900/50 px-2 py-0.5 text-xs text-green-400">embedded</span>
35
+ <% end %>
36
+ </div>
37
+ </li>
38
+ <% end %>
39
+ </ul>
40
+ <% else %>
41
+ <p class="text-sm text-gray-500">No memories stored yet. <%= link_to 'Add one', new_memory_path, class: 'text-indigo-400 hover:text-indigo-300' %>.</p>
42
+ <% end %>
43
+ </div>
44
+
45
+ <!-- Top Tags -->
46
+ <div class="rounded-lg bg-gray-800 border border-gray-700 p-6">
47
+ <div class="flex items-center justify-between mb-4">
48
+ <h2 class="text-lg font-medium text-white">Top Tags</h2>
49
+ <%= link_to 'View All', tags_path, class: 'text-sm text-indigo-400 hover:text-indigo-300' %>
50
+ </div>
51
+ <% if @top_tags.any? %>
52
+ <div class="flex flex-wrap gap-2">
53
+ <% @top_tags.each do |tag| %>
54
+ <%= link_to tag_path(tag), class: 'group' do %>
55
+ <span class="inline-flex items-center gap-1 rounded-full bg-indigo-900/50 border border-indigo-500/30 px-3 py-1 text-sm text-indigo-300 hover:bg-indigo-800/50 hover:border-indigo-500/50 transition-colors">
56
+ <%= tag.name %>
57
+ <span class="text-xs text-indigo-400/70">(<%= tag.node_count %>)</span>
58
+ </span>
59
+ <% end %>
60
+ <% end %>
61
+ </div>
62
+ <% else %>
63
+ <p class="text-sm text-gray-500">No tags created yet.</p>
64
+ <% end %>
65
+ </div>
66
+ </div>
67
+
68
+ <!-- Robots Section -->
69
+ <div class="rounded-lg bg-gray-800 border border-gray-700 p-6">
70
+ <div class="flex items-center justify-between mb-4">
71
+ <h2 class="text-lg font-medium text-white">Active Robots</h2>
72
+ <%= link_to 'View All', robots_path, class: 'text-sm text-indigo-400 hover:text-indigo-300' %>
73
+ </div>
74
+ <% if @robots.any? %>
75
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
76
+ <% @robots.each do |robot| %>
77
+ <%= link_to robot_path(robot), class: 'block rounded-md bg-gray-700/50 p-4 hover:bg-gray-700 transition-colors' do %>
78
+ <div class="flex items-center gap-3">
79
+ <div class="flex-shrink-0">
80
+ <div class="h-10 w-10 rounded-full bg-purple-900/50 flex items-center justify-center">
81
+ <svg class="h-5 w-5 text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
82
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
83
+ </svg>
84
+ </div>
85
+ </div>
86
+ <div>
87
+ <p class="text-sm font-medium text-white"><%= robot.name %></p>
88
+ <p class="text-xs text-gray-500">Created <%= time_ago_in_words(robot.created_at) %> ago</p>
89
+ </div>
90
+ </div>
91
+ <% end %>
92
+ <% end %>
93
+ </div>
94
+ <% else %>
95
+ <p class="text-sm text-gray-500">No robots registered yet.</p>
96
+ <% end %>
97
+ </div>
98
+
99
+ <!-- Quick Actions -->
100
+ <div class="rounded-lg bg-gray-800 border border-gray-700 p-6">
101
+ <h2 class="text-lg font-medium text-white mb-4">Quick Actions</h2>
102
+ <div class="flex flex-wrap gap-3">
103
+ <%= link_to new_memory_path, class: 'inline-flex items-center gap-2 rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-500 transition-colors' do %>
104
+ <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
105
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
106
+ </svg>
107
+ Add Memory
108
+ <% end %>
109
+ <%= link_to search_path, class: 'inline-flex items-center gap-2 rounded-md bg-gray-700 px-4 py-2 text-sm font-medium text-white hover:bg-gray-600 transition-colors' do %>
110
+ <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
111
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
112
+ </svg>
113
+ Search Memories
114
+ <% end %>
115
+ <%= link_to tags_path(view: 'tree'), class: 'inline-flex items-center gap-2 rounded-md bg-gray-700 px-4 py-2 text-sm font-medium text-white hover:bg-gray-600 transition-colors' do %>
116
+ <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
117
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7" />
118
+ </svg>
119
+ View Tag Tree
120
+ <% end %>
121
+ </div>
122
+ </div>
123
+ </div>