htm 0.0.31 → 0.0.32

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 (157) hide show
  1. checksums.yaml +4 -4
  2. data/.irbrc +2 -3
  3. data/.rubocop.yml +184 -0
  4. data/CHANGELOG.md +46 -0
  5. data/README.md +2 -0
  6. data/Rakefile +93 -12
  7. data/db/migrate/00008_create_node_relationships.rb +54 -0
  8. data/db/migrate/00009_fix_node_relationships_column_types.rb +17 -0
  9. data/db/schema.sql +124 -1
  10. data/docs/api/database.md +35 -57
  11. data/docs/api/embedding-service.md +1 -1
  12. data/docs/api/index.md +26 -15
  13. data/docs/api/working-memory.md +8 -8
  14. data/docs/architecture/index.md +5 -7
  15. data/docs/architecture/overview.md +5 -8
  16. data/docs/assets/images/htm-architecture-overview.svg +1 -1
  17. data/docs/assets/images/htm-context-assembly-flow.svg +2 -2
  18. data/docs/assets/images/htm-layered-architecture.svg +3 -3
  19. data/docs/assets/images/two-tier-memory-architecture.svg +1 -1
  20. data/docs/database/README.md +1 -0
  21. data/docs/database_rake_tasks.md +20 -28
  22. data/docs/development/contributing.md +5 -5
  23. data/docs/development/index.md +4 -7
  24. data/docs/development/schema.md +71 -1
  25. data/docs/development/setup.md +40 -82
  26. data/docs/development/testing.md +1 -1
  27. data/docs/examples/file-loading.md +4 -4
  28. data/docs/examples/mcp-client.md +1 -1
  29. data/docs/getting-started/quick-start.md +4 -4
  30. data/docs/guides/adding-memories.md +14 -1
  31. data/docs/guides/configuration.md +5 -5
  32. data/docs/guides/context-assembly.md +4 -4
  33. data/docs/guides/file-loading.md +12 -12
  34. data/docs/guides/getting-started.md +2 -2
  35. data/docs/guides/long-term-memory.md +7 -27
  36. data/docs/guides/propositions.md +20 -19
  37. data/docs/guides/recalling-memories.md +5 -5
  38. data/docs/guides/tags.md +18 -13
  39. data/docs/multi_framework_support.md +1 -1
  40. data/docs/robots/hive-mind.md +1 -1
  41. data/docs/robots/multi-robot.md +2 -2
  42. data/docs/robots/robot-groups.md +1 -1
  43. data/docs/robots/two-tier-memory.md +72 -94
  44. data/docs/setup_local_database.md +8 -54
  45. data/docs/using_rake_tasks_in_your_app.md +6 -6
  46. data/examples/01_basic_usage.rb +1 -0
  47. data/examples/03_custom_llm_configuration.rb +1 -0
  48. data/examples/04_file_loader_usage.rb +1 -0
  49. data/examples/05_timeframe_demo.rb +1 -0
  50. data/examples/06_example_app/app.rb +1 -0
  51. data/examples/07_cli_app/htm_cli.rb +1 -0
  52. data/examples/09_mcp_client.rb +1 -0
  53. data/examples/10_telemetry/demo.rb +1 -0
  54. data/examples/11_robot_groups/multi_process.rb +1 -0
  55. data/examples/11_robot_groups/same_process.rb +1 -0
  56. data/examples/12_rails_app/.envrc +12 -0
  57. data/examples/12_rails_app/Gemfile +8 -3
  58. data/examples/12_rails_app/Gemfile.lock +94 -89
  59. data/examples/12_rails_app/README.md +70 -19
  60. data/examples/12_rails_app/app/controllers/application_controller.rb +6 -0
  61. data/examples/12_rails_app/app/controllers/chats_controller.rb +305 -0
  62. data/examples/12_rails_app/app/controllers/dashboard_controller.rb +3 -0
  63. data/examples/12_rails_app/app/controllers/files_controller.rb +17 -2
  64. data/examples/12_rails_app/app/controllers/home_controller.rb +8 -0
  65. data/examples/12_rails_app/app/controllers/memories_controller.rb +9 -4
  66. data/examples/12_rails_app/app/controllers/messages_controller.rb +214 -0
  67. data/examples/12_rails_app/app/controllers/robots_controller.rb +11 -1
  68. data/examples/12_rails_app/app/controllers/tags_controller.rb +14 -1
  69. data/examples/12_rails_app/app/javascript/application.js +1 -1
  70. data/examples/12_rails_app/app/models/application_record.rb +5 -0
  71. data/examples/12_rails_app/app/models/chat.rb +36 -0
  72. data/examples/12_rails_app/app/models/message.rb +5 -0
  73. data/examples/12_rails_app/app/models/model.rb +5 -0
  74. data/examples/12_rails_app/app/models/tool_call.rb +5 -0
  75. data/examples/12_rails_app/app/views/chats/index.html.erb +61 -0
  76. data/examples/12_rails_app/app/views/chats/show.html.erb +213 -0
  77. data/examples/12_rails_app/app/views/dashboard/index.html.erb +3 -0
  78. data/examples/12_rails_app/app/views/files/index.html.erb +10 -5
  79. data/examples/12_rails_app/app/views/files/new.html.erb +4 -2
  80. data/examples/12_rails_app/app/views/files/show.html.erb +19 -3
  81. data/examples/12_rails_app/app/views/home/index.html.erb +45 -0
  82. data/examples/12_rails_app/app/views/layouts/application.html.erb +20 -18
  83. data/examples/12_rails_app/app/views/memories/_memory_card.html.erb +1 -1
  84. data/examples/12_rails_app/app/views/memories/deleted.html.erb +3 -1
  85. data/examples/12_rails_app/app/views/memories/edit.html.erb +2 -0
  86. data/examples/12_rails_app/app/views/memories/index.html.erb +2 -0
  87. data/examples/12_rails_app/app/views/memories/new.html.erb +2 -0
  88. data/examples/12_rails_app/app/views/memories/show.html.erb +4 -2
  89. data/examples/12_rails_app/app/views/messages/_message.html.erb +20 -0
  90. data/examples/12_rails_app/app/views/robots/index.html.erb +2 -0
  91. data/examples/12_rails_app/app/views/robots/new.html.erb +2 -0
  92. data/examples/12_rails_app/app/views/robots/show.html.erb +2 -0
  93. data/examples/12_rails_app/app/views/search/index.html.erb +59 -8
  94. data/examples/12_rails_app/app/views/shared/_navbar.html.erb +75 -29
  95. data/examples/12_rails_app/app/views/tags/index.html.erb +2 -0
  96. data/examples/12_rails_app/app/views/tags/show.html.erb +3 -1
  97. data/examples/12_rails_app/config/application.rb +1 -1
  98. data/examples/12_rails_app/config/database.yml +9 -5
  99. data/examples/12_rails_app/config/importmap.rb +1 -1
  100. data/examples/12_rails_app/config/initializers/htm.rb +9 -2
  101. data/examples/12_rails_app/config/initializers/ruby_llm.rb +33 -0
  102. data/examples/12_rails_app/config/routes.rb +39 -23
  103. data/examples/12_rails_app/db/migrate/20250124000001_create_ruby_llm_tables.rb +34 -0
  104. data/examples/12_rails_app/db/migrate/20250124000002_create_models_table.rb +28 -0
  105. data/examples/12_rails_app/db/schema.rb +67 -0
  106. data/examples/examples_helper.rb +25 -0
  107. data/lib/htm/circuit_breaker.rb +5 -6
  108. data/lib/htm/config/builder.rb +12 -12
  109. data/lib/htm/config/database.rb +21 -27
  110. data/lib/htm/config/validator.rb +12 -18
  111. data/lib/htm/config.rb +76 -65
  112. data/lib/htm/database.rb +193 -199
  113. data/lib/htm/embedding_service.rb +4 -9
  114. data/lib/htm/integrations/sinatra.rb +7 -7
  115. data/lib/htm/job_adapter.rb +14 -21
  116. data/lib/htm/jobs/generate_embedding_job.rb +28 -44
  117. data/lib/htm/jobs/generate_propositions_job.rb +29 -55
  118. data/lib/htm/jobs/generate_relationships_job.rb +137 -0
  119. data/lib/htm/jobs/generate_tags_job.rb +45 -67
  120. data/lib/htm/loaders/markdown_loader.rb +65 -112
  121. data/lib/htm/long_term_memory/fulltext_search.rb +1 -1
  122. data/lib/htm/long_term_memory/hybrid_search.rb +300 -128
  123. data/lib/htm/long_term_memory/node_operations.rb +2 -2
  124. data/lib/htm/long_term_memory/relevance_scorer.rb +100 -68
  125. data/lib/htm/long_term_memory/tag_operations.rb +87 -120
  126. data/lib/htm/long_term_memory/vector_search.rb +1 -1
  127. data/lib/htm/long_term_memory.rb +2 -1
  128. data/lib/htm/mcp/cli.rb +59 -58
  129. data/lib/htm/mcp/server.rb +5 -6
  130. data/lib/htm/mcp/tools.rb +30 -36
  131. data/lib/htm/migration.rb +10 -10
  132. data/lib/htm/models/node.rb +2 -3
  133. data/lib/htm/models/node_relationship.rb +72 -0
  134. data/lib/htm/models/node_tag.rb +2 -2
  135. data/lib/htm/models/robot_node.rb +2 -2
  136. data/lib/htm/models/tag.rb +41 -28
  137. data/lib/htm/observability.rb +45 -51
  138. data/lib/htm/proposition_service.rb +3 -7
  139. data/lib/htm/query_cache.rb +13 -15
  140. data/lib/htm/railtie.rb +1 -2
  141. data/lib/htm/robot_group.rb +9 -9
  142. data/lib/htm/sequel_config.rb +1 -0
  143. data/lib/htm/sql_builder.rb +1 -1
  144. data/lib/htm/tag_service.rb +2 -6
  145. data/lib/htm/timeframe.rb +4 -5
  146. data/lib/htm/timeframe_extractor.rb +42 -83
  147. data/lib/htm/version.rb +1 -1
  148. data/lib/htm/workflows/remember_workflow.rb +112 -115
  149. data/lib/htm/working_memory.rb +21 -26
  150. data/lib/htm.rb +103 -116
  151. data/lib/tasks/db.rake +0 -2
  152. data/lib/tasks/doc.rake +14 -13
  153. data/lib/tasks/files.rake +5 -12
  154. data/lib/tasks/htm.rake +70 -71
  155. data/lib/tasks/jobs.rake +41 -47
  156. data/lib/tasks/tags.rake +3 -8
  157. metadata +25 -100
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Chat < ApplicationRecord
4
+ acts_as_chat
5
+
6
+ # Override to_llm to pass assume_model_exists for cloud providers
7
+ # This allows using models that may not be in RubyLLM's local registry
8
+ def to_llm
9
+ model_record = model_association
10
+ api_provider = map_provider_for_api(model_record.provider)
11
+
12
+ Rails.logger.info "Chat#to_llm: provider=#{model_record.provider} -> #{api_provider}, model=#{model_record.model_id}"
13
+
14
+ # Create fresh chat object each request to respect current provider config
15
+ @chat = (context || RubyLLM).chat(
16
+ model: model_record.model_id,
17
+ provider: api_provider,
18
+ assume_model_exists: true
19
+ )
20
+
21
+ @chat.reset_messages!
22
+ messages_association.each { |msg| @chat.add_message(msg.to_llm) }
23
+ setup_persistence_callbacks
24
+ end
25
+
26
+ private
27
+
28
+ # Map display providers to RubyLLM API providers
29
+ # LM Studio uses OpenAI-compatible API
30
+ def map_provider_for_api(provider)
31
+ case provider.to_s
32
+ when 'lmstudio' then :openai
33
+ else provider.to_sym
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Message < ApplicationRecord
4
+ acts_as_message
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Model < ApplicationRecord
4
+ acts_as_model
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ToolCall < ApplicationRecord
4
+ acts_as_tool_call
5
+ end
@@ -0,0 +1,61 @@
1
+ <% content_for :body_attributes do %>data-no-auto-refresh<% end %>
2
+
3
+ <div class="space-y-6">
4
+ <!-- Header -->
5
+ <div class="flex items-center justify-between">
6
+ <div>
7
+ <h1 class="text-2xl font-bold text-white">Chats</h1>
8
+ <p class="mt-1 text-sm text-gray-400">AI-powered conversations with RAG context from HTM</p>
9
+ </div>
10
+ <%= button_to chats_path, method: :post, 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 cursor-pointer' do %>
11
+ <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
12
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
13
+ </svg>
14
+ New Chat
15
+ <% end %>
16
+ </div>
17
+
18
+ <% if @chats.any? %>
19
+ <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
20
+ <% @chats.each do |chat| %>
21
+ <%= link_to chat_path(chat), class: 'block rounded-lg bg-gray-800 border border-gray-700 p-4 hover:border-indigo-500 transition-colors group' do %>
22
+ <div class="flex items-center gap-3 mb-3">
23
+ <div class="rounded-full bg-indigo-900/50 p-2">
24
+ <svg class="h-5 w-5 text-indigo-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
25
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
26
+ </svg>
27
+ </div>
28
+ <div class="flex-1 min-w-0">
29
+ <p class="text-sm font-medium text-white group-hover:text-indigo-400 transition-colors">
30
+ Chat #<%= chat.id %>
31
+ </p>
32
+ <p class="text-xs text-gray-500">
33
+ <%= chat.messages.count %> messages
34
+ </p>
35
+ </div>
36
+ </div>
37
+ <% if (last_message = chat.messages.last) %>
38
+ <p class="text-sm text-gray-400 line-clamp-2"><%= truncate(last_message.content, length: 100) %></p>
39
+ <% else %>
40
+ <p class="text-sm text-gray-500 italic">No messages yet</p>
41
+ <% end %>
42
+ <p class="mt-2 text-xs text-gray-500"><%= time_ago_in_words(chat.updated_at) %> ago</p>
43
+ <% end %>
44
+ <% end %>
45
+ </div>
46
+ <% else %>
47
+ <div class="rounded-lg bg-gray-800 border border-gray-700 p-12 text-center">
48
+ <svg class="mx-auto h-12 w-12 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
49
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
50
+ </svg>
51
+ <h3 class="mt-4 text-lg font-medium text-white">No chats yet</h3>
52
+ <p class="mt-2 text-sm text-gray-400">Start a new conversation to begin chatting with AI using HTM context.</p>
53
+ <%= button_to chats_path, method: :post, class: 'mt-4 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 cursor-pointer' do %>
54
+ <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
55
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
56
+ </svg>
57
+ Start New Chat
58
+ <% end %>
59
+ </div>
60
+ <% end %>
61
+ </div>
@@ -0,0 +1,213 @@
1
+ <% content_for :body_attributes do %>data-no-auto-refresh<% end %>
2
+
3
+ <div class="flex flex-col h-[calc(100vh-10rem)]">
4
+ <!-- Header -->
5
+ <div class="flex items-center justify-between pb-4 border-b border-gray-700">
6
+ <div class="flex items-center gap-4">
7
+ <%= link_to app_root_path, class: 'text-gray-400 hover:text-white transition-colors' do %>
8
+ <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
9
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
10
+ </svg>
11
+ <% end %>
12
+ <div>
13
+ <h1 class="text-lg font-bold text-white">Chat #<%= @chat.id %></h1>
14
+ <p class="text-xs text-gray-500"><%= @messages.count %> messages</p>
15
+ </div>
16
+ </div>
17
+ <%= button_to chat_path(@chat), method: :delete, form: { data: { turbo_confirm: 'Delete this chat?' } }, class: 'text-gray-400 hover:text-red-400 transition-colors cursor-pointer' do %>
18
+ <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
19
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
20
+ </svg>
21
+ <% end %>
22
+ </div>
23
+
24
+ <!-- Messages -->
25
+ <div id="messages-container" class="flex-1 overflow-y-auto py-4 space-y-4">
26
+ <% if @messages.any? %>
27
+ <% @messages.each do |message| %>
28
+ <%= render 'messages/message', message: message %>
29
+ <% end %>
30
+ <% else %>
31
+ <div class="flex items-center justify-center h-full">
32
+ <div class="text-center">
33
+ <svg class="mx-auto h-12 w-12 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
34
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
35
+ </svg>
36
+ <p class="mt-4 text-gray-400">Start the conversation</p>
37
+ <p class="mt-1 text-sm text-gray-500">Your messages will be enhanced with HTM context</p>
38
+ </div>
39
+ </div>
40
+ <% end %>
41
+ </div>
42
+
43
+ <!-- Input -->
44
+ <div class="pt-4 border-t border-gray-700">
45
+ <%= form_with url: chat_messages_path(@chat), method: :post, id: 'message-form', class: 'flex gap-3' do |f| %>
46
+ <div class="flex-1">
47
+ <%= f.text_area :content,
48
+ rows: 1,
49
+ placeholder: 'Type your message...',
50
+ class: 'block w-full rounded-lg border-gray-600 bg-gray-800 text-white placeholder-gray-400 focus:border-indigo-500 focus:ring-indigo-500 resize-none px-4 py-3',
51
+ autofocus: true,
52
+ id: 'message-input' %>
53
+ </div>
54
+ <button type="submit" id="send-button" class="inline-flex items-center gap-2 rounded-lg bg-indigo-600 px-4 py-3 text-sm font-medium text-white hover:bg-indigo-500 transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed">
55
+ <span id="send-text">Send</span>
56
+ <svg id="send-spinner" class="hidden animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
57
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
58
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
59
+ </svg>
60
+ </button>
61
+ <% end %>
62
+ <div class="mt-2 flex items-center justify-center gap-3 text-xs text-gray-500">
63
+ <% current_provider = @chat.model&.provider || 'ollama' %>
64
+ <% current_model_name = @chat.model&.model_id %>
65
+
66
+ <!-- Provider dropdown -->
67
+ <div class="flex items-center gap-1">
68
+ <span>Provider:</span>
69
+ <%= form_with url: chat_path(@chat), method: :patch, id: 'provider-form', class: 'inline-flex items-center' do |f| %>
70
+ <%= f.select :provider,
71
+ options_for_select(@available_providers.map { |p| [p[:name], p[:id]] }, current_provider),
72
+ {},
73
+ id: 'provider-select',
74
+ class: 'rounded border-gray-600 bg-gray-800 text-gray-300 text-xs py-1 px-2 focus:border-indigo-500 focus:ring-indigo-500',
75
+ onchange: 'this.form.submit()' %>
76
+ <% end %>
77
+ </div>
78
+
79
+ <span class="text-gray-600">|</span>
80
+
81
+ <!-- Model dropdown -->
82
+ <div class="flex items-center gap-1">
83
+ <span>Model:</span>
84
+ <%= form_with url: chat_path(@chat), method: :patch, id: 'model-form', class: 'inline-flex items-center' do |f| %>
85
+ <%= f.select :model_id,
86
+ options_for_select(@available_models, current_model_name),
87
+ {},
88
+ id: 'model-select',
89
+ class: 'rounded border-gray-600 bg-gray-800 text-gray-300 text-xs py-1 px-2 focus:border-indigo-500 focus:ring-indigo-500',
90
+ onchange: 'this.form.submit()' %>
91
+ <% end %>
92
+ </div>
93
+ </div>
94
+
95
+ <!-- HTM Context Enhancement Controls -->
96
+ <div class="mt-3 pt-3 border-t border-gray-700">
97
+ <%= form_with url: update_htm_settings_chat_path(@chat), method: :patch, id: 'htm-settings-form', class: 'flex flex-col gap-2' do |f| %>
98
+ <div class="flex items-center justify-center gap-4 text-xs text-gray-500">
99
+ <span class="text-gray-400 font-medium">HTM:</span>
100
+
101
+ <!-- Strategy radio buttons -->
102
+ <div class="flex items-center gap-3">
103
+ <% strategies = [['Off', 'off'], ['Vector', 'vector'], ['Fulltext', 'fulltext'], ['Hybrid', 'hybrid']] %>
104
+ <% strategies.each do |label, value| %>
105
+ <label class="flex items-center gap-1 cursor-pointer">
106
+ <%= radio_button_tag :htm_strategy, value,
107
+ @htm_settings[:strategy] == value,
108
+ class: 'text-indigo-500 focus:ring-indigo-500 bg-gray-800 border-gray-600',
109
+ onchange: 'this.form.submit()' %>
110
+ <span class="<%= @htm_settings[:strategy] == value ? 'text-indigo-400' : 'text-gray-400' %>"><%= label %></span>
111
+ </label>
112
+ <% end %>
113
+ </div>
114
+
115
+ <span class="text-gray-600">|</span>
116
+
117
+ <!-- Limit dropdown -->
118
+ <div class="flex items-center gap-1">
119
+ <span>Limit:</span>
120
+ <%= select_tag :htm_limit,
121
+ options_for_select([[3, 3], [5, 5], [10, 10], [15, 15], [20, 20], [30, 30], [40, 40], [50, 50], [75, 75], [100, 100], ['All', 'all']], @htm_settings[:limit]),
122
+ class: 'rounded border-gray-600 bg-gray-800 text-gray-300 text-xs py-1 px-2 focus:border-indigo-500 focus:ring-indigo-500',
123
+ onchange: 'this.form.submit()',
124
+ disabled: @htm_settings[:strategy] == 'off' %>
125
+ </div>
126
+ </div>
127
+
128
+ <!-- Additional options -->
129
+ <div class="flex items-center justify-center gap-4 text-xs text-gray-500">
130
+ <label class="flex items-center gap-1 cursor-pointer <%= @htm_settings[:strategy] == 'off' ? 'opacity-50' : '' %>">
131
+ <%= check_box_tag :htm_propositions, '1',
132
+ @htm_settings[:propositions],
133
+ class: 'rounded text-indigo-500 focus:ring-indigo-500 bg-gray-800 border-gray-600',
134
+ onchange: 'this.form.submit()',
135
+ disabled: @htm_settings[:strategy] == 'off' %>
136
+ <span>Include propositions</span>
137
+ </label>
138
+
139
+ <label class="flex items-center gap-1 cursor-pointer <%= @htm_settings[:strategy] == 'off' ? 'opacity-50' : '' %>">
140
+ <%= check_box_tag :htm_tags_only, '1',
141
+ @htm_settings[:tags_only],
142
+ class: 'rounded text-indigo-500 focus:ring-indigo-500 bg-gray-800 border-gray-600',
143
+ onchange: 'this.form.submit()',
144
+ disabled: @htm_settings[:strategy] == 'off' %>
145
+ <span>Tags only</span>
146
+ </label>
147
+
148
+ <!-- Tag filter input -->
149
+ <div class="flex items-center gap-1 <%= @htm_settings[:strategy] == 'off' ? 'opacity-50' : '' %>">
150
+ <span>Tag filter:</span>
151
+ <%= text_field_tag :htm_tag_filter,
152
+ @htm_settings[:tag_filter],
153
+ placeholder: 'e.g. database:',
154
+ class: 'rounded border-gray-600 bg-gray-800 text-gray-300 text-xs py-1 px-2 w-24 focus:border-indigo-500 focus:ring-indigo-500 placeholder-gray-600',
155
+ disabled: @htm_settings[:strategy] == 'off',
156
+ onblur: 'this.form.submit()' %>
157
+ </div>
158
+ </div>
159
+ <% end %>
160
+ </div>
161
+ </div>
162
+ </div>
163
+
164
+ <script>
165
+ // Auto-scroll messages container to bottom
166
+ function scrollToBottom() {
167
+ const container = document.getElementById('messages-container');
168
+ if (container) {
169
+ container.scrollTop = container.scrollHeight;
170
+ }
171
+ }
172
+
173
+ // Convert UTC timestamps to local timezone
174
+ function convertTimestampsToLocal() {
175
+ document.querySelectorAll('time.local-time').forEach(el => {
176
+ const utcTime = new Date(el.getAttribute('datetime'));
177
+ const hours = utcTime.getHours().toString().padStart(2, '0');
178
+ const minutes = utcTime.getMinutes().toString().padStart(2, '0');
179
+ el.textContent = `${hours}:${minutes}`;
180
+ });
181
+ }
182
+
183
+ // Initialize on page load (same pattern as search page spinner)
184
+ document.addEventListener('DOMContentLoaded', () => {
185
+ scrollToBottom();
186
+ convertTimestampsToLocal();
187
+
188
+ const form = document.getElementById('message-form');
189
+ const btn = document.getElementById('send-button');
190
+ const spinner = document.getElementById('send-spinner');
191
+ const btnText = document.getElementById('send-text');
192
+ const input = document.getElementById('message-input');
193
+
194
+ if (form && btn) {
195
+ form.addEventListener('submit', (e) => {
196
+ // Validate: prevent submission if input is empty
197
+ if (!input || !input.value.trim()) {
198
+ e.preventDefault();
199
+ input && input.focus();
200
+ return;
201
+ }
202
+
203
+ // Show spinner and disable button (same pattern as search page)
204
+ spinner.classList.remove('hidden');
205
+ btnText.textContent = 'Sending...';
206
+ btn.disabled = true;
207
+ btn.classList.add('opacity-75', 'cursor-wait');
208
+ btn.classList.remove('hover:bg-indigo-500');
209
+ // Form submits normally - spinner shows until page reloads with response
210
+ });
211
+ }
212
+ });
213
+ </script>
@@ -3,6 +3,9 @@
3
3
  <div>
