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.
- checksums.yaml +4 -4
- data/.envrc +1 -0
- data/CHANGELOG.md +67 -0
- data/README.md +97 -1592
- data/bin/htm_mcp +31 -0
- data/config/database.yml +7 -4
- data/docs/getting-started/installation.md +31 -11
- data/docs/guides/mcp-server.md +456 -21
- data/docs/multi_framework_support.md +2 -2
- data/examples/mcp_client.rb +2 -2
- data/examples/rails_app/.gitignore +2 -0
- data/examples/rails_app/Gemfile +22 -0
- data/examples/rails_app/Gemfile.lock +438 -0
- data/examples/rails_app/Procfile.dev +1 -0
- data/examples/rails_app/README.md +98 -0
- data/examples/rails_app/Rakefile +5 -0
- data/examples/rails_app/app/assets/stylesheets/application.css +83 -0
- data/examples/rails_app/app/assets/stylesheets/inter-font.css +6 -0
- data/examples/rails_app/app/controllers/application_controller.rb +19 -0
- data/examples/rails_app/app/controllers/dashboard_controller.rb +27 -0
- data/examples/rails_app/app/controllers/files_controller.rb +205 -0
- data/examples/rails_app/app/controllers/memories_controller.rb +102 -0
- data/examples/rails_app/app/controllers/robots_controller.rb +44 -0
- data/examples/rails_app/app/controllers/search_controller.rb +46 -0
- data/examples/rails_app/app/controllers/tags_controller.rb +30 -0
- data/examples/rails_app/app/javascript/application.js +4 -0
- data/examples/rails_app/app/javascript/controllers/application.js +9 -0
- data/examples/rails_app/app/javascript/controllers/index.js +6 -0
- data/examples/rails_app/app/views/dashboard/index.html.erb +123 -0
- data/examples/rails_app/app/views/files/index.html.erb +108 -0
- data/examples/rails_app/app/views/files/new.html.erb +321 -0
- data/examples/rails_app/app/views/files/show.html.erb +130 -0
- data/examples/rails_app/app/views/layouts/application.html.erb +124 -0
- data/examples/rails_app/app/views/memories/_memory_card.html.erb +51 -0
- data/examples/rails_app/app/views/memories/deleted.html.erb +62 -0
- data/examples/rails_app/app/views/memories/edit.html.erb +35 -0
- data/examples/rails_app/app/views/memories/index.html.erb +81 -0
- data/examples/rails_app/app/views/memories/new.html.erb +71 -0
- data/examples/rails_app/app/views/memories/show.html.erb +126 -0
- data/examples/rails_app/app/views/robots/index.html.erb +106 -0
- data/examples/rails_app/app/views/robots/new.html.erb +36 -0
- data/examples/rails_app/app/views/robots/show.html.erb +79 -0
- data/examples/rails_app/app/views/search/index.html.erb +184 -0
- data/examples/rails_app/app/views/shared/_navbar.html.erb +52 -0
- data/examples/rails_app/app/views/shared/_stat_card.html.erb +52 -0
- data/examples/rails_app/app/views/tags/index.html.erb +131 -0
- data/examples/rails_app/app/views/tags/show.html.erb +67 -0
- data/examples/rails_app/bin/dev +8 -0
- data/examples/rails_app/bin/rails +4 -0
- data/examples/rails_app/bin/rake +4 -0
- data/examples/rails_app/config/application.rb +33 -0
- data/examples/rails_app/config/boot.rb +5 -0
- data/examples/rails_app/config/database.yml +15 -0
- data/examples/rails_app/config/environment.rb +5 -0
- data/examples/rails_app/config/importmap.rb +7 -0
- data/examples/rails_app/config/routes.rb +38 -0
- data/examples/rails_app/config/tailwind.config.js +35 -0
- data/examples/rails_app/config.ru +5 -0
- data/examples/rails_app/log/.keep +0 -0
- data/examples/rails_app/tmp/local_secret.txt +1 -0
- data/lib/htm/active_record_config.rb +2 -5
- data/lib/htm/configuration.rb +35 -2
- data/lib/htm/database.rb +3 -6
- data/lib/htm/mcp/cli.rb +333 -0
- data/lib/htm/mcp/group_tools.rb +476 -0
- data/lib/htm/mcp/resources.rb +89 -0
- data/lib/htm/mcp/server.rb +98 -0
- data/lib/htm/mcp/tools.rb +488 -0
- data/lib/htm/models/file_source.rb +5 -3
- data/lib/htm/railtie.rb +0 -4
- data/lib/htm/tasks.rb +7 -4
- data/lib/htm/version.rb +1 -1
- data/lib/tasks/htm.rake +6 -9
- metadata +59 -4
- data/bin/htm_mcp.rb +0 -621
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<div class="space-y-6">
|
|
2
|
+
<!-- Header -->
|
|
3
|
+
<div class="flex items-center justify-between">
|
|
4
|
+
<div>
|
|
5
|
+
<h1 class="text-2xl font-bold text-white">File Sources</h1>
|
|
6
|
+
<p class="mt-1 text-sm text-gray-400">Manage files loaded into HTM memory</p>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="flex gap-3">
|
|
9
|
+
<% if @file_sources.any? %>
|
|
10
|
+
<%= button_to sync_all_files_path, method: :post, 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 %>
|
|
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="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" />
|
|
13
|
+
</svg>
|
|
14
|
+
Sync All
|
|
15
|
+
<% end %>
|
|
16
|
+
<% end %>
|
|
17
|
+
<%= link_to new_file_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 %>
|
|
18
|
+
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
19
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
|
20
|
+
</svg>
|
|
21
|
+
Load File
|
|
22
|
+
<% end %>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<!-- Info Box -->
|
|
27
|
+
<div class="rounded-lg bg-blue-900/30 border border-blue-500/30 p-4">
|
|
28
|
+
<div class="flex">
|
|
29
|
+
<div class="flex-shrink-0">
|
|
30
|
+
<svg class="h-5 w-5 text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
31
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
32
|
+
</svg>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="ml-3">
|
|
35
|
+
<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.
|
|
37
|
+
</p>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<!-- File Sources List -->
|
|
43
|
+
<% if @file_sources.any? %>
|
|
44
|
+
<div class="rounded-lg bg-gray-800 border border-gray-700 overflow-hidden">
|
|
45
|
+
<table class="min-w-full divide-y divide-gray-700">
|
|
46
|
+
<thead class="bg-gray-800/50">
|
|
47
|
+
<tr>
|
|
48
|
+
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">File</th>
|
|
49
|
+
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Chunks</th>
|
|
50
|
+
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Status</th>
|
|
51
|
+
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Last Synced</th>
|
|
52
|
+
<th scope="col" class="relative px-6 py-3"><span class="sr-only">Actions</span></th>
|
|
53
|
+
</tr>
|
|
54
|
+
</thead>
|
|
55
|
+
<tbody class="divide-y divide-gray-700">
|
|
56
|
+
<% @file_sources.each do |source| %>
|
|
57
|
+
<tr class="hover:bg-gray-700/50 transition-colors">
|
|
58
|
+
<td class="px-6 py-4">
|
|
59
|
+
<%= link_to file_path(source), class: 'text-sm text-indigo-400 hover:text-indigo-300' do %>
|
|
60
|
+
<span class="font-mono"><%= source.file_path %></span>
|
|
61
|
+
<% end %>
|
|
62
|
+
<% if source.frontmatter.present? && source.frontmatter['title'] %>
|
|
63
|
+
<p class="text-xs text-gray-500 mt-1"><%= source.frontmatter['title'] %></p>
|
|
64
|
+
<% end %>
|
|
65
|
+
</td>
|
|
66
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-400">
|
|
67
|
+
<%= source.chunks.count %>
|
|
68
|
+
</td>
|
|
69
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
70
|
+
<% if source.needs_sync? %>
|
|
71
|
+
<span class="inline-flex items-center gap-1 rounded-full bg-yellow-900/50 border border-yellow-500/30 px-2 py-0.5 text-xs text-yellow-400">
|
|
72
|
+
<svg class="h-2 w-2" fill="currentColor" viewBox="0 0 8 8"><circle cx="4" cy="4" r="3"/></svg>
|
|
73
|
+
Needs Sync
|
|
74
|
+
</span>
|
|
75
|
+
<% else %>
|
|
76
|
+
<span class="inline-flex items-center gap-1 rounded-full bg-green-900/50 border border-green-500/30 px-2 py-0.5 text-xs text-green-400">
|
|
77
|
+
<svg class="h-2 w-2" fill="currentColor" viewBox="0 0 8 8"><circle cx="4" cy="4" r="3"/></svg>
|
|
78
|
+
Synced
|
|
79
|
+
</span>
|
|
80
|
+
<% end %>
|
|
81
|
+
</td>
|
|
82
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-400">
|
|
83
|
+
<%= time_ago_in_words(source.updated_at) %> ago
|
|
84
|
+
</td>
|
|
85
|
+
<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' %>
|
|
87
|
+
<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' %>
|
|
89
|
+
<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.' } %>
|
|
91
|
+
</td>
|
|
92
|
+
</tr>
|
|
93
|
+
<% end %>
|
|
94
|
+
</tbody>
|
|
95
|
+
</table>
|
|
96
|
+
</div>
|
|
97
|
+
<% else %>
|
|
98
|
+
<div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
|
|
99
|
+
<svg class="mx-auto h-12 w-12 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
100
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
101
|
+
</svg>
|
|
102
|
+
<h3 class="mt-4 text-lg font-medium text-white">No files loaded</h3>
|
|
103
|
+
<p class="mt-2 text-sm text-gray-400">
|
|
104
|
+
<%= link_to 'Load a file', new_file_path, class: 'text-indigo-400 hover:text-indigo-300' %> to get started.
|
|
105
|
+
</p>
|
|
106
|
+
</div>
|
|
107
|
+
<% end %>
|
|
108
|
+
</div>
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
<!-- Full-page loading overlay -->
|
|
2
|
+
<div id="upload-overlay" class="hidden fixed inset-0 bg-gray-900/90 z-50 flex items-center justify-center">
|
|
3
|
+
<div class="bg-gray-800 rounded-lg border border-gray-700 p-8 max-w-md w-full mx-4 text-center">
|
|
4
|
+
<svg class="animate-spin h-12 w-12 text-indigo-500 mx-auto mb-4" fill="none" viewBox="0 0 24 24">
|
|
5
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
6
|
+
<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>
|
|
7
|
+
</svg>
|
|
8
|
+
<h3 class="text-lg font-medium text-white mb-2" id="overlay-title">Uploading Files...</h3>
|
|
9
|
+
<p class="text-sm text-gray-400 mb-4" id="overlay-status">Preparing upload</p>
|
|
10
|
+
<div class="w-full bg-gray-700 rounded-full h-2 mb-2">
|
|
11
|
+
<div id="overlay-progress" class="bg-indigo-500 h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
|
|
12
|
+
</div>
|
|
13
|
+
<p class="text-xs text-gray-500" id="overlay-detail">This may take a moment while files are chunked and processed...</p>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div class="space-y-6">
|
|
18
|
+
<!-- Back link -->
|
|
19
|
+
<div>
|
|
20
|
+
<%= link_to files_path, class: 'inline-flex items-center gap-1 text-sm text-gray-400 hover:text-white transition-colors' do %>
|
|
21
|
+
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
22
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
|
23
|
+
</svg>
|
|
24
|
+
Back to Files
|
|
25
|
+
<% end %>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
29
|
+
<!-- Upload Files -->
|
|
30
|
+
<div class="rounded-lg bg-gray-800 border border-gray-700 p-6">
|
|
31
|
+
<h2 class="text-lg font-bold text-white mb-4">Upload Files</h2>
|
|
32
|
+
|
|
33
|
+
<%= form_with url: upload_files_path, method: :post, multipart: true, class: 'space-y-4', id: 'upload-files-form', data: { turbo: false } do %>
|
|
34
|
+
<div>
|
|
35
|
+
<label for="files" class="block text-sm font-medium text-gray-300 mb-2">Select Files</label>
|
|
36
|
+
<input
|
|
37
|
+
type="file"
|
|
38
|
+
name="files[]"
|
|
39
|
+
id="files"
|
|
40
|
+
multiple
|
|
41
|
+
accept=".md,.markdown,.txt"
|
|
42
|
+
class="w-full rounded-md bg-gray-700 border border-gray-600 text-white file:mr-4 file:py-2 file:px-4 file:rounded-l-md file:border-0 file:bg-indigo-600 file:text-white file:cursor-pointer hover:file:bg-indigo-500 focus:ring-indigo-500 focus:border-indigo-500"
|
|
43
|
+
/>
|
|
44
|
+
<p class="mt-1 text-xs text-gray-500">Select one or more markdown files (.md, .markdown, .txt)</p>
|
|
45
|
+
<div id="files-selected" class="mt-2 text-sm text-indigo-400 hidden"></div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="flex items-center gap-2">
|
|
49
|
+
<input type="checkbox" name="force" value="true" id="upload_force" class="rounded bg-gray-700 border-gray-600 text-indigo-600 focus:ring-indigo-500">
|
|
50
|
+
<label for="upload_force" class="text-sm text-gray-300">Force reload (re-sync even if unchanged)</label>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="pt-4 border-t border-gray-700">
|
|
54
|
+
<button type="submit" id="upload-files-btn" 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 cursor-pointer transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
|
55
|
+
<svg id="upload-files-spinner" class="hidden animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
|
|
56
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
57
|
+
<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>
|
|
58
|
+
</svg>
|
|
59
|
+
<span id="upload-files-text">Upload Files</span>
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
<% end %>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<!-- Upload Directory -->
|
|
66
|
+
<div class="rounded-lg bg-gray-800 border border-gray-700 p-6">
|
|
67
|
+
<h2 class="text-lg font-bold text-white mb-4">Upload Directory</h2>
|
|
68
|
+
|
|
69
|
+
<%= form_with url: upload_directory_files_path, method: :post, multipart: true, class: 'space-y-4', id: 'upload-dir-form', data: { turbo: false } do %>
|
|
70
|
+
<div>
|
|
71
|
+
<label for="directory" class="block text-sm font-medium text-gray-300 mb-2">Select Directory</label>
|
|
72
|
+
<input
|
|
73
|
+
type="file"
|
|
74
|
+
name="files[]"
|
|
75
|
+
id="directory"
|
|
76
|
+
webkitdirectory
|
|
77
|
+
directory
|
|
78
|
+
multiple
|
|
79
|
+
class="w-full rounded-md bg-gray-700 border border-gray-600 text-white file:mr-4 file:py-2 file:px-4 file:rounded-l-md file:border-0 file:bg-indigo-600 file:text-white file:cursor-pointer hover:file:bg-indigo-500 focus:ring-indigo-500 focus:border-indigo-500"
|
|
80
|
+
/>
|
|
81
|
+
<p class="mt-1 text-xs text-gray-500">Select a folder - all markdown files will be loaded</p>
|
|
82
|
+
<div id="dir-selected" class="mt-2 text-sm text-indigo-400 hidden"></div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div>
|
|
86
|
+
<label for="upload_pattern" class="block text-sm font-medium text-gray-300 mb-2">File Extension Filter</label>
|
|
87
|
+
<select name="extension" id="upload_pattern" class="w-full rounded-md bg-gray-700 border-gray-600 text-white text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
|
88
|
+
<option value=".md">Markdown (.md)</option>
|
|
89
|
+
<option value=".markdown">Markdown (.markdown)</option>
|
|
90
|
+
<option value=".txt">Text (.txt)</option>
|
|
91
|
+
<option value="">All files</option>
|
|
92
|
+
</select>
|
|
93
|
+
<p class="mt-1 text-xs text-gray-500">Filter files by extension</p>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div class="pt-4 border-t border-gray-700">
|
|
97
|
+
<button type="submit" id="upload-dir-btn" 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 cursor-pointer transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
|
98
|
+
<svg id="upload-dir-spinner" class="hidden animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
|
|
99
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
100
|
+
<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>
|
|
101
|
+
</svg>
|
|
102
|
+
<span id="upload-dir-text">Upload Directory</span>
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
<% end %>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<!-- Divider -->
|
|
110
|
+
<div class="relative">
|
|
111
|
+
<div class="absolute inset-0 flex items-center">
|
|
112
|
+
<div class="w-full border-t border-gray-700"></div>
|
|
113
|
+
</div>
|
|
114
|
+
<div class="relative flex justify-center text-sm">
|
|
115
|
+
<span class="px-2 bg-gray-900 text-gray-500">or load by path</span>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
120
|
+
<!-- Load Single File by Path -->
|
|
121
|
+
<div class="rounded-lg bg-gray-800 border border-gray-700 p-6">
|
|
122
|
+
<h2 class="text-lg font-bold text-white mb-4">Load File by Path</h2>
|
|
123
|
+
|
|
124
|
+
<%= form_with url: files_path, method: :post, class: 'space-y-4' do %>
|
|
125
|
+
<div>
|
|
126
|
+
<label for="path" class="block text-sm font-medium text-gray-300 mb-2">File Path</label>
|
|
127
|
+
<input
|
|
128
|
+
type="text"
|
|
129
|
+
name="path"
|
|
130
|
+
id="path"
|
|
131
|
+
placeholder="/path/to/document.md"
|
|
132
|
+
class="w-full rounded-md bg-gray-700 border-gray-600 text-white placeholder-gray-400 font-mono text-sm focus:ring-indigo-500 focus:border-indigo-500"
|
|
133
|
+
/>
|
|
134
|
+
<p class="mt-1 text-xs text-gray-500">Absolute path to a markdown file on the server</p>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div class="flex items-center gap-2">
|
|
138
|
+
<input type="checkbox" name="force" value="true" id="force" class="rounded bg-gray-700 border-gray-600 text-indigo-600 focus:ring-indigo-500">
|
|
139
|
+
<label for="force" class="text-sm text-gray-300">Force reload (re-sync even if unchanged)</label>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<div class="pt-4 border-t border-gray-700">
|
|
143
|
+
<%= submit_tag 'Load File', class: 'rounded-md bg-gray-600 px-4 py-2 text-sm font-medium text-white hover:bg-gray-500 cursor-pointer transition-colors' %>
|
|
144
|
+
</div>
|
|
145
|
+
<% end %>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<!-- Load Directory by Path -->
|
|
149
|
+
<div class="rounded-lg bg-gray-800 border border-gray-700 p-6">
|
|
150
|
+
<h2 class="text-lg font-bold text-white mb-4">Load Directory by Path</h2>
|
|
151
|
+
|
|
152
|
+
<%= form_with url: load_directory_files_path, method: :post, class: 'space-y-4' do %>
|
|
153
|
+
<div>
|
|
154
|
+
<label for="dir_path" class="block text-sm font-medium text-gray-300 mb-2">Directory Path</label>
|
|
155
|
+
<input
|
|
156
|
+
type="text"
|
|
157
|
+
name="path"
|
|
158
|
+
id="dir_path"
|
|
159
|
+
placeholder="/path/to/docs/"
|
|
160
|
+
class="w-full rounded-md bg-gray-700 border-gray-600 text-white placeholder-gray-400 font-mono text-sm focus:ring-indigo-500 focus:border-indigo-500"
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<div>
|
|
165
|
+
<label for="pattern" class="block text-sm font-medium text-gray-300 mb-2">Glob Pattern</label>
|
|
166
|
+
<input
|
|
167
|
+
type="text"
|
|
168
|
+
name="pattern"
|
|
169
|
+
id="pattern"
|
|
170
|
+
value="**/*.md"
|
|
171
|
+
placeholder="**/*.md"
|
|
172
|
+
class="w-full rounded-md bg-gray-700 border-gray-600 text-white placeholder-gray-400 font-mono text-sm focus:ring-indigo-500 focus:border-indigo-500"
|
|
173
|
+
/>
|
|
174
|
+
<p class="mt-1 text-xs text-gray-500">Pattern to match files (default: **/*.md)</p>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div class="pt-4 border-t border-gray-700">
|
|
178
|
+
<%= submit_tag 'Load Directory', class: 'rounded-md bg-gray-600 px-4 py-2 text-sm font-medium text-white hover:bg-gray-500 cursor-pointer transition-colors' %>
|
|
179
|
+
</div>
|
|
180
|
+
<% end %>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<!-- Info -->
|
|
185
|
+
<div class="rounded-lg bg-gray-800/50 border border-gray-700 p-4">
|
|
186
|
+
<h3 class="text-sm font-medium text-gray-300 mb-2">How File Loading Works</h3>
|
|
187
|
+
<ul class="text-xs text-gray-400 space-y-1 list-disc list-inside">
|
|
188
|
+
<li>Markdown files are split into chunks using the Baran gem's markdown-aware splitter</li>
|
|
189
|
+
<li>Each chunk becomes a separate memory node with its own embedding and tags</li>
|
|
190
|
+
<li>YAML frontmatter is extracted and stored as file metadata</li>
|
|
191
|
+
<li>Files are tracked by path with mtime-based change detection for efficient re-sync</li>
|
|
192
|
+
<li>Default chunk size: <%= HTM.configuration.chunk_size %> characters, overlap: <%= HTM.configuration.chunk_overlap %> characters</li>
|
|
193
|
+
</ul>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<script>
|
|
198
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
199
|
+
// Overlay elements
|
|
200
|
+
const overlay = document.getElementById('upload-overlay');
|
|
201
|
+
const overlayTitle = document.getElementById('overlay-title');
|
|
202
|
+
const overlayStatus = document.getElementById('overlay-status');
|
|
203
|
+
const overlayProgress = document.getElementById('overlay-progress');
|
|
204
|
+
const overlayDetail = document.getElementById('overlay-detail');
|
|
205
|
+
|
|
206
|
+
let fileCount = 0;
|
|
207
|
+
let progressInterval = null;
|
|
208
|
+
|
|
209
|
+
function showOverlay(title, count) {
|
|
210
|
+
fileCount = count;
|
|
211
|
+
overlayTitle.textContent = title;
|
|
212
|
+
overlayStatus.textContent = 'Uploading ' + count + ' file' + (count > 1 ? 's' : '') + '...';
|
|
213
|
+
overlayProgress.style.width = '0%';
|
|
214
|
+
overlayDetail.textContent = 'This may take a moment while files are chunked and processed...';
|
|
215
|
+
overlay.classList.remove('hidden');
|
|
216
|
+
|
|
217
|
+
// Simulate progress since we can't track actual server progress
|
|
218
|
+
let progress = 0;
|
|
219
|
+
const stages = [
|
|
220
|
+
{ pct: 15, status: 'Uploading files to server...' },
|
|
221
|
+
{ pct: 30, status: 'Reading file contents...' },
|
|
222
|
+
{ pct: 45, status: 'Splitting into chunks...' },
|
|
223
|
+
{ pct: 60, status: 'Creating memory nodes...' },
|
|
224
|
+
{ pct: 75, status: 'Generating embeddings...' },
|
|
225
|
+
{ pct: 85, status: 'Extracting tags...' },
|
|
226
|
+
{ pct: 95, status: 'Finalizing...' }
|
|
227
|
+
];
|
|
228
|
+
let stageIndex = 0;
|
|
229
|
+
|
|
230
|
+
progressInterval = setInterval(function() {
|
|
231
|
+
if (stageIndex < stages.length) {
|
|
232
|
+
const stage = stages[stageIndex];
|
|
233
|
+
if (progress < stage.pct) {
|
|
234
|
+
progress += 1;
|
|
235
|
+
overlayProgress.style.width = progress + '%';
|
|
236
|
+
} else {
|
|
237
|
+
overlayStatus.textContent = stage.status;
|
|
238
|
+
stageIndex++;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}, 200);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// File selection display for single files
|
|
245
|
+
const filesInput = document.getElementById('files');
|
|
246
|
+
const filesSelected = document.getElementById('files-selected');
|
|
247
|
+
|
|
248
|
+
if (filesInput) {
|
|
249
|
+
filesInput.addEventListener('change', function() {
|
|
250
|
+
if (this.files.length > 0) {
|
|
251
|
+
const names = Array.from(this.files).map(f => f.name);
|
|
252
|
+
if (names.length <= 3) {
|
|
253
|
+
filesSelected.textContent = 'Selected: ' + names.join(', ');
|
|
254
|
+
} else {
|
|
255
|
+
filesSelected.textContent = 'Selected: ' + names.slice(0, 3).join(', ') + ' and ' + (names.length - 3) + ' more';
|
|
256
|
+
}
|
|
257
|
+
filesSelected.classList.remove('hidden');
|
|
258
|
+
} else {
|
|
259
|
+
filesSelected.classList.add('hidden');
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// File selection display for directory
|
|
265
|
+
const dirInput = document.getElementById('directory');
|
|
266
|
+
const dirSelected = document.getElementById('dir-selected');
|
|
267
|
+
|
|
268
|
+
if (dirInput) {
|
|
269
|
+
dirInput.addEventListener('change', function() {
|
|
270
|
+
if (this.files.length > 0) {
|
|
271
|
+
const mdFiles = Array.from(this.files).filter(f => /\.(md|markdown|txt)$/i.test(f.name));
|
|
272
|
+
if (mdFiles.length > 0) {
|
|
273
|
+
dirSelected.textContent = 'Found ' + mdFiles.length + ' matching file(s) in selected folder';
|
|
274
|
+
} else {
|
|
275
|
+
dirSelected.textContent = 'No markdown files found in selected folder';
|
|
276
|
+
}
|
|
277
|
+
dirSelected.classList.remove('hidden');
|
|
278
|
+
} else {
|
|
279
|
+
dirSelected.classList.add('hidden');
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Upload files form submission
|
|
285
|
+
const uploadFilesForm = document.getElementById('upload-files-form');
|
|
286
|
+
const uploadFilesBtn = document.getElementById('upload-files-btn');
|
|
287
|
+
const uploadFilesSpinner = document.getElementById('upload-files-spinner');
|
|
288
|
+
const uploadFilesText = document.getElementById('upload-files-text');
|
|
289
|
+
|
|
290
|
+
if (uploadFilesForm) {
|
|
291
|
+
uploadFilesForm.addEventListener('submit', function() {
|
|
292
|
+
const files = filesInput.files;
|
|
293
|
+
if (files.length === 0) return;
|
|
294
|
+
|
|
295
|
+
uploadFilesBtn.disabled = true;
|
|
296
|
+
uploadFilesSpinner.classList.remove('hidden');
|
|
297
|
+
uploadFilesText.textContent = 'Processing...';
|
|
298
|
+
showOverlay('Uploading Files...', files.length);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Upload directory form submission
|
|
303
|
+
const uploadDirForm = document.getElementById('upload-dir-form');
|
|
304
|
+
const uploadDirBtn = document.getElementById('upload-dir-btn');
|
|
305
|
+
const uploadDirSpinner = document.getElementById('upload-dir-spinner');
|
|
306
|
+
const uploadDirText = document.getElementById('upload-dir-text');
|
|
307
|
+
|
|
308
|
+
if (uploadDirForm) {
|
|
309
|
+
uploadDirForm.addEventListener('submit', function() {
|
|
310
|
+
const files = dirInput.files;
|
|
311
|
+
const mdFiles = Array.from(files).filter(f => /\.(md|markdown|txt)$/i.test(f.name));
|
|
312
|
+
if (mdFiles.length === 0) return;
|
|
313
|
+
|
|
314
|
+
uploadDirBtn.disabled = true;
|
|
315
|
+
uploadDirSpinner.classList.remove('hidden');
|
|
316
|
+
uploadDirText.textContent = 'Processing...';
|
|
317
|
+
showOverlay('Uploading Directory...', mdFiles.length);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
</script>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<div class="space-y-6">
|
|
2
|
+
<!-- Back link -->
|
|
3
|
+
<div>
|
|
4
|
+
<%= link_to files_path, class: 'inline-flex items-center gap-1 text-sm text-gray-400 hover:text-white transition-colors' do %>
|
|
5
|
+
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
6
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
|
7
|
+
</svg>
|
|
8
|
+
Back to Files
|
|
9
|
+
<% end %>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<!-- File Header -->
|
|
13
|
+
<div class="rounded-lg bg-gray-800 border border-gray-700 p-6">
|
|
14
|
+
<div class="flex items-start justify-between">
|
|
15
|
+
<div>
|
|
16
|
+
<div class="flex items-center gap-3">
|
|
17
|
+
<div class="rounded-full bg-blue-900/50 border border-blue-500/30 p-2">
|
|
18
|
+
<svg class="h-5 w-5 text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
19
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
20
|
+
</svg>
|
|
21
|
+
</div>
|
|
22
|
+
<div>
|
|
23
|
+
<h1 class="text-xl font-bold text-white font-mono"><%= @file_source.file_path %></h1>
|
|
24
|
+
<% if @file_source.frontmatter.present? && @file_source.frontmatter['title'] %>
|
|
25
|
+
<p class="text-sm text-gray-400 mt-1"><%= @file_source.frontmatter['title'] %></p>
|
|
26
|
+
<% end %>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div class="flex items-center gap-3">
|
|
32
|
+
<% if @file_source.needs_sync? %>
|
|
33
|
+
<span class="inline-flex items-center gap-1 rounded-full bg-yellow-900/50 border border-yellow-500/30 px-3 py-1 text-sm text-yellow-400">
|
|
34
|
+
<svg class="h-2 w-2" fill="currentColor" viewBox="0 0 8 8"><circle cx="4" cy="4" r="3"/></svg>
|
|
35
|
+
Needs Sync
|
|
36
|
+
</span>
|
|
37
|
+
<% else %>
|
|
38
|
+
<span class="inline-flex items-center gap-1 rounded-full bg-green-900/50 border border-green-500/30 px-3 py-1 text-sm text-green-400">
|
|
39
|
+
<svg class="h-2 w-2" fill="currentColor" viewBox="0 0 8 8"><circle cx="4" cy="4" r="3"/></svg>
|
|
40
|
+
Synced
|
|
41
|
+
</span>
|
|
42
|
+
<% end %>
|
|
43
|
+
|
|
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 %>
|
|
45
|
+
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
46
|
+
<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
|
+
</svg>
|
|
48
|
+
Sync Now
|
|
49
|
+
<% end %>
|
|
50
|
+
|
|
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 %>
|
|
52
|
+
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
53
|
+
<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
|
+
</svg>
|
|
55
|
+
Unload
|
|
56
|
+
<% end %>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<!-- Stats -->
|
|
61
|
+
<div class="mt-6 grid grid-cols-3 gap-4 pt-6 border-t border-gray-700">
|
|
62
|
+
<div>
|
|
63
|
+
<p class="text-2xl font-bold text-white"><%= @chunks.count %></p>
|
|
64
|
+
<p class="text-sm text-gray-400">Chunks</p>
|
|
65
|
+
</div>
|
|
66
|
+
<div>
|
|
67
|
+
<p class="text-2xl font-bold text-white"><%= @file_source.mtime&.strftime('%Y-%m-%d %H:%M') || 'N/A' %></p>
|
|
68
|
+
<p class="text-sm text-gray-400">File Modified</p>
|
|
69
|
+
</div>
|
|
70
|
+
<div>
|
|
71
|
+
<p class="text-2xl font-bold text-white"><%= @file_source.updated_at.strftime('%Y-%m-%d %H:%M') %></p>
|
|
72
|
+
<p class="text-sm text-gray-400">Last Synced</p>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<!-- Frontmatter -->
|
|
77
|
+
<% if @file_source.frontmatter.present? && @file_source.frontmatter.any? %>
|
|
78
|
+
<div class="mt-6 pt-6 border-t border-gray-700">
|
|
79
|
+
<h3 class="text-sm font-medium text-gray-400 mb-2">Frontmatter</h3>
|
|
80
|
+
<div class="rounded-md bg-gray-900 p-3">
|
|
81
|
+
<pre class="text-xs text-gray-300 overflow-x-auto"><%= JSON.pretty_generate(@file_source.frontmatter) %></pre>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
<% end %>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<!-- Chunks -->
|
|
88
|
+
<div class="rounded-lg bg-gray-800 border border-gray-700 overflow-hidden">
|
|
89
|
+
<div class="px-6 py-4 bg-gray-800/50 border-b border-gray-700">
|
|
90
|
+
<h2 class="text-lg font-medium text-white">Chunks</h2>
|
|
91
|
+
<p class="text-sm text-gray-400">Each chunk is stored as a separate memory node</p>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<% if @chunks.any? %>
|
|
95
|
+
<ul class="divide-y divide-gray-700">
|
|
96
|
+
<% @chunks.each_with_index do |chunk, i| %>
|
|
97
|
+
<li class="p-4 hover:bg-gray-700/50 transition-colors">
|
|
98
|
+
<div class="flex items-start gap-4">
|
|
99
|
+
<span class="flex-shrink-0 w-8 h-8 rounded-full bg-gray-700 flex items-center justify-center text-sm text-gray-400 font-mono">
|
|
100
|
+
<%= i + 1 %>
|
|
101
|
+
</span>
|
|
102
|
+
<div class="flex-1 min-w-0">
|
|
103
|
+
<%= link_to memory_path(chunk), class: 'block group' do %>
|
|
104
|
+
<p class="text-sm text-gray-300 group-hover:text-white transition-colors whitespace-pre-wrap line-clamp-4">
|
|
105
|
+
<%= chunk.content %>
|
|
106
|
+
</p>
|
|
107
|
+
<% end %>
|
|
108
|
+
<div class="mt-2 flex items-center gap-4 text-xs text-gray-500">
|
|
109
|
+
<span>Node ID: <%= chunk.id %></span>
|
|
110
|
+
<% if chunk.metadata && chunk.metadata['cursor'] %>
|
|
111
|
+
<span>Position: <%= chunk.metadata['cursor'] %></span>
|
|
112
|
+
<% end %>
|
|
113
|
+
<% if chunk.embedding.present? %>
|
|
114
|
+
<span class="text-green-500">embedded</span>
|
|
115
|
+
<% else %>
|
|
116
|
+
<span class="text-yellow-500">pending</span>
|
|
117
|
+
<% end %>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</li>
|
|
122
|
+
<% end %>
|
|
123
|
+
</ul>
|
|
124
|
+
<% else %>
|
|
125
|
+
<div class="p-8 text-center">
|
|
126
|
+
<p class="text-sm text-gray-500">No chunks found.</p>
|
|
127
|
+
</div>
|
|
128
|
+
<% end %>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|