relay.app 0.1.0
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 +7 -0
- data/CHANGELOG.md +23 -0
- data/LICENSE +17 -0
- data/README.md +132 -0
- data/app/concerns/attachment.rb +12 -0
- data/app/concerns/context.rb +147 -0
- data/app/concerns/roda.rb +50 -0
- data/app/concerns/view.rb +90 -0
- data/app/forms/mcp/forgejo.rb +55 -0
- data/app/forms/mcp/github.rb +47 -0
- data/app/forms/mcp.rb +89 -0
- data/app/hooks/require_user.rb +10 -0
- data/app/init/database.rb +36 -0
- data/app/init/env.rb +21 -0
- data/app/init/router.rb +164 -0
- data/app/models/context.rb +82 -0
- data/app/models/mcp/preset.rb +60 -0
- data/app/models/mcp.rb +165 -0
- data/app/models/model_record.rb +70 -0
- data/app/models/song.rb +11 -0
- data/app/models/user.rb +31 -0
- data/app/pages/base.rb +25 -0
- data/app/pages/chat.rb +18 -0
- data/app/pages/mcp.rb +12 -0
- data/app/pages/sign_in.rb +14 -0
- data/app/prompts/system.md +129 -0
- data/app/resources/jukebox.yml +90 -0
- data/app/routes/base.rb +36 -0
- data/app/routes/clear_attachment.rb +13 -0
- data/app/routes/list_chat.rb +11 -0
- data/app/routes/list_contexts.rb +17 -0
- data/app/routes/list_controls.rb +11 -0
- data/app/routes/list_mcp.rb +16 -0
- data/app/routes/list_models.rb +14 -0
- data/app/routes/list_providers.rb +11 -0
- data/app/routes/list_tools.rb +13 -0
- data/app/routes/mcp/base.rb +16 -0
- data/app/routes/mcp/create.rb +19 -0
- data/app/routes/mcp/delete.rb +17 -0
- data/app/routes/mcp/form.rb +11 -0
- data/app/routes/mcp/new.rb +16 -0
- data/app/routes/mcp/show.rb +17 -0
- data/app/routes/mcp/toggle.rb +17 -0
- data/app/routes/mcp/update.rb +20 -0
- data/app/routes/settings/new_context.rb +23 -0
- data/app/routes/settings/set_context.rb +26 -0
- data/app/routes/settings/set_model.rb +23 -0
- data/app/routes/settings/set_provider.rb +38 -0
- data/app/routes/sign_in.rb +39 -0
- data/app/routes/upload_attachment.rb +35 -0
- data/app/routes/websocket/connection.rb +247 -0
- data/app/routes/websocket/interrupt.rb +25 -0
- data/app/routes/websocket/stream.rb +46 -0
- data/app/routes/websocket.rb +62 -0
- data/app/tools/add_song.rb +27 -0
- data/app/tools/juke_box.rb +41 -0
- data/app/tools/relay_knowledge.rb +59 -0
- data/app/tools/remove_song.rb +53 -0
- data/app/validators/mcp.rb +42 -0
- data/app/views/fragments/_append_message.erb +1 -0
- data/app/views/fragments/_chat.erb +15 -0
- data/app/views/fragments/_contexts.erb +7 -0
- data/app/views/fragments/_contexts_body.erb +35 -0
- data/app/views/fragments/_controls.erb +15 -0
- data/app/views/fragments/_iframe.erb +8 -0
- data/app/views/fragments/_input.erb +67 -0
- data/app/views/fragments/_mcp_settings.erb +52 -0
- data/app/views/fragments/_message.erb +31 -0
- data/app/views/fragments/_models.erb +25 -0
- data/app/views/fragments/_providers.erb +26 -0
- data/app/views/fragments/_remove_empty_state.erb +1 -0
- data/app/views/fragments/_replace_last_message.erb +1 -0
- data/app/views/fragments/_sidebar_menu.erb +11 -0
- data/app/views/fragments/_sidebar_status.erb +21 -0
- data/app/views/fragments/_status.erb +40 -0
- data/app/views/fragments/_stream.erb +26 -0
- data/app/views/fragments/_tools.erb +34 -0
- data/app/views/fragments/_tools_panel.erb +4 -0
- data/app/views/fragments/mcp/_editor.erb +54 -0
- data/app/views/fragments/mcp/_fields_forgejo.erb +16 -0
- data/app/views/fragments/mcp/_fields_github.erb +12 -0
- data/app/views/fragments/mcp/_list.erb +55 -0
- data/app/views/fragments/mcp/_workspace.erb +14 -0
- data/app/views/fragments/models/_loading.erb +4 -0
- data/app/views/fragments/settings/_chat.erb +1 -0
- data/app/views/fragments/settings/_input.erb +1 -0
- data/app/views/fragments/settings/_replace_contexts.erb +1 -0
- data/app/views/fragments/settings/_workspace.erb +4 -0
- data/app/views/layout.erb +19 -0
- data/app/views/pages/chat.erb +13 -0
- data/app/views/pages/mcps.erb +10 -0
- data/app/views/pages/sign_in.erb +45 -0
- data/app/views/partials/_sidebar.erb +24 -0
- data/bin/relay +38 -0
- data/config.ru +21 -0
- data/db/migrate/20260319131927_create_users.rb +12 -0
- data/db/migrate/20260327000000_create_contexts.rb +20 -0
- data/db/migrate/20260426130000_create_mcps.rb +19 -0
- data/db/migrate/20260426170000_create_model_infos.rb +20 -0
- data/db/migrate/20260503120000_create_songs.rb +17 -0
- data/db/migrate/20260503153000_drop_chat_from_model_infos.rb +8 -0
- data/db/migrate/20260503160000_rename_model_infos_to_model_records.rb +5 -0
- data/db/seeds.rb +13 -0
- data/lib/relay/attachment/session.rb +154 -0
- data/lib/relay/attachment.rb +55 -0
- data/lib/relay/cache/in_memory_cache.rb +60 -0
- data/lib/relay/cache.rb +5 -0
- data/lib/relay/jukebox.rb +96 -0
- data/lib/relay/markdown.rb +45 -0
- data/lib/relay/model.rb +12 -0
- data/lib/relay/reloader.rb +29 -0
- data/lib/relay/task.rb +66 -0
- data/lib/relay/task_monitor.rb +80 -0
- data/lib/relay/test.rb +11 -0
- data/lib/relay/theme.rb +5 -0
- data/lib/relay/tool.rb +12 -0
- data/lib/relay/version.rb +5 -0
- data/lib/relay.rb +183 -0
- data/libexec/relay/bootstrap +10 -0
- data/libexec/relay/configure +100 -0
- data/libexec/relay/migrate +7 -0
- data/libexec/relay/setup +10 -0
- data/libexec/relay/start +31 -0
- data/public/.gitkeep +0 -0
- data/public/images/relay.png +0 -0
- data/public/js/relay.js +68669 -0
- data/public/js/relay.js.map +1 -0
- data/public/stylesheets/application.css +2292 -0
- data/public/stylesheets/application.css.map +1 -0
- metadata +465 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<div
|
|
2
|
+
id="chat-panel"
|
|
3
|
+
class="workspace-chat"
|
|
4
|
+
hx-ext="ws"
|
|
5
|
+
<%= 'hx-swap-oob="outerHTML"' if swap_oob %>
|
|
6
|
+
ws-connect="/api/ws"
|
|
7
|
+
>
|
|
8
|
+
<section class="chat-surface">
|
|
9
|
+
<%== partial("fragments/stream", locals: {messages: messages || []}) %>
|
|
10
|
+
<div class="chat-footer space-y-4">
|
|
11
|
+
<%== partial("fragments/input", locals: {swap_oob:}) %>
|
|
12
|
+
</div>
|
|
13
|
+
<%== partial("fragments/status", locals: status_bar.merge(swap_oob:)) %>
|
|
14
|
+
</section>
|
|
15
|
+
</div>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<% if show_label %>
|
|
2
|
+
<div class="flex items-center justify-between gap-3">
|
|
3
|
+
<span class="label">Chats</span>
|
|
4
|
+
<span class="sidebar-meta"><%= contexts.size %></span>
|
|
5
|
+
</div>
|
|
6
|
+
<% end %>
|
|
7
|
+
<form action="/settings/new-context" method="post" class="pb-3" hx-post="/settings/new-context" hx-target="#workspace-main" hx-swap="outerHTML">
|
|
8
|
+
<button class="sidebar-create-button button-primary w-full px-4 py-2.5 text-sm font-semibold transition focus:outline-none focus:ring-4" style="--tw-ring-color: color-mix(in srgb, var(--theme-accent) 16%, transparent);" type="submit">New Chat</button>
|
|
9
|
+
</form>
|
|
10
|
+
<div class="sidebar-contexts-list scrollbar-chat min-h-0 flex-1 overflow-y-auto pr-1">
|
|
11
|
+
<% if contexts.empty? %>
|
|
12
|
+
<div class="field theme-muted text-sm">No saved chats for this AI yet.</div>
|
|
13
|
+
<% else %>
|
|
14
|
+
<form class="flex flex-col gap-2" action="/settings/set-context" method="post" hx-post="/settings/set-context" hx-target="#workspace-main" hx-swap="outerHTML" hx-trigger="change from:input[type='radio']">
|
|
15
|
+
<% contexts.each do |context| %>
|
|
16
|
+
<% active = context.id == ctx.id %>
|
|
17
|
+
<label class="sidebar-list-item <%= "is-active" if active %> flex cursor-pointer items-center gap-3 text-sm">
|
|
18
|
+
<input
|
|
19
|
+
type="radio"
|
|
20
|
+
name="context_id"
|
|
21
|
+
value="<%= context.id %>"
|
|
22
|
+
<%= "checked" if active %>
|
|
23
|
+
class="sr-only"
|
|
24
|
+
>
|
|
25
|
+
<span class="sidebar-avatar"><%= initials(context.title || "Untitled Chat") %></span>
|
|
26
|
+
<span class="flex min-w-0 flex-1 flex-col">
|
|
27
|
+
<span class="truncate font-medium"><%= context.title || "Untitled Context" %></span>
|
|
28
|
+
<span class="sidebar-meta"><%= context.messages.size %> messages</span>
|
|
29
|
+
</span>
|
|
30
|
+
<span class="sidebar-presence" aria-hidden="true"></span>
|
|
31
|
+
</label>
|
|
32
|
+
<% end %>
|
|
33
|
+
</form>
|
|
34
|
+
<% end %>
|
|
35
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<div id="sidebar-controls" class="theme-strong flex h-full min-h-0 w-full flex-col text-sm">
|
|
2
|
+
<section class="sidebar-group">
|
|
3
|
+
<span class="label">Provider</span>
|
|
4
|
+
<div class="sidebar-group-body mt-3 space-y-4">
|
|
5
|
+
<%== partial("fragments/providers", locals: {show_label: false}) %>
|
|
6
|
+
<%== partial("fragments/models", locals: {show_label: false}) %>
|
|
7
|
+
</div>
|
|
8
|
+
</section>
|
|
9
|
+
<section class="sidebar-group mt-[20px] min-h-0 flex-1">
|
|
10
|
+
<%== partial("fragments/contexts", locals: {contexts: contexts, show_label: true, swap_oob: false}) %>
|
|
11
|
+
</section>
|
|
12
|
+
<section class="sidebar-group mt-[20px]">
|
|
13
|
+
<%== partial("fragments/mcp_settings", locals: {servers: mcps, show_label: true, swap_oob: false}) %>
|
|
14
|
+
</section>
|
|
15
|
+
</div>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<iframe src="<%= entry["track"] %>"
|
|
2
|
+
title="<%= entry["title"] %>"
|
|
3
|
+
width="704"
|
|
4
|
+
height="396"
|
|
5
|
+
style="display:block;width:704px;max-width:100%;height:396px;margin:0.75rem auto 0;border:1px solid var(--theme-panel-border);border-radius:1rem;background:var(--theme-embed-bg);box-shadow:0 10px 24px rgba(0,0,0,0.16);" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"
|
|
6
|
+
allowfullscreen>
|
|
7
|
+
</iframe>
|
|
8
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<div
|
|
2
|
+
id="chat-input"
|
|
3
|
+
class="space-y-3"
|
|
4
|
+
<%= 'hx-swap-oob="outerHTML"' if swap_oob %>
|
|
5
|
+
>
|
|
6
|
+
<% file = attachment.file %>
|
|
7
|
+
<% if attachment.error %>
|
|
8
|
+
<div class="theme-strong text-sm"><%= attachment.error %></div>
|
|
9
|
+
<% end %>
|
|
10
|
+
<% if file.attached? %>
|
|
11
|
+
<div class="composer-attachment theme-strong flex items-center justify-between gap-4 text-sm">
|
|
12
|
+
<div class="space-y-1">
|
|
13
|
+
<div>Attached: <%= file.name %></div>
|
|
14
|
+
<div class="text-xs opacity-80">Will be sent with your next message.</div>
|
|
15
|
+
</div>
|
|
16
|
+
<form hx-post="/clear-attachment" hx-target="#chat-input" hx-swap="outerHTML">
|
|
17
|
+
<button class="composer-submit px-3 py-1 text-sm" type="submit">Remove</button>
|
|
18
|
+
</form>
|
|
19
|
+
</div>
|
|
20
|
+
<% end %>
|
|
21
|
+
<div class="composer-shell">
|
|
22
|
+
<form
|
|
23
|
+
id="chat-composer"
|
|
24
|
+
class="flex min-w-0 flex-1 items-end gap-3"
|
|
25
|
+
hx-on::after-settle="this.querySelector('textarea')?.focus()"
|
|
26
|
+
ws-send
|
|
27
|
+
>
|
|
28
|
+
<% if file.attached? %>
|
|
29
|
+
<input type="hidden" name="attachment_name" value="<%= file.name %>">
|
|
30
|
+
<input type="hidden" name="attachment_path" value="<%= file.path %>">
|
|
31
|
+
<input type="hidden" name="attachment_type" value="<%= file.type %>">
|
|
32
|
+
<% end %>
|
|
33
|
+
<textarea
|
|
34
|
+
name="message"
|
|
35
|
+
rows="1"
|
|
36
|
+
class="composer-input"
|
|
37
|
+
placeholder="Message Relay"
|
|
38
|
+
autocomplete="off"
|
|
39
|
+
autofocus
|
|
40
|
+
onkeydown="if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); this.form.requestSubmit(); }"
|
|
41
|
+
></textarea>
|
|
42
|
+
<div class="composer-actions shrink-0">
|
|
43
|
+
<% if attachment.supported? %>
|
|
44
|
+
<label for="file-upload-input" class="composer-submit button-subtle flex cursor-pointer items-center px-3 py-2 text-sm">
|
|
45
|
+
Attach
|
|
46
|
+
</label>
|
|
47
|
+
<input
|
|
48
|
+
id="file-upload-input"
|
|
49
|
+
name="file"
|
|
50
|
+
type="file"
|
|
51
|
+
accept="<%= attachment.accept %>"
|
|
52
|
+
class="hidden"
|
|
53
|
+
>
|
|
54
|
+
<% end %>
|
|
55
|
+
<button
|
|
56
|
+
class="composer-submit button-primary"
|
|
57
|
+
type="submit"
|
|
58
|
+
>
|
|
59
|
+
Send
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
</form>
|
|
63
|
+
</div>
|
|
64
|
+
<div id="file-upload-indicator" class="upload-indicator text-xs">
|
|
65
|
+
Uploading attachment...
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<div
|
|
2
|
+
id="mcp-settings"
|
|
3
|
+
class="theme-strong flex flex-col gap-3 text-sm"
|
|
4
|
+
<%= %(hx-swap-oob="outerHTML") if swap_oob %>
|
|
5
|
+
>
|
|
6
|
+
<% if show_label %>
|
|
7
|
+
<div class="flex items-center justify-between gap-3">
|
|
8
|
+
<span class="label">MCP</span>
|
|
9
|
+
<span class="sidebar-meta"><%= servers.size %></span>
|
|
10
|
+
</div>
|
|
11
|
+
<% end %>
|
|
12
|
+
<div class="flex items-center justify-between gap-3">
|
|
13
|
+
<span class="theme-muted text-[11px]"><%= servers.empty? ? "No servers" : "#{servers.count { _1[:enabled] }}/#{servers.size} enabled" %></span>
|
|
14
|
+
<a
|
|
15
|
+
class="theme-muted text-[11px] hover:underline"
|
|
16
|
+
href="/mcps"
|
|
17
|
+
hx-get="/mcps"
|
|
18
|
+
hx-target="#chat-panel"
|
|
19
|
+
hx-swap="outerHTML"
|
|
20
|
+
>
|
|
21
|
+
Manage
|
|
22
|
+
</a>
|
|
23
|
+
</div>
|
|
24
|
+
<% unless servers.empty? %>
|
|
25
|
+
<div class="mcp-settings-list scrollbar-chat overflow-y-auto pr-1">
|
|
26
|
+
<ul>
|
|
27
|
+
<% servers.each do |server| %>
|
|
28
|
+
<li class="py-1 first:pt-0 last:pb-0">
|
|
29
|
+
<div class="sidebar-list-item flex items-center justify-between gap-3">
|
|
30
|
+
<div class="min-w-0">
|
|
31
|
+
<div class="theme-strong truncate text-sm font-semibold tracking-tight"><%= server.name %></div>
|
|
32
|
+
<div class="sidebar-meta mt-0.5 line-clamp-1 leading-4"><%= server.transport %></div>
|
|
33
|
+
</div>
|
|
34
|
+
<form class="shrink-0" hx-post="/mcps/<%= server.id %>/toggle" hx-swap="none">
|
|
35
|
+
<button
|
|
36
|
+
type="submit"
|
|
37
|
+
class="inline-flex min-w-[4rem] items-center justify-center gap-1 rounded-full border px-2 py-0.5 text-[10px] font-semibold transition"
|
|
38
|
+
style="<%= server[:enabled] ? "border-color: var(--theme-success-border); background-color: var(--theme-success-bg); color: var(--theme-success-text);" : "border-color: var(--theme-toggle-off-border); background-color: var(--theme-toggle-off-bg); color: var(--theme-toggle-off-text);" %>"
|
|
39
|
+
aria-label="<%= server[:enabled] ? "Disable" : "Enable" %> <%= server.name %>"
|
|
40
|
+
title="<%= server[:enabled] ? "Enabled" : "Disabled" %>"
|
|
41
|
+
>
|
|
42
|
+
<span aria-hidden="true"><%= server[:enabled] ? "●" : "○" %></span>
|
|
43
|
+
<span><%= server[:enabled] ? "On" : "Off" %></span>
|
|
44
|
+
</button>
|
|
45
|
+
</form>
|
|
46
|
+
</div>
|
|
47
|
+
</li>
|
|
48
|
+
<% end %>
|
|
49
|
+
</ul>
|
|
50
|
+
</div>
|
|
51
|
+
<% end %>
|
|
52
|
+
</div>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<%
|
|
2
|
+
role = message.respond_to?(:role) ? message.role.to_sym : message[:role]
|
|
3
|
+
content = message.respond_to?(:content) ? message.content : message[:content]
|
|
4
|
+
%>
|
|
5
|
+
<% if role == :user %>
|
|
6
|
+
<div class="mt-3 flex justify-end first:mt-0">
|
|
7
|
+
<div class="chat-bubble-user w-fit max-w-[75%] [&_*]:text-inherit">
|
|
8
|
+
<div class="assistant-content space-y-2">
|
|
9
|
+
<% Array(content).each do |part| %>
|
|
10
|
+
<% if String === part && !part.empty? %>
|
|
11
|
+
<div><%== markdown(part) %></div>
|
|
12
|
+
<% elsif defined?(LLM::Object) && LLM::Object === part && part.kind == :local_file %>
|
|
13
|
+
<div class="rounded-lg border border-current/20 px-3 py-2 text-sm">
|
|
14
|
+
PDF: <%= File.basename(part.value.path) %>
|
|
15
|
+
</div>
|
|
16
|
+
<% end %>
|
|
17
|
+
<% end %>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
<% else %>
|
|
22
|
+
<div class="message-row">
|
|
23
|
+
<div class="message-avatar">AI</div>
|
|
24
|
+
<div class="message-stack max-w-[88%]">
|
|
25
|
+
<div class="message-label">Assistant</div>
|
|
26
|
+
<div class="assistant-content chat-bubble-assistant">
|
|
27
|
+
<div><%== markdown(content) %></div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
<% end %>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<div id="models-control" class="flex flex-col gap-2">
|
|
2
|
+
<% if show_label %>
|
|
3
|
+
<span class="label">Provider Model</span>
|
|
4
|
+
<% end %>
|
|
5
|
+
<form class="w-full" action="/settings/set-model" method="post">
|
|
6
|
+
<select
|
|
7
|
+
class="select-field"
|
|
8
|
+
name="model"
|
|
9
|
+
hx-post="/settings/set-model"
|
|
10
|
+
hx-sync="#sidebar-controls:replace"
|
|
11
|
+
hx-target="#workspace-main"
|
|
12
|
+
hx-swap="outerHTML"
|
|
13
|
+
hx-trigger="change"
|
|
14
|
+
>
|
|
15
|
+
<% models.each do |model| %>
|
|
16
|
+
<option
|
|
17
|
+
<%= "selected" if model.model_id == self.model %>
|
|
18
|
+
value="<%= model.model_id %>">
|
|
19
|
+
<%= model.name %>
|
|
20
|
+
</option>
|
|
21
|
+
<% end %>
|
|
22
|
+
</select>
|
|
23
|
+
</form>
|
|
24
|
+
<span class="theme-muted text-[11px]"><%= models.size %> models ready to use</span>
|
|
25
|
+
</div>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<div id="providers-control" class="flex flex-col gap-2">
|
|
2
|
+
<% if show_label %>
|
|
3
|
+
<span class="label">Provider Name</span>
|
|
4
|
+
<% end %>
|
|
5
|
+
<form action="/settings/set-provider" method="post">
|
|
6
|
+
<select
|
|
7
|
+
class="select-field"
|
|
8
|
+
name="provider"
|
|
9
|
+
hx-post="/settings/set-provider"
|
|
10
|
+
hx-sync="#sidebar-controls:replace"
|
|
11
|
+
hx-target="#workspace-main"
|
|
12
|
+
hx-swap="outerHTML"
|
|
13
|
+
hx-trigger="change"
|
|
14
|
+
>
|
|
15
|
+
<% Relay.providers.each do |_, builder| %>
|
|
16
|
+
<% llm = builder.call %>
|
|
17
|
+
<option
|
|
18
|
+
<%= "selected" if llm.name.to_s == provider.to_s %>
|
|
19
|
+
value="<%= llm.name %>">
|
|
20
|
+
<%= format_name(llm.name) %>
|
|
21
|
+
</option>
|
|
22
|
+
<% end %>
|
|
23
|
+
</select>
|
|
24
|
+
</form>
|
|
25
|
+
<span class="theme-muted text-[11px]">Choose where responses come from</span>
|
|
26
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div id="chatbot-empty-state" hx-swap-oob="delete"></div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div hx-swap-oob="outerHTML:#chatbot-messages > :last-child .assistant-content > div"><div><%== markdown(message[:content]) %></div></div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<details class="sidebar-overflow">
|
|
2
|
+
<summary aria-label="Open sidebar menu">...</summary>
|
|
3
|
+
<div class="sidebar-overflow-menu">
|
|
4
|
+
<div class="flex flex-col gap-4">
|
|
5
|
+
<%== partial("fragments/providers", locals: {show_label: true}) %>
|
|
6
|
+
<%== partial("fragments/models", locals: {show_label: true}) %>
|
|
7
|
+
<div class="theme-divider"></div>
|
|
8
|
+
<%== partial("fragments/mcp_settings", locals: {servers: mcps, show_label: true, swap_oob: false}) %>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
</details>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<div
|
|
2
|
+
id="chatbot-sidebar-status"
|
|
3
|
+
class="status-block"
|
|
4
|
+
hx-swap-oob="outerHTML"
|
|
5
|
+
>
|
|
6
|
+
<div class="status-label">Status</div>
|
|
7
|
+
<div class="status-value status-value-inline">
|
|
8
|
+
<span class="status-text min-w-0"><%= status %></span>
|
|
9
|
+
<% if cancellable?(status) %>
|
|
10
|
+
<form class="shrink-0" ws-send>
|
|
11
|
+
<input type="hidden" name="type" value="interrupt">
|
|
12
|
+
<button
|
|
13
|
+
class="inline-flex h-5 w-5 items-center justify-center rounded-full border text-[11px] leading-none transition"
|
|
14
|
+
style="border-color: var(--theme-danger-border); background-color: var(--theme-danger-bg); color: var(--theme-danger-text);"
|
|
15
|
+
type="submit"
|
|
16
|
+
aria-label="Cancel request"
|
|
17
|
+
>✕</button>
|
|
18
|
+
</form>
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<div
|
|
2
|
+
id="chatbot-status"
|
|
3
|
+
<%= 'hx-swap-oob="outerHTML"' if swap_oob %>
|
|
4
|
+
class="status-strip"
|
|
5
|
+
>
|
|
6
|
+
<div class="status-block">
|
|
7
|
+
<div class="status-label">Status</div>
|
|
8
|
+
<div class="status-value status-value-inline">
|
|
9
|
+
<span class="status-text min-w-0"><%= status %></span>
|
|
10
|
+
<% if cancellable?(status) %>
|
|
11
|
+
<form class="shrink-0" ws-send>
|
|
12
|
+
<input type="hidden" name="type" value="interrupt">
|
|
13
|
+
<button
|
|
14
|
+
class="inline-flex h-5 w-5 items-center justify-center rounded-full border text-[11px] leading-none transition"
|
|
15
|
+
style="border-color: var(--theme-danger-border); background-color: var(--theme-danger-bg); color: var(--theme-danger-text);"
|
|
16
|
+
type="submit"
|
|
17
|
+
aria-label="Cancel request"
|
|
18
|
+
>✕</button>
|
|
19
|
+
</form>
|
|
20
|
+
<% end %>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="status-block status-block--center">
|
|
24
|
+
<div class="status-label">Context Window</div>
|
|
25
|
+
<div class="status-window">
|
|
26
|
+
<progress
|
|
27
|
+
class="context-progress h-2 w-full overflow-hidden rounded-full"
|
|
28
|
+
value="<%= context_window[:used] %>"
|
|
29
|
+
max="<%= [context_window[:max], 1].max %>"
|
|
30
|
+
><%= context_window[:label] %></progress>
|
|
31
|
+
<span class="status-meta"><%= context_window[:label] %></span>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="status-block status-block--right">
|
|
35
|
+
<div class="status-label">Cost</div>
|
|
36
|
+
<div class="status-value-inline justify-end">
|
|
37
|
+
<span><%= cost %></span>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<div
|
|
2
|
+
id="chatbot-stream"
|
|
3
|
+
class="chat-stream scrollbar-chat min-h-0 min-w-0 flex-1 self-stretch overflow-y-auto"
|
|
4
|
+
>
|
|
5
|
+
<div class="chat-meta">
|
|
6
|
+
<div class="chat-meta-copy min-w-0">
|
|
7
|
+
<div class="theme-strong truncate font-display text-[1.8rem] leading-none tracking-[-0.035em]">Conversation</div>
|
|
8
|
+
<div class="theme-muted mt-1.5 truncate text-sm"><%= ctx.title || "New Chat" %></div>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="space-y-5 pt-6" id="chatbot-messages">
|
|
12
|
+
<% messages.each do |message| %>
|
|
13
|
+
<%== partial("fragments/message", locals: {message:}) %>
|
|
14
|
+
<% end %>
|
|
15
|
+
<% if messages.empty? %>
|
|
16
|
+
<div class="chat-empty-state mx-auto max-w-2xl" id="chatbot-empty-state">
|
|
17
|
+
<div class="section-kicker">Start Here</div>
|
|
18
|
+
<div class="theme-strong mt-2 text-lg font-medium tracking-tight">Ask a question, switch models, or use a tool-backed workflow.</div>
|
|
19
|
+
<div class="theme-muted mt-2.5">
|
|
20
|
+
Relay keeps the conversation, tools, and MCP servers in one workspace so the chat stays central.
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
<% end %>
|
|
24
|
+
<div id="chatbot-bottom"></div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<aside id="tools-list" class="rail-section theme-strong flex min-h-0 flex-1 flex-col text-sm" hx-swap-oob="outerHTML">
|
|
2
|
+
<div class="flex min-h-0 flex-1 flex-col gap-3">
|
|
3
|
+
<span class="label">Tools</span>
|
|
4
|
+
<div class="scrollbar-chat min-h-0 flex-1 overflow-y-auto pr-1">
|
|
5
|
+
<ul>
|
|
6
|
+
<% tools.each do |tool| %>
|
|
7
|
+
<li class="py-2 first:pt-0 last:pb-0">
|
|
8
|
+
<div class="flex items-center justify-between gap-3">
|
|
9
|
+
<div class="theme-strong min-w-0 text-sm font-semibold tracking-tight"><%= tool.name %></div>
|
|
10
|
+
<div class="group relative shrink-0">
|
|
11
|
+
<button
|
|
12
|
+
type="button"
|
|
13
|
+
class="detail-button"
|
|
14
|
+
>
|
|
15
|
+
Details
|
|
16
|
+
</button>
|
|
17
|
+
<div class="tooltip tooltip-card">
|
|
18
|
+
<div class="theme-accent text-xs font-semibold uppercase tracking-[0.16em]">
|
|
19
|
+
About
|
|
20
|
+
</div>
|
|
21
|
+
<div class="theme-divider my-2"></div>
|
|
22
|
+
<div class="tooltip text-sm leading-6">
|
|
23
|
+
<%= tool.description %>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</li>
|
|
29
|
+
<% end %>
|
|
30
|
+
</ul>
|
|
31
|
+
</div>
|
|
32
|
+
<span class="theme-muted text-[11px]"><%= tools.size %> tools available</span>
|
|
33
|
+
</div>
|
|
34
|
+
</aside>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<% preset = Relay::Models::MCP::Preset[form.preset] %>
|
|
2
|
+
<section id="mcp-editor-pane" class="mcp-pane scrollbar-chat min-h-0 overflow-y-auto px-1 py-1 pr-2">
|
|
3
|
+
<div class="mb-4 flex items-start justify-between gap-4">
|
|
4
|
+
<div>
|
|
5
|
+
<div id="mcp-editor-title" class="text-lg font-semibold tracking-tight">
|
|
6
|
+
<%= form.persisted? ? preset[:title] : "Add MCP Server" %>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="theme-muted mt-1 text-sm"><%= preset[:summary] %></div>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
<form
|
|
12
|
+
id="mcp-editor"
|
|
13
|
+
class="grid gap-3"
|
|
14
|
+
action="<%= form.persisted? ? "/mcps/#{form.id}" : "/mcps" %>"
|
|
15
|
+
method="post"
|
|
16
|
+
hx-post="<%= form.persisted? ? "/mcps/#{form.id}" : "/mcps" %>"
|
|
17
|
+
hx-target="#chat-panel"
|
|
18
|
+
hx-swap="outerHTML"
|
|
19
|
+
>
|
|
20
|
+
<input type="hidden" name="id" value="<%= form.id %>">
|
|
21
|
+
<input type="hidden" name="preset" value="<%= form.preset %>">
|
|
22
|
+
<div class="space-y-2">
|
|
23
|
+
<span class="label">Preset</span>
|
|
24
|
+
<div class="mcp-transport-grid">
|
|
25
|
+
<% Relay::Models::MCP::Preset.all.each do |entry| %>
|
|
26
|
+
<button
|
|
27
|
+
class="mcp-transport-option <%= "is-active" if form.preset == entry[:id] %>"
|
|
28
|
+
type="button"
|
|
29
|
+
hx-post="/mcps/form"
|
|
30
|
+
hx-include="#mcp-editor"
|
|
31
|
+
hx-vals='{"preset":"<%= entry[:id] %>"}'
|
|
32
|
+
hx-target="#mcp-editor-pane"
|
|
33
|
+
hx-swap="outerHTML"
|
|
34
|
+
>
|
|
35
|
+
<span class="mcp-transport-copy">
|
|
36
|
+
<span class="mcp-transport-title"><%= entry[:title] %></span>
|
|
37
|
+
<span class="mcp-transport-hint"><%= entry[:summary] %></span>
|
|
38
|
+
</span>
|
|
39
|
+
</button>
|
|
40
|
+
<% end %>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
<% if form.preset == "forgejo" %>
|
|
44
|
+
<%== partial("fragments/mcp/fields_forgejo", locals: {form:}) %>
|
|
45
|
+
<% else %>
|
|
46
|
+
<%== partial("fragments/mcp/fields_github", locals: {form:}) %>
|
|
47
|
+
<% end %>
|
|
48
|
+
</form>
|
|
49
|
+
<div class="mt-3 flex flex-wrap items-center justify-end gap-3">
|
|
50
|
+
<button id="mcp-save" class="composer-submit px-4 py-2" type="submit" form="mcp-editor">
|
|
51
|
+
Save
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
</section>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<div class="grid gap-4 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
|
|
2
|
+
<div class="space-y-2">
|
|
3
|
+
<label class="label" for="mcp-url">Forgejo URL</label>
|
|
4
|
+
<input id="mcp-url" class="field font-mono text-sm" name="url" type="url" placeholder="https://code.example.com" required value="<%= form.url %>">
|
|
5
|
+
</div>
|
|
6
|
+
<div class="space-y-2">
|
|
7
|
+
<label class="label" for="mcp-token">Forgejo token</label>
|
|
8
|
+
<input id="mcp-token" class="field font-mono text-sm" name="token" type="password" placeholder="Paste access token" required value="<%= form.token %>">
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="mcp-helper-copy">
|
|
12
|
+
<div class="label">Transport</div>
|
|
13
|
+
<div class="theme-muted mt-2 text-sm leading-6">
|
|
14
|
+
Relay will run the Forgejo server locally and pass your URL and token as environment variables.
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<div class="grid gap-3">
|
|
2
|
+
<div class="space-y-2">
|
|
3
|
+
<label class="label" for="mcp-token">GitHub token</label>
|
|
4
|
+
<input id="mcp-token" class="field font-mono text-sm" name="token" type="password" placeholder="ghp_..." required value="<%= form.token %>">
|
|
5
|
+
</div>
|
|
6
|
+
<div class="mcp-helper-copy">
|
|
7
|
+
<div class="label">Transport</div>
|
|
8
|
+
<div class="theme-muted mt-2 text-sm leading-6">
|
|
9
|
+
Relay will connect to <code>https://api.githubcopilot.com/mcp/</code> and send your token as <code>Authorization: Bearer ...</code>.
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<aside class="mcp-pane scrollbar-chat flex min-h-0 flex-col overflow-y-auto border-l pl-4">
|
|
2
|
+
<div class="mb-4 flex items-center justify-between gap-3">
|
|
3
|
+
<div>
|
|
4
|
+
<div class="text-sm font-semibold">Configured</div>
|
|
5
|
+
</div>
|
|
6
|
+
<button
|
|
7
|
+
id="mcp-new"
|
|
8
|
+
class="composer-submit px-3 py-2 text-sm"
|
|
9
|
+
type="button"
|
|
10
|
+
hx-get="/mcps/new"
|
|
11
|
+
hx-target="#chat-panel"
|
|
12
|
+
hx-swap="outerHTML"
|
|
13
|
+
>
|
|
14
|
+
New
|
|
15
|
+
</button>
|
|
16
|
+
</div>
|
|
17
|
+
<% if mcps.empty? %>
|
|
18
|
+
<div class="field theme-muted text-sm">No MCP servers yet.</div>
|
|
19
|
+
<% else %>
|
|
20
|
+
<div class="flex flex-col gap-2 pr-1">
|
|
21
|
+
<% mcps.each do |mcp| %>
|
|
22
|
+
<div class="mcp-card <%= "is-active" if selected_id == mcp.id %>">
|
|
23
|
+
<div class="flex items-start justify-between gap-3">
|
|
24
|
+
<div class="min-w-0">
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
class="mcp-card-select w-full text-left"
|
|
28
|
+
hx-get="/mcps/<%= mcp.id %>"
|
|
29
|
+
hx-target="#chat-panel"
|
|
30
|
+
hx-swap="outerHTML"
|
|
31
|
+
>
|
|
32
|
+
<div class="truncate font-medium"><%= mcp.name %></div>
|
|
33
|
+
<div class="theme-muted mt-1 line-clamp-1 text-xs leading-5"><%= mcp[:enabled] ? "Enabled" : "Disabled" %></div>
|
|
34
|
+
</button>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="flex items-center gap-2">
|
|
37
|
+
<form class="shrink-0" hx-post="/mcps/<%= mcp.id %>/toggle?page=1" hx-target="#chat-panel" hx-swap="outerHTML">
|
|
38
|
+
<button
|
|
39
|
+
type="submit"
|
|
40
|
+
class="mcp-card-status"
|
|
41
|
+
aria-label="<%= mcp[:enabled] ? "Disable" : "Enable" %> <%= mcp.name %>"
|
|
42
|
+
>
|
|
43
|
+
<%= mcp[:enabled] ? "On" : "Off" %>
|
|
44
|
+
</button>
|
|
45
|
+
</form>
|
|
46
|
+
<form action="/mcps/<%= mcp.id %>/delete" method="post" hx-post="/mcps/<%= mcp.id %>/delete" hx-target="#chat-panel" hx-swap="outerHTML">
|
|
47
|
+
<button class="mcp-card-delete" type="submit" aria-label="Delete <%= mcp.name %>">X</button>
|
|
48
|
+
</form>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
<% end %>
|
|
53
|
+
</div>
|
|
54
|
+
<% end %>
|
|
55
|
+
</aside>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<div id="chat-panel" class="workspace-chat">
|
|
2
|
+
<section id="mcp-workspace" class="chat-surface">
|
|
3
|
+
<header class="chat-meta">
|
|
4
|
+
<div class="min-w-0">
|
|
5
|
+
<div class="text-lg font-semibold tracking-tight">MCP Servers</div>
|
|
6
|
+
</div>
|
|
7
|
+
<a class="mcp-nav-link" href="/" hx-get="/chat" hx-target="#chat-panel" hx-swap="outerHTML">Close</a>
|
|
8
|
+
</header>
|
|
9
|
+
<div class="mcp-workspace-body min-h-0 flex-1">
|
|
10
|
+
<%== partial("fragments/mcp/editor", locals: {form:}) %>
|
|
11
|
+
<%== partial("fragments/mcp/list", locals: {mcps:, selected_id:}) %>
|
|
12
|
+
</div>
|
|
13
|
+
</section>
|
|
14
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%== partial("fragments/chat", locals: {messages:, swap_oob: false}) %>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%== partial("fragments/input", locals: {swap_oob: false}) %>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%== partial("fragments/contexts", locals: {contexts:, show_label: true, swap_oob: true}) %>
|