4
4
  <h1 class="text-2xl font-bold text-white">HTM Memory Explorer</h1>
5
5
  <p class="mt-1 text-sm text-gray-400">Monitor and explore your Hierarchical Temporal Memory system</p>
6
+ <p class="mt-2 text-xs text-gray-500 font-mono bg-gray-800 rounded px-2 py-1 inline-block">
7
+ <span class="text-gray-400">HTM Database:</span> <%= @htm_database_url %>
8
+ </p>
6
9
  </div>
7
10
 
8
11
  <!-- Stats Grid -->
@@ -1,3 +1,5 @@
1
+ <% content_for :body_attributes do %>data-no-auto-refresh<% end %>
2
+
1
3
  <div class="space-y-6">
2
4
  <!-- Header -->
3
5
  <div class="flex items-center justify-between">
@@ -33,7 +35,10 @@
33
35
  </div>
34
36
  <div class="ml-3">
35
37
  <p class="text-sm text-blue-300">
36
- <strong>File Loading:</strong> HTM can load markdown files into memory with automatic chunking. Each chunk becomes a separate memory node with its own embedding and tags. Changes are tracked for re-sync.
38
+ <strong>File Loading:</strong> HTM can load markdown files into memory with automatic chunking. Each chunk becomes a separate memory node with its own embedding and tags.
39
+ </p>
40
+ <p class="text-sm text-blue-300/70 mt-2">
41
+ <strong>Sync:</strong> Re-reads the source file and performs a differential update. Unchanged content is preserved (keeping embeddings and tags); new content is added; removed content is soft-deleted (recoverable).
37
42
  </p>
38
43
  </div>
39
44
  </div>
@@ -56,7 +61,7 @@
56
61
  <% @file_sources.each do |source| %>
57
62
  <tr class="hover:bg-gray-700/50 transition-colors">
58
63
  <td class="px-6 py-4">
59
- <%= link_to file_path(source), class: 'text-sm text-indigo-400 hover:text-indigo-300' do %>
64
+ <%= link_to file_path(source.id), class: 'text-sm text-indigo-400 hover:text-indigo-300' do %>
60
65
  <span class="font-mono"><%= source.file_path %></span>
61
66
  <% end %>
62
67
  <% if source.frontmatter.present? && source.frontmatter['title'] %>
@@ -83,11 +88,11 @@
83
88
  <%= time_ago_in_words(source.updated_at) %> ago
84
89
  </td>
85
90
  <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
86
- <%= link_to 'View', file_path(source), class: 'text-indigo-400 hover:text-indigo-300' %>
91
+ <%= link_to 'View', file_path(source.id), class: 'text-indigo-400 hover:text-indigo-300' %>
87
92
  <span class="mx-2 text-gray-600">|</span>
88
- <%= button_to 'Sync', sync_file_path(source), method: :post, class: 'text-indigo-400 hover:text-indigo-300 bg-transparent border-0 cursor-pointer' %>
93
+ <%= button_to 'Sync', sync_file_path(source.id), method: :post, class: 'text-indigo-400 hover:text-indigo-300 bg-transparent border-0 cursor-pointer' %>
89
94
  <span class="mx-2 text-gray-600">|</span>
90
- <%= button_to 'Unload', file_path(source), method: :delete, class: 'text-red-400 hover:text-red-300 bg-transparent border-0 cursor-pointer', data: { turbo_confirm: 'Unload this file? All chunks will be soft-deleted.' } %>
95
+ <%= button_to 'Unload', file_path(source.id), method: :delete, data: { confirm: 'Unload this file? All chunks will be soft-deleted.' }, class: 'text-red-400 hover:text-red-300 bg-transparent border-0 cursor-pointer' %>
91
96
  </td>
92
97
  </tr>
93
98
  <% end %>
@@ -1,3 +1,5 @@
1
+ <% content_for :body_attributes do %>data-no-auto-refresh<% end %>
2
+
1
3
  <!-- Full-page loading overlay -->
2
4
  <div id="upload-overlay" class="hidden fixed inset-0 bg-gray-900/90 z-50 flex items-center justify-center">
3
5
  <div class="bg-gray-800 rounded-lg border border-gray-700 p-8 max-w-md w-full mx-4 text-center">
@@ -30,7 +32,7 @@
30
32
  <div class="rounded-lg bg-gray-800 border border-gray-700 p-6">
31
33
  <h2 class="text-lg font-bold text-white mb-4">Upload Files</h2>
32
34
 
33
- <%= form_with url: upload_files_path, method: :post, multipart: true, class: 'space-y-4', id: 'upload-files-form', data: { turbo: false } do %>
35
+ <%= form_with url: upload_files_path, method: :post, multipart: true, class: 'space-y-4', id: 'upload-files-form' do %>
34
36
  <div>
35
37
  <label for="files" class="block text-sm font-medium text-gray-300 mb-2">Select Files</label>
36
38
  <input
@@ -69,7 +71,7 @@
69
71
  <p class="text-xs text-yellow-400"><strong>Note:</strong> Browser uploads limited to ~100 files due to OS restrictions. For larger directories, use <strong>Load Directory by Path</strong> below.</p>
70
72
  </div>
71
73
 
72
- <%= form_with url: upload_directory_files_path, method: :post, multipart: true, class: 'space-y-4', id: 'upload-dir-form', data: { turbo: false } do %>
74
+ <%= form_with url: upload_directory_files_path, method: :post, multipart: true, class: 'space-y-4', id: 'upload-dir-form' do %>
73
75
  <div>
74
76
  <label for="directory" class="block text-sm font-medium text-gray-300 mb-2">Select Directory</label>
75
77
  <input
@@ -1,3 +1,6 @@
1
+ <%# Disable auto-refresh on file detail page %>
2
+ <% content_for :body_attributes do %>data-no-auto-refresh<% end %>
3
+
1
4
  <div class="space-y-6">
2
5
  <!-- Back link -->
3
6
  <div>
@@ -41,14 +44,14 @@
41
44
  </span>
42
45
  <% end %>
43
46
 
44
- <%= button_to sync_file_path(@file_source), method: :post, class: 'inline-flex items-center gap-2 rounded-md bg-gray-700 px-3 py-2 text-sm font-medium text-white hover:bg-gray-600 transition-colors' do %>
47
+ <%= button_to sync_file_path(@file_source.id), method: :post, class: 'inline-flex items-center gap-2 rounded-md bg-gray-700 px-3 py-2 text-sm font-medium text-white hover:bg-gray-600 transition-colors' do %>
45
48
  <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
46
49
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
47
50
  </svg>
48
51
  Sync Now
49
52
  <% end %>
50
53
 
51
- <%= button_to file_path(@file_source), method: :delete, class: 'inline-flex items-center gap-2 rounded-md bg-red-600/80 px-3 py-2 text-sm font-medium text-white hover:bg-red-600 transition-colors', data: { turbo_confirm: 'Unload this file? All chunks will be soft-deleted.' } do %>
54
+ <%= button_to file_path(@file_source.id), method: :delete, data: { confirm: 'Unload this file? All chunks will be soft-deleted.' }, class: 'inline-flex items-center gap-2 rounded-md bg-red-600/80 px-3 py-2 text-sm font-medium text-white hover:bg-red-600 transition-colors' do %>
52
55
  <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
53
56
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
54
57
  </svg>
@@ -57,6 +60,19 @@
57
60
  </div>
58
61
  </div>
59
62
 
63
+ <!-- Sync Info -->
64
+ <div class="mt-6 pt-6 border-t border-gray-700">
65
+ <div class="rounded-md bg-gray-700/50 p-3">
66
+ <p class="text-xs text-gray-400">
67
+ <strong class="text-gray-300">What does Sync do?</strong>
68
+ Re-reads the source file and performs a differential update. Unchanged chunks are preserved (keeping their embeddings and tags); new content creates new chunks; removed content is soft-deleted (recoverable).
69
+ <% if @file_source.needs_sync? %>
70
+ <span class="text-yellow-400 ml-1">The source file has changed since last sync.</span>
71
+ <% end %>
72
+ </p>
73
+ </div>
74
+ </div>
75
+
60
76
  <!-- Stats -->
61
77
  <div class="mt-6 grid grid-cols-3 gap-4 pt-6 border-t border-gray-700">
62
78
  <div>
@@ -100,7 +116,7 @@
100
116
  <%= i + 1 %>
101
117
  </span>
102
118
  <div class="flex-1 min-w-0">
103
- <%= link_to memory_path(chunk), class: 'block group' do %>
119
+ <%= link_to memory_path(chunk.id), class: 'block group' do %>
104
120
  <p class="text-sm text-gray-300 group-hover:text-white transition-colors whitespace-pre-wrap line-clamp-4">
105
121
  <%= chunk.content %>
106
122
  </p>
@@ -0,0 +1,45 @@
1
+ <% content_for :body_attributes do %>data-no-auto-refresh<% end %>
2
+
3
+ <div class="min-h-[calc(100vh-8rem)] flex items-center justify-center">
4
+ <div class="max-w-4xl mx-auto px-4">
5
+ <div class="text-center mb-12">
6
+ <div class="flex justify-center mb-4">
7
+ <svg class="h-16 w-16 text-indigo-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
8
+ <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
9
+ </svg>
10
+ </div>
11
+ <h1 class="text-4xl font-bold text-white mb-4">HTM Demo</h1>
12
+ <p class="text-xl text-gray-400">Hierarchical Temporal Memory + RAG Chatbot</p>
13
+ </div>
14
+
15
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
16
+ <!-- Chat Section -->
17
+ <%= link_to app_root_path, class: 'block rounded-xl bg-gray-800 border border-gray-700 p-8 hover:border-indigo-500 transition-colors group' do %>
18
+ <div class="flex items-center gap-4 mb-4">
19
+ <div class="rounded-full bg-indigo-900/50 p-3">
20
+ <svg class="h-8 w-8 text-indigo-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
21
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
22
+ </svg>
23
+ </div>
24
+ <h2 class="text-2xl font-bold text-white group-hover:text-indigo-400 transition-colors">Chat</h2>
25
+ </div>
26
+ <p class="text-gray-400 mb-4">AI-powered chatbot with RAG. Answers are augmented with context from HTM memory.</p>
27
+ <p class="text-sm text-gray-500"><%= pluralize(@chat_count, 'conversation') %></p>
28
+ <% end %>
29
+
30
+ <!-- HTM Management Section -->
31
+ <%= link_to htm_root_path, class: 'block rounded-xl bg-gray-800 border border-gray-700 p-8 hover:border-purple-500 transition-colors group' do %>
32
+ <div class="flex items-center gap-4 mb-4">
33
+ <div class="rounded-full bg-purple-900/50 p-3">
34
+ <svg class="h-8 w-8 text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
35
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4" />
36
+ </svg>
37
+ </div>
38
+ <h2 class="text-2xl font-bold text-white group-hover:text-purple-400 transition-colors">HTM Management</h2>
39
+ </div>
40
+ <p class="text-gray-400 mb-4">Manage memories, tags, robots, and files in the HTM knowledge base.</p>
41
+ <p class="text-sm text-gray-500"><%= pluralize(@memory_count, 'memory') %></p>
42
+ <% end %>
43
+ </div>
44
+ </div>
45
+ </div>
@@ -3,9 +3,6 @@
3
3
  <head>
4
4
  <title>HTM Memory Explorer</title>
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
- <meta name="turbo-cache-control" content="no-preview">
7
- <meta name="turbo-refresh-method" content="morph">
8
- <meta name="turbo-refresh-scroll" content="preserve">
9
6
  <%= csrf_meta_tags %>
10
7
  <%= csp_meta_tag %>
11
8
 
@@ -40,28 +37,33 @@
40
37
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
41
38
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
42
39
 
43
- <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
40
+ <%= stylesheet_link_tag "application" %>
44
41
 
45
- <%# Hotwire via CDN - no build step needed %>
46
- <script type="module">
47
- import * as Turbo from 'https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.12/+esm'
48
-
49
- // Live reload: auto-refresh every 3 seconds using Turbo morphing
50
- // This preserves scroll position and smoothly updates only changed elements
42
+ <%# Auto-refresh without Turbo Drive - CDN Turbo doesn't integrate well with Rails %>
43
+ <script>
44
+ // Simple auto-refresh using standard page reload
45
+ // Turbo Drive via CDN causes navigation issues, so we use plain JS instead
46
+ // Auto-refresh can be disabled by adding data-no-auto-refresh to the body
51
47
  let autoRefreshEnabled = true;
52
48
  let formFieldFocused = false;
53
49
  let refreshInterval = 3000;
54
50
 
55
51
  function autoRefresh() {
52
+ // Don't refresh if disabled globally or via data attribute
53
+ if (!autoRefreshEnabled) return;
54
+ if (document.body.hasAttribute('data-no-auto-refresh')) return;
56
55
  // Don't refresh if form field is focused (would interrupt typing)
57
- if (autoRefreshEnabled && document.visibilityState === 'visible' && !formFieldFocused) {
58
- Turbo.visit(window.location.href, { action: 'replace' });
59
- }
56
+ if (document.visibilityState !== 'visible' || formFieldFocused) return;
57
+
58
+ window.location.reload();
60
59
  }
61
60
 
62
61
  // Start auto-refresh after page loads
63
- document.addEventListener('turbo:load', () => {
64
- setInterval(autoRefresh, refreshInterval);
62
+ document.addEventListener('DOMContentLoaded', () => {
63
+ // Only start auto-refresh if not disabled
64
+ if (!document.body.hasAttribute('data-no-auto-refresh')) {
65
+ setInterval(autoRefresh, refreshInterval);
66
+ }
65
67
 
66
68
  // Pause refresh when any form field is focused
67
69
  document.addEventListener('focusin', (e) => {
@@ -79,8 +81,8 @@
79
81
 
80
82
  // Pause refresh when page is hidden
81
83
  document.addEventListener('visibilitychange', () => {
82
- // Refresh immediately when tab becomes visible again
83
- if (document.visibilityState === 'visible') {
84
+ // Refresh immediately when tab becomes visible again (if enabled)
85
+ if (document.visibilityState === 'visible' && !document.body.hasAttribute('data-no-auto-refresh')) {
84
86
  autoRefresh();
85
87
  }
86
88
  });
@@ -99,7 +101,7 @@
99
101
  </style>
100
102
  </head>
101
103
 
102
- <body class="h-full bg-gray-900 text-gray-100">
104
+ <body class="h-full bg-gray-900 text-gray-100" <%= yield :body_attributes %>>
103
105
  <div class="min-h-full">
104
106
  <%= render 'shared/navbar' %>
105
107
 
@@ -41,7 +41,7 @@
41
41
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
42
42
  </svg>
43
43
  <% end %>
44
- <%= button_to memory_path(memory.id), method: :delete, class: 'rounded p-1.5 text-gray-400 hover:text-red-400 hover:bg-gray-700 transition-colors', title: 'Delete', data: { turbo_confirm: 'Move this memory to trash?' } do %>
44
+ <%= button_to memory_path(memory.id), method: :delete, data: { confirm: 'Move this memory to trash?' }, class: 'rounded p-1.5 text-gray-400 hover:text-red-400 hover:bg-gray-700 transition-colors', title: 'Delete' do %>
45
45
  <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
46
46
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
47
47
  </svg>
@@ -1,3 +1,5 @@
1
+ <% content_for :body_attributes do %>data-no-auto-refresh<% end %>
2
+
1
3
  <div class="space-y-6">
2
4
  <!-- Header -->
3
5
  <div class="flex items-center justify-between">
@@ -39,7 +41,7 @@
39
41
  </svg>
40
42
  Restore
41
43
  <% end %>
42
- <%= button_to memory_path(memory.id, permanent: true), method: :delete, class: 'inline-flex items-center gap-1 rounded-md bg-red-600/80 px-3 py-1.5 text-xs font-medium text-white hover:bg-red-600 transition-colors', data: { turbo_confirm: 'Permanently delete? This cannot be undone!' } do %>
44
+ <%= button_to memory_path(memory.id, permanent: true), method: :delete, data: { confirm: 'Permanently delete? This cannot be undone!' }, class: 'inline-flex items-center gap-1 rounded-md bg-red-600/80 px-3 py-1.5 text-xs font-medium text-white hover:bg-red-600 transition-colors' do %>
43
45
  <svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
44
46
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
45
47
  </svg>
@@ -1,3 +1,5 @@
1
+ <% content_for :body_attributes do %>data-no-auto-refresh<% end %>
2
+
1
3
  <div class="space-y-6">
2
4
  <!-- Back link -->
3
5
  <div>
@@ -1,3 +1,5 @@
1
+ <% content_for :body_attributes do %>data-no-auto-refresh<% end %>
2
+
1
3
  <div class="space-y-6">
2
4
  <!-- Header -->
3
5
  <div class="flex items-center justify-between">
@@ -1,3 +1,5 @@
1
+ <% content_for :body_attributes do %>data-no-auto-refresh<% end %>
2
+
1
3
  <div class="space-y-6">
2
4
  <!-- Back link -->
3
5
  <div>