insika 0.3.0 → 0.7.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.
Files changed (190) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +180 -0
  3. data/README.md +45 -10
  4. data/bin/insika +684 -0
  5. data/bin/insika-router +87 -0
  6. data/docs/AGENTS.md +94 -403
  7. data/docs/API.md +5 -5
  8. data/docs/ARCHITECTURE.md +3 -2
  9. data/docs/ARTIFACTS.md +95 -0
  10. data/docs/BENCHMARK.md +2 -2
  11. data/docs/CHANNELS.md +14 -14
  12. data/docs/CONTEXT.md +9 -7
  13. data/docs/DEMO.md +80 -0
  14. data/docs/DEPLOY.md +71 -3
  15. data/docs/EMBEDDING.md +1 -1
  16. data/docs/EVALS.md +128 -3
  17. data/docs/FACTS.md +3 -3
  18. data/docs/HARVEST.md +5 -6
  19. data/docs/KNOWLEDGE.md +290 -0
  20. data/docs/LOADTEST.md +2 -2
  21. data/docs/MEDIA.md +128 -0
  22. data/docs/OBSERVABILITY.md +15 -10
  23. data/docs/OUTCOMES.md +137 -0
  24. data/docs/PLUGINS.md +51 -6
  25. data/docs/POLICY.md +216 -0
  26. data/docs/REFINEMENT.md +14 -9
  27. data/docs/RELEASING.md +4 -4
  28. data/docs/ROUTER.md +213 -0
  29. data/docs/RUNNING-LOCAL.md +3 -3
  30. data/docs/SCHEDULING.md +121 -0
  31. data/docs/SECURITY.md +22 -6
  32. data/docs/SKILLS.md +11 -2
  33. data/docs/SOAK.md +2 -2
  34. data/docs/TEMPLATES.md +134 -0
  35. data/docs/TOOLS.md +152 -27
  36. data/docs/WHY.md +1 -1
  37. data/docs/WORKFLOWS.md +2 -2
  38. data/docs/_includes/head_custom.html +5 -0
  39. data/docs/_includes/title.html +13 -0
  40. data/docs/_sass/color_schemes/insika.scss +32 -0
  41. data/docs/_sass/custom/custom.scss +199 -0
  42. data/docs/_sass/custom/setup.scss +26 -0
  43. data/docs/assets/img/favicon.svg +7 -0
  44. data/docs/assets/img/insika-mark.svg +7 -0
  45. data/docs/core-concepts.md +21 -0
  46. data/docs/domain.md +4 -4
  47. data/docs/improve.md +20 -0
  48. data/docs/index.md +8 -5
  49. data/docs/integrate.md +20 -0
  50. data/docs/operate.md +13 -6
  51. data/docs/prompts/ADD-TOOL.md +118 -0
  52. data/docs/prompts/DIAGNOSE-TURN.md +65 -0
  53. data/docs/prompts/GO-LIVE.md +138 -0
  54. data/docs/prompts/RUN-EXAMPLES.md +70 -0
  55. data/docs/reference.md +19 -0
  56. data/docs/ship.md +10 -2
  57. data/docs/start-here.md +18 -0
  58. data/lib/insika/agent_profile.rb +73 -16
  59. data/lib/insika/artifact_signing.rb +82 -0
  60. data/lib/insika/artifact_store.rb +160 -0
  61. data/lib/insika/channel_delivery.rb +1 -1
  62. data/lib/insika/chat_builder.rb +22 -2
  63. data/lib/insika/commands/agent_payload.rb +2 -2
  64. data/lib/insika/commands/backfill_knowledge.rb +145 -0
  65. data/lib/insika/commands/delete_artifact.rb +35 -0
  66. data/lib/insika/commands/delete_concept.rb +34 -0
  67. data/lib/insika/commands/delete_mcp.rb +6 -2
  68. data/lib/insika/commands/delete_tenant_data.rb +15 -3
  69. data/lib/insika/commands/gate_refinement.rb +1 -1
  70. data/lib/insika/commands/refresh_mcp_tools.rb +47 -0
  71. data/lib/insika/commands/restore_concept.rb +34 -0
  72. data/lib/insika/commands/seed_demo_data.rb +31 -0
  73. data/lib/insika/commands/upsert_mcp.rb +6 -3
  74. data/lib/insika/commands/write_concept.rb +57 -0
  75. data/lib/insika/context/priority.rb +2 -0
  76. data/lib/insika/context/providers/knowledge.rb +108 -0
  77. data/lib/insika/context/providers/prompt.rb +30 -24
  78. data/lib/insika/cron.rb +189 -0
  79. data/lib/insika/demo/agent_attrs.rb +43 -0
  80. data/lib/insika/demo/golden_cases.rb +81 -0
  81. data/lib/insika/demo/seeder.rb +336 -0
  82. data/lib/insika/doctor.rb +176 -8
  83. data/lib/insika/dsl/definition.rb +3 -2
  84. data/lib/insika/dsl/runtime.rb +60 -79
  85. data/lib/insika/dsl/server_boot.rb +23 -1
  86. data/lib/insika/dsl/system.rb +10 -2
  87. data/lib/insika/dsl.rb +103 -2
  88. data/lib/insika/env_schema.rb +16 -1
  89. data/lib/insika/evals/golden.rb +41 -4
  90. data/lib/insika/evals/judge.rb +47 -2
  91. data/lib/insika/evals/pairwise.rb +11 -0
  92. data/lib/insika/evals/persona.rb +98 -0
  93. data/lib/insika/evals/runner.rb +9 -0
  94. data/lib/insika/evals/simulator.rb +225 -0
  95. data/lib/insika/evals/transport.rb +83 -1
  96. data/lib/insika/event_stream.rb +10 -0
  97. data/lib/insika/executor.rb +231 -55
  98. data/lib/insika/followup_policy.rb +2 -25
  99. data/lib/insika/golden_store.rb +16 -1
  100. data/lib/insika/grounding/matcher.rb +1 -1
  101. data/lib/insika/knowledge.rb +680 -0
  102. data/lib/insika/knowledge_store.rb +140 -0
  103. data/lib/insika/mcp_client.rb +94 -0
  104. data/lib/insika/mcp_json.rb +74 -0
  105. data/lib/insika/mcp_live_tool.rb +43 -0
  106. data/lib/insika/mcp_store.rb +98 -26
  107. data/lib/insika/mcp_tool_ingestor.rb +30 -8
  108. data/lib/insika/mcp_tool_registry.rb +100 -0
  109. data/lib/insika/media.rb +115 -31
  110. data/lib/insika/message_origin.rb +1 -1
  111. data/lib/insika/middleware.rb +9 -0
  112. data/lib/insika/onboarding.rb +17 -1
  113. data/lib/insika/outcome_store.rb +1 -1
  114. data/lib/insika/overlay_tool_registry.rb +37 -17
  115. data/lib/insika/packaging.rb +2 -2
  116. data/lib/insika/profile_source.rb +8 -1
  117. data/lib/insika/prompt_catalog.rb +10 -0
  118. data/lib/insika/retention.rb +36 -1
  119. data/lib/insika/router/app.rb +157 -0
  120. data/lib/insika/router/backend_pool.rb +98 -0
  121. data/lib/insika/router/hash_ring.rb +55 -0
  122. data/lib/insika/router/proxy_body.rb +34 -0
  123. data/lib/insika/router/session_key.rb +54 -0
  124. data/lib/insika/router.rb +18 -0
  125. data/lib/insika/schedule.rb +177 -0
  126. data/lib/insika/schedule_engine.rb +314 -0
  127. data/lib/insika/schedule_store.rb +208 -0
  128. data/lib/insika/server/app.rb +105 -15
  129. data/lib/insika/server/rack_app.rb +5 -1
  130. data/lib/insika/server/responses.rb +1 -1
  131. data/lib/insika/skill_catalog.rb +12 -0
  132. data/lib/insika/steer_injector.rb +21 -10
  133. data/lib/insika/studio/app.rb +567 -45
  134. data/lib/insika/studio/assets/dist/application.css +1 -1
  135. data/lib/insika/studio/assets/dist/application.js +21 -21
  136. data/lib/insika/studio/forms.rb +46 -5
  137. data/lib/insika/studio/nav_icons.rb +14 -1
  138. data/lib/insika/studio/views/_agent_tab_cache.erb +25 -0
  139. data/lib/insika/studio/views/_agent_tab_config.erb +514 -0
  140. data/lib/insika/studio/views/_agent_tab_history.erb +24 -0
  141. data/lib/insika/studio/views/_agent_tab_loops.erb +54 -0
  142. data/lib/insika/studio/views/_agent_tab_memory.erb +51 -0
  143. data/lib/insika/studio/views/_agent_tab_outcomes.erb +31 -0
  144. data/lib/insika/studio/views/_agent_tab_prompts.erb +108 -0
  145. data/lib/insika/studio/views/_agent_tab_skills.erb +38 -0
  146. data/lib/insika/studio/views/_agents_master.erb +44 -0
  147. data/lib/insika/studio/views/_message.erb +49 -32
  148. data/lib/insika/studio/views/agent_detail.erb +61 -820
  149. data/lib/insika/studio/views/agents.erb +70 -57
  150. data/lib/insika/studio/views/artifact.erb +23 -0
  151. data/lib/insika/studio/views/artifacts.erb +59 -0
  152. data/lib/insika/studio/views/evals.erb +2 -2
  153. data/lib/insika/studio/views/facts.erb +1 -1
  154. data/lib/insika/studio/views/funnel.erb +1 -1
  155. data/lib/insika/studio/views/home.erb +106 -67
  156. data/lib/insika/studio/views/knowledge.erb +123 -0
  157. data/lib/insika/studio/views/layout.erb +14 -11
  158. data/lib/insika/studio/views/mcp.erb +174 -80
  159. data/lib/insika/studio/views/session.erb +231 -177
  160. data/lib/insika/studio/views/settings.erb +39 -1
  161. data/lib/insika/studio/views/skills.erb +1 -1
  162. data/lib/insika/studio/views/tools.erb +24 -9
  163. data/lib/insika/templates/browser-agent/README.md +36 -0
  164. data/lib/insika/templates/browser-agent/agent.rb +49 -0
  165. data/lib/insika/templates/daily-digest/README.md +38 -0
  166. data/lib/insika/templates/daily-digest/agent.rb +77 -0
  167. data/lib/insika/templates/repo-explorer/README.md +36 -0
  168. data/lib/insika/templates/repo-explorer/agent.rb +45 -0
  169. data/lib/insika/templates/research-analyst/README.md +26 -0
  170. data/lib/insika/templates/research-analyst/agent.rb +58 -0
  171. data/lib/insika/templates/review-panel/README.md +20 -0
  172. data/lib/insika/templates/review-panel/agent.rb +50 -0
  173. data/lib/insika/templates/travel-planner/README.md +35 -0
  174. data/lib/insika/templates/travel-planner/agent.rb +87 -0
  175. data/lib/insika/templates.rb +112 -0
  176. data/lib/insika/tick.rb +24 -12
  177. data/lib/insika/timezone.rb +45 -0
  178. data/lib/insika/tools/generate_image.rb +52 -7
  179. data/lib/insika/tools/load_knowledge.rb +74 -0
  180. data/lib/insika/tools/run_persona_eval.rb +328 -0
  181. data/lib/insika/tools/save_artifact.rb +95 -0
  182. data/lib/insika/turn_output.rb +1 -1
  183. data/lib/insika/turn_state.rb +15 -4
  184. data/lib/insika/version.rb +1 -1
  185. data/lib/insika/wiring/graph.rb +184 -12
  186. data/lib/insika/wiring/graph_chat.rb +102 -0
  187. data/lib/insika.rb +57 -0
  188. metadata +105 -5
  189. data/docs/build.md +0 -14
  190. data/docs/understand.md +0 -10
@@ -1,67 +1,80 @@
1
+ <%# Agents — miller columns: master list | detail frame. A row
2
+ click loads the detail beside the list (no full page load) and advances the
3
+ URL. The detail pane's landing state hosts the template gallery and the
4
+ create form; both submit to the top window so the fresh master list
5
+ renders with the new agent in it. %>
1
6
  <header class="page-head">
2
7
  <h1>Agents<span class="sub"><%= @agents.size %> configured · one agent each</span></h1>
3
8
  <a class="btn primary" href="/studio/playground">Open playground →</a>
4
9
  </header>
5
10
 
6
- <% if @agents.empty? %>
7
- <div class="empty">No agents yet. Create your first agent below (id + model), then give it an identity on its detail page.</div>
8
- <% else %>
9
- <div data-controller="list-filter">
10
- <div class="filter-bar">
11
- <input type="search" class="filter-input" placeholder="Filter agents by id, model or provider…" aria-label="Filter agents"
12
- data-list-filter-target="query" data-action="input->list-filter#filter keydown->list-filter#clear">
13
- <span class="filter-count mono muted" data-list-filter-target="count"></span>
11
+ <div class="drill" data-controller="list-filter">
12
+ <%== render("_agents_master", locals: { active_id: nil, show_outcomes: true }) %>
13
+
14
+ <turbo-frame id="agent-detail" class="drill-pane drill-detail">
15
+ <div class="drill-pane-head">
16
+ <div class="title">
17
+ <strong>Pick an agent</strong>
18
+ <span class="count">select on the left, or start a new one below</span>
19
+ </div>
14
20
  </div>
15
- <div class="agent-grid">
16
- <% @agents.each do |a| %>
17
- <article class="card agent" data-list-filter-target="item" data-filter-text="<%= a.id %> <%= a.model %> <%= a.provider %>">
18
- <a class="agent-head" href="/studio/agents/<%= Rack::Utils.escape(a.id) %>">
19
- <span class="avatar <%= avatar_class(a.id) %>"><%= a.id[0, 2].upcase %></span>
20
- <div>
21
- <div class="agent-id"><%= a.id %></div>
22
- <div class="muted mono"><%= a.model %><%== " · #{h(a.provider)}" if a.provider %></div>
21
+ <div class="drill-pane-body">
22
+ <% if @templates&.any? %>
23
+ <div class="section">
24
+ <div class="section-head">
25
+ <h2>From template</h2>
26
+ <p class="muted">Example agents shipped in the gem — pick one to see a working pattern, then edit it like any agent.</p>
27
+ </div>
28
+ <div class="template-grid">
29
+ <% @templates.each do |t| %>
30
+ <article class="card template">
31
+ <div class="template-head">
32
+ <strong><%= t.title %></strong>
33
+ <span class="pill info"><%= t.trail %></span>
34
+ </div>
35
+ <p class="muted"><%= t.description %></p>
36
+ <% if t.capabilities.any? %>
37
+ <p class="mono muted template-caps"><%= t.capabilities.join(" · ") %></p>
38
+ <% end %>
39
+ <% if t.studio? %>
40
+ <form method="post" action="/studio/agents/from_template/<%= Rack::Utils.escape(t.name) %>" data-turbo-frame="_top">
41
+ <%== csrf_tag %>
42
+ <button type="submit" class="btn" data-turbo-submits-with="Creating…">Create</button>
43
+ </form>
44
+ <% else %>
45
+ <p class="muted">CLI-only for now — run <code>insika new <%= t.name %></code>.</p>
46
+ <% end %>
47
+ </article>
48
+ <% end %>
23
49
  </div>
24
- <% if a.memory %><span class="pill info">memory</span><% end %>
25
- <% if (latest = @latest_outcomes&.dig(a.id)) %>
26
- <span class="pill <%= latest[:outcome] == "conversion" ? "ok" : "warn" %>"
27
- title="last outcome recorded via /v1/outcomes"><%= latest[:outcome] %><%== format(" · R$ %0.2f", latest[:value]) if latest[:value] && latest[:value].positive? %></span>
28
- <% end %>
29
- </a>
30
- <dl class="agent-meta">
31
- <div><dt>tools</dt><dd><%= a.tools_allow.nil? ? "all" : a.tools_allow.size %></dd></div>
32
- <div><dt>skills</dt><dd><%= a.skills.nil? ? "all" : Array(a.skills).size %></dd></div>
33
- <div><dt>prompts</dt><dd><%= Array(a.prompt_files).size %></dd></div>
34
- </dl>
35
- <div class="agent-actions">
36
- <a class="btn" href="/studio/agents/<%= Rack::Utils.escape(a.id) %>">Edit</a>
37
- <a class="btn ghost" href="/studio/playground?agent=<%= Rack::Utils.escape(a.id) %>">Test</a>
38
50
  </div>
39
- </article>
40
- <% end %>
41
- </div>
42
- <div class="empty" data-list-filter-target="empty" hidden>No agents match your filter.</div>
43
- </div>
44
- <% end %>
51
+ <% end %>
45
52
 
46
- <details class="card newfile"<%== " open" if @agents.empty? %>>
47
- <summary>+ new agent</summary>
48
- <form method="post" action="/studio/agents">
49
- <%== csrf_tag %>
50
- <div class="grid-2">
51
- <label>id <span class="muted">(unique slug, e.g. demo)</span>
52
- <input type="text" name="id" placeholder="demo" required>
53
- </label>
54
- <label>model
55
- <input type="text" name="model" placeholder="deepseek-v4-flash" required>
56
- </label>
53
+ <div class="section">
54
+ <div class="section-head">
55
+ <h2>New agent</h2>
56
+ <p class="muted">id + model — give it an identity on its detail page.</p>
57
+ </div>
58
+ <form method="post" action="/studio/agents" data-turbo-frame="_top">
59
+ <%== csrf_tag %>
60
+ <div class="grid-2">
61
+ <label>id <span class="muted">(unique slug, e.g. demo)</span>
62
+ <input type="text" name="id" placeholder="demo" required>
63
+ </label>
64
+ <label>model
65
+ <input type="text" name="model" placeholder="deepseek-v4-flash" required>
66
+ </label>
67
+ </div>
68
+ <label>provider <span class="muted">(optional)</span>
69
+ <input type="text" name="provider" placeholder="deepseek">
70
+ </label>
71
+ <label class="check">
72
+ <input type="checkbox" name="memory" value="1" checked>
73
+ cross-session memory (the agent remembers across conversations)
74
+ </label>
75
+ <div class="actions"><button type="submit" class="primary" data-turbo-submits-with="Creating…">Create agent</button></div>
76
+ </form>
77
+ </div>
57
78
  </div>
58
- <label>provider <span class="muted">(optional)</span>
59
- <input type="text" name="provider" placeholder="deepseek">
60
- </label>
61
- <label class="check">
62
- <input type="checkbox" name="memory" value="1" checked>
63
- cross-session memory (the agent remembers across conversations)
64
- </label>
65
- <div class="actions"><button type="submit" class="primary" data-turbo-submits-with="Creating…">Create agent</button></div>
66
- </form>
67
- </details>
79
+ </turbo-frame>
80
+ </div>
@@ -0,0 +1,23 @@
1
+ <%# the artifact preview page. The content renders ONLY inside a SANDBOXED
2
+ iframe (no scripts, no same-origin, no forms) on top of the content
3
+ route's own restrictive CSP — artifact content is LLM output, untrusted. %>
4
+ <header class="page-head">
5
+ <div>
6
+ <nav class="crumbs"><a href="/studio/artifacts">Artifacts</a></nav>
7
+ <h1><%= @artifact.title %><span class="sub"><span class="mono"><%= @artifact.id %></span> · <%= @artifact.mime %> · <%= @artifact.created_at %></span></h1>
8
+ </div>
9
+ <div class="head-actions">
10
+ <% if @signed_url %>
11
+ <button type="button" class="btn sm" data-copy="<%= @signed_url %>">copy signed link</button>
12
+ <% end %>
13
+ <a class="btn sm ghost" href="<%= @content_url %>" target="_blank" rel="noopener">open raw</a>
14
+ <form method="post" action="/studio/artifacts/<%= Rack::Utils.escape_path(@artifact.id) %>/delete" class="inline">
15
+ <%== csrf_tag %>
16
+ <button class="btn sm ghost danger">delete</button>
17
+ </form>
18
+ </div>
19
+ </header>
20
+
21
+ <div class="card artifact-frame">
22
+ <iframe sandbox src="<%= @content_url %>" title="<%= @artifact.title %>"></iframe>
23
+ </div>
@@ -0,0 +1,59 @@
1
+ <%# the Artifacts page: the report destination. Reads the ArtifactStore
2
+ directly (the listing IS the history); the only mutation — delete —
3
+ dispatches :delete_artifact on the bus. The preview is a sandboxed iframe
4
+ on top of the content route's own restrictive CSP. %>
5
+ <header class="page-head">
6
+ <div>
7
+ <nav class="crumbs"><span>operate</span></nav>
8
+ <h1>Artifacts<span class="sub">the report destination — saved pages, per agent</span></h1>
9
+ </div>
10
+ <div class="head-actions">
11
+ <%== agent_filter_form("/studio/artifacts", @agent) %>
12
+ </div>
13
+ </header>
14
+
15
+ <% if @artifacts.empty? %>
16
+ <div class="empty">
17
+ <% if @agent %>
18
+ No artifacts for <span class="mono"><%= @agent %></span> — an agent that
19
+ has <span class="mono">save_artifact</span> in its <span class="mono">tools_allow</span>
20
+ writes its reports here.
21
+ <% else %>
22
+ No artifacts yet, for any agent.
23
+ <% end %>
24
+ </div>
25
+ <% else %>
26
+ <h2><%= @agent || "all agents" %><span class="sub"><%= @artifacts.size %> artifact(s)</span></h2>
27
+ <div class="card">
28
+ <table class="grid">
29
+ <thead>
30
+ <tr>
31
+ <% unless @agent %><th>agent</th><% end %>
32
+ <th>title</th><th>mime</th><th>task</th><th>created_at</th><th></th>
33
+ </tr>
34
+ </thead>
35
+ <tbody>
36
+ <% @artifacts.each do |a| %>
37
+ <tr>
38
+ <% unless @agent %><td class="mono"><%= a.agent %></td><% end %>
39
+ <td><a href="/studio/artifacts/<%= Rack::Utils.escape_path(a.id) %>"><%= a.title %></a></td>
40
+ <td><span class="pill"><%= a.mime %></span></td>
41
+ <td class="mono"><%= a.task_id %></td>
42
+ <td class="mono"><%= a.created_at %></td>
43
+ <td class="row-actions">
44
+ <% if @signed_available %>
45
+ <button type="button" class="btn ghost sm" data-copy="<%= signed_artifact_url(a.id) %>">copy link</button>
46
+ <% end %>
47
+ <form method="post" action="/studio/artifacts/<%= Rack::Utils.escape_path(a.id) %>/delete"
48
+ class="inline" data-confirm="Delete this artifact?">
49
+ <%== csrf_tag %>
50
+ <input type="hidden" name="agent" value="<%= a.agent %>">
51
+ <button class="btn ghost sm danger">delete</button>
52
+ </form>
53
+ </td>
54
+ </tr>
55
+ <% end %>
56
+ </tbody>
57
+ </table>
58
+ </div>
59
+ <% end %>
@@ -28,8 +28,8 @@
28
28
  <div class="nav-label"><%= agent %></div>
29
29
  <% cases.each do |g| %>
30
30
  <a class="drill-item<%== ' active' if @case && @case.id == g.id %>" href="/studio/evals?id=<%= Rack::Utils.escape(g.id) %>">
31
- <div class="drill-item-name"><span class="txt mono"><%= g.id %></span><% if g.rubric.to_s.strip.empty? %><span class="pill info plain">no rubric</span><% end %></div>
32
- <div class="drill-item-sub"><%= g.user_turns.first %></div>
31
+ <div class="drill-item-name"><span class="txt mono"><%= g.id %></span><% if g.rubric.to_s.strip.empty? %><span class="pill info plain">no rubric</span><% end %><% if g.simulated? %><span class="pill accent plain">simulated</span><% end %></div>
32
+ <div class="drill-item-sub"><%= g.opens_with.to_s %></div>
33
33
  </a>
34
34
  <% end %>
35
35
  <% end %>
@@ -12,7 +12,7 @@
12
12
  <form method="get" action="/studio/facts" class="actions inline">
13
13
  <label>
14
14
  Agent / scope
15
- <select name="agent" onchange="this.form.submit()">
15
+ <select name="agent" data-controller="auto-submit" data-action="change->auto-submit#submit">
16
16
  <option value="">all</option>
17
17
  <% @stores.each do |scope| %>
18
18
  <option value="<%= scope %>"<%== ' selected' if scope == @filter %>><%= scope %></option>
@@ -13,7 +13,7 @@
13
13
  <form method="get" action="/studio/funnel" class="actions inline">
14
14
  <label>
15
15
  Period (days)
16
- <select name="period" onchange="this.form.submit()">
16
+ <select name="period" data-controller="auto-submit" data-action="change->auto-submit#submit">
17
17
  <% [7, 30, 90].each do |p| %>
18
18
  <option value="<%= p %>"<%== ' selected' if @period == p %>><%= p %></option>
19
19
  <% end %>
@@ -1,73 +1,112 @@
1
- <%# Overview home (T3): stat cards + activity chart + recent conversations.
2
- All from data the Studio already reads; charts are inline SVG (geometry via
3
- attributes, fill via stylesheet — CSP-safe, no inline style). %>
4
- <header class="page-head">
5
- <div>
6
- <h1>Overview<span class="sub">at a glance</span></h1>
7
- </div>
8
- <div class="head-actions">
9
- <%== agent_filter_form("/studio/home", @agent) %>
10
- <span class="pill <%= @active_now.positive? ? "ok online" : "info" %> plain"><%= @active_now %> active now</span>
11
- <a class="btn primary" href="/studio/playground">Open playground →</a>
12
- </div>
13
- </header>
14
-
15
- <div class="stat-cards">
16
- <% [["conversations", "Conversations"], ["messages", "Messages"], ["agents", "Agents"],
17
- ["skills", "Skills"], ["tools", "Tools"], ["providers", "LLM providers"]].each do |key, label| %>
18
- <div class="stat-card">
19
- <div class="stat-num"><%= @counts[key] %></div>
20
- <div class="stat-label"><%= label %></div>
1
+ <%# Overview home: stat cards + activity chart + recent
2
+ conversations, all LIVE over the existing SSE channel (live_home_controller
3
+ subscribes to /studio/events; the server-rendered numbers are its baseline).
4
+ Charts are inline SVG (geometry via attributes, fill via stylesheet —
5
+ CSP-safe, no inline style). %>
6
+ <div data-controller="live-home" data-live-home-active-value="<%= @active_now %>">
7
+ <header class="page-head">
8
+ <div>
9
+ <h1>Overview<span class="sub">at a glance</span></h1>
21
10
  </div>
22
- <% end %>
23
- </div>
24
-
25
- <div class="home-grid">
26
- <section class="card home-activity">
27
- <div class="section-head">
28
- <h2>Activity</h2>
29
- <span class="muted">conversations touched · last 14 days</span>
30
- </div>
31
- <% max = [@activity.map(&:last).max || 0, 1].max %>
32
- <svg class="barchart" viewBox="0 0 700 176" preserveAspectRatio="none" role="img" aria-label="conversations per day, last 14 days">
33
- <% n = @activity.size; slot = 700.0 / n; bw = slot * 0.58 %>
34
- <% @activity.each_with_index do |(d, c), i| %>
35
- <% h = (c.to_f / max) * 150.0; x = (i * slot) + ((slot - bw) / 2.0); y = 160.0 - h %>
36
- <rect class="bar<%== ' bar-empty' if c.zero? %>" x="<%= format('%.1f', x) %>" y="<%= format('%.1f', c.zero? ? 157.0 : y) %>"
37
- width="<%= format('%.1f', bw) %>" height="<%= format('%.1f', c.zero? ? 3.0 : [h, 3.0].max) %>" rx="2">
38
- <title><%= d.strftime('%b %-d') %> · <%= c %> conversation<%= 's' unless c == 1 %></title>
39
- </rect>
40
- <% end %>
41
- <line class="barchart-axis-line" x1="0" y1="160" x2="700" y2="160" />
42
- </svg>
43
- <div class="barchart-axis">
44
- <span><%= @activity.first&.first&.strftime('%b %-d') %></span>
45
- <span class="peak">peak <%= max %>/day</span>
46
- <span><%= @activity.last&.first&.strftime('%b %-d') %></span>
11
+ <div class="head-actions">
12
+ <%== agent_filter_form("/studio/home", @agent) %>
13
+ <%# "active now" — conversations touched in the last 5 minutes (the same
14
+ number render_home computes). The ring spins while anything is hot;
15
+ the ago label tracks the last event the page has seen. %>
16
+ <span class="live-badge<%= @active_now.positive? ? " hot" : "" %>" title="conversations touched in the last 5 minutes">
17
+ <svg viewBox="0 0 16 16" aria-hidden="true"><circle class="ring" cx="8" cy="8" r="6.5"/></svg>
18
+ <b class="tnum" data-live-home-target="count"><%= @active_now %></b> active now
19
+ <span class="live-ago muted" data-live-home-target="ago"></span>
20
+ </span>
21
+ <a class="btn primary" href="/studio/playground">Open playground →</a>
47
22
  </div>
48
- </section>
23
+ </header>
49
24
 
50
- <section class="card home-recent">
51
- <div class="section-head">
52
- <h2>Recent conversations</h2>
53
- <a class="muted" href="/studio/chats">all →</a>
54
- </div>
55
- <% if @recent.empty? %>
56
- <div class="empty">No conversations yet. Start one in the <a href="/studio/playground">playground</a>.</div>
57
- <% else %>
58
- <ul class="convo-rail">
59
- <% @recent.each do |s| %>
60
- <li>
61
- <a href="/studio/sessions/<%= Rack::Utils.escape(s.id) %>">
62
- <span class="avatar sm <%= avatar_class(s.id) %>"><%= s.id[0, 2].upcase %></span>
63
- <span class="convo-main">
64
- <span class="convo-id mono"><%= s.id[0, 20] %></span>
65
- <span class="convo-sub"><%= Array(s.messages).size %> msg · <%= time_ago(s.updated_at) %></span>
66
- </span>
67
- </a>
68
- </li>
25
+ <div class="stat-cards">
26
+ <% [["conversations", "Conversations", @conv_trend],
27
+ ["messages", "Messages", @msg_trend],
28
+ ["agents", "Agents", nil],
29
+ ["skills", "Skills", nil],
30
+ ["tools", "Tools", nil],
31
+ ["providers", "LLM providers", nil]].each do |key, label, trend| %>
32
+ <div class="stat-card">
33
+ <div class="stat-num tnum"><%= @counts[key] %></div>
34
+ <div class="stat-label"><%= label %><% if trend && !trend.zero? %>
35
+ <span class="trend <%= trend.positive? ? "up" : "down" %>" title="vs yesterday"><%= trend.positive? ? "+" : "−" %><%= trend.abs %></span>
36
+ <% end %></div>
37
+ </div>
38
+ <% end %>
39
+ </div>
40
+
41
+ <div class="home-grid">
42
+ <section class="card home-activity">
43
+ <div class="section-head">
44
+ <h2>Activity</h2>
45
+ <span class="muted">conversations touched · last 14 days</span>
46
+ </div>
47
+ <%# 24h sparkline: area-fill + 1.5px line over the hourly
48
+ series; same session scan, finer buckets. Geometry via attributes,
49
+ colors via classes — CSP-safe like the barchart below. %>
50
+ <% s_max = [@activity_24h.map(&:last).max || 0, 1].max %>
51
+ <% s_n = @activity_24h.size %>
52
+ <% s_pts = @activity_24h.each_with_index.map do |(_t, c), i|
53
+ [i * (480.0 / (s_n - 1)), 44.0 - (c.to_f / s_max) * 36.0]
54
+ end %>
55
+ <% s_line = s_pts.map { |x, y| format("%.1f,%.1f", x, y) }.join(" ") %>
56
+ <div class="sparkline-strip">
57
+ <div class="sparkline-meta">
58
+ <b class="tnum"><%= @activity_24h.sum { |_t, c| c } %></b> conversations · last 24h
59
+ </div>
60
+ <svg class="sparkline" viewBox="0 0 480 48" preserveAspectRatio="none" role="img"
61
+ aria-label="conversations per hour, last 24 hours">
62
+ <polygon class="spark-area" points="<%= s_line %> 480,48 0,48"/>
63
+ <polyline class="spark-line" points="<%= s_line %>" vector-effect="non-scaling-stroke"/>
64
+ </svg>
65
+ </div>
66
+ <% max = [@activity.map(&:last).max || 0, 1].max %>
67
+ <svg class="barchart" viewBox="0 0 700 176" preserveAspectRatio="none" role="img" aria-label="conversations per day, last 14 days">
68
+ <% n = @activity.size; slot = 700.0 / n; bw = slot * 0.58 %>
69
+ <% @activity.each_with_index do |(d, c), i| %>
70
+ <% h = (c.to_f / max) * 150.0; x = (i * slot) + ((slot - bw) / 2.0); y = 160.0 - h %>
71
+ <rect class="bar<%== ' bar-empty' if c.zero? %>" x="<%= format('%.1f', x) %>" y="<%= format('%.1f', c.zero? ? 157.0 : y) %>"
72
+ width="<%= format('%.1f', bw) %>" height="<%= format('%.1f', c.zero? ? 3.0 : [h, 3.0].max) %>" rx="2">
73
+ <title><%= d.strftime('%b %-d') %> · <%= c %> conversation<%= 's' unless c == 1 %></title>
74
+ </rect>
75
+ <% end %>
76
+ <line class="barchart-axis-line" x1="0" y1="160" x2="700" y2="160" />
77
+ </svg>
78
+ <div class="barchart-axis">
79
+ <span><%= @activity.first&.first&.strftime('%b %-d') %></span>
80
+ <span class="peak">peak <%= max %>/day</span>
81
+ <span><%= @activity.last&.first&.strftime('%b %-d') %></span>
82
+ </div>
83
+ </section>
84
+
85
+ <section class="card home-recent">
86
+ <div class="section-head">
87
+ <h2>Recent conversations</h2>
88
+ <a class="muted" href="/studio/chats">all →</a>
89
+ </div>
90
+ <% cutoff = Time.now - (5 * 60) %>
91
+ <ul class="convo-rail" data-live-home-target="list">
92
+ <% if @recent.empty? %>
93
+ <li data-live-home-target="placeholder"><div class="empty">No conversations yet. Start one in the <a href="/studio/playground">playground</a>.</div></li>
94
+ <% else %>
95
+ <% @recent.each do |s| %>
96
+ <% fresh = (t = parse_time(s.updated_at)) && t >= cutoff %>
97
+ <li data-session-id="<%= s.id %>">
98
+ <a href="/studio/sessions/<%= Rack::Utils.escape(s.id) %>">
99
+ <span class="avatar sm <%= avatar_class(s.id) %>"><%= s.id[0, 2].upcase %></span>
100
+ <span class="convo-main">
101
+ <span class="convo-id mono"><%= s.id[0, 20] %></span>
102
+ <span class="convo-sub"><%= Array(s.messages).size %> msg · <%= time_ago(s.updated_at) %></span>
103
+ </span>
104
+ <span class="presence<%= " present" if fresh %>" title="touched in the last 5 minutes"></span>
105
+ </a>
106
+ </li>
107
+ <% end %>
69
108
  <% end %>
70
109
  </ul>
71
- <% end %>
72
- </section>
110
+ </section>
111
+ </div>
73
112
  </div>
@@ -0,0 +1,123 @@
1
+ <%# Knowledge — drill-down, single-agent-scoped (like Harvest, not Skills'
2
+ shared+override shape): the KnowledgeStore is a real per-agent partition.
3
+ @agent/@tenant/@status pick the scope; @selected drives the detail pane
4
+ (nil = none, "" = new, name = edit). %>
5
+ <header class="page-head">
6
+ <div>
7
+ <nav class="crumbs"><span>knowledge</span></nav>
8
+ <h1>Knowledge<span class="sub"><%= @agent ? @agent.to_s : "no agent" %></span></h1>
9
+ </div>
10
+ </header>
11
+
12
+ <p class="muted matrix-hint">Concepts the engine extracted from finished conversations — facts, procedures, policies, objections. Learned content is <code>provenance: observed</code>; an operator promotes one to <code>policy</code> by editing it here. A conflict is never resolved automatically — a human decides.</p>
13
+
14
+ <% if insika[:knowledge_store].nil? %>
15
+ <div class="empty">No knowledge — the store is not wired.</div>
16
+ <% elsif @agents.empty? %>
17
+ <div class="empty">No agents yet. Add a <span class="mono">knowledge:</span> block to an agent's pack to start learning from its traffic.</div>
18
+ <% else %>
19
+ <% if @agents.size > 1 %>
20
+ <div class="filter-bar">
21
+ <% @agents.each do |id| %>
22
+ <a class="btn" href="/studio/knowledge?agent=<%= Rack::Utils.escape(id) %>"<%== ' aria-current="page"' if id == @agent %>><%= id %></a>
23
+ <% end %>
24
+ </div>
25
+ <% end %>
26
+ <div class="filter-bar">
27
+ <a class="btn" href="/studio/knowledge?agent=<%= Rack::Utils.escape(@agent.to_s) %>&status=all"<%== ' aria-current="page"' if @status == "all" %>>all <span class="count"><%= @all_count %></span></a>
28
+ <a class="btn" href="/studio/knowledge?agent=<%= Rack::Utils.escape(@agent.to_s) %>&status=conflict"<%== ' aria-current="page"' if @status == "conflict" %>>conflicts <span class="count"><%= @conflict_count %></span></a>
29
+ </div>
30
+
31
+ <div class="drill" data-controller="list-filter">
32
+ <aside class="drill-pane drill-master">
33
+ <div class="drill-pane-head">
34
+ <div class="title"><strong>Concepts <span class="count"><%= @concepts.size %></span></strong></div>
35
+ <a class="btn primary btn-sm" href="/studio/knowledge/new?agent=<%= Rack::Utils.escape(@agent.to_s) %>">+ new</a>
36
+ </div>
37
+ <div class="drill-search">
38
+ <input type="search" class="filter-input" placeholder="Search concepts…" aria-label="Filter concepts"
39
+ data-list-filter-target="query" data-action="input->list-filter#filter keydown->list-filter#clear">
40
+ </div>
41
+ <nav class="drill-pane-body drill-list" aria-label="concepts">
42
+ <% @concepts.each do |c| %>
43
+ <a class="drill-item<%== ' active' if @selected == c[:name] %>"
44
+ href="/studio/knowledge/<%= Rack::Utils.escape(c[:name]) %>?agent=<%= Rack::Utils.escape(@agent.to_s) %>"
45
+ data-list-filter-target="item" data-filter-text="<%= c[:name] %> <%= c[:description] %>">
46
+ <div class="drill-item-name">
47
+ <span class="txt mono"><%= c[:name] %></span>
48
+ <% if concept_conflict?(c) %><span class="pill warn">conflict</span><% end %>
49
+ <span class="pill"><%= c[:type] %></span>
50
+ </div>
51
+ <div class="drill-item-sub"><%= c[:description] %> · <span class="pill info"><%= format("%.2f", c[:confidence]) %></span> · ×<%= c[:occurrences] %></div>
52
+ </a>
53
+ <% end %>
54
+ <div class="empty" data-list-filter-target="empty" hidden>No concepts match your filter.</div>
55
+ </nav>
56
+ </aside>
57
+
58
+ <section class="drill-pane drill-detail">
59
+ <% if @selected.nil? %>
60
+ <div class="drill-empty">
61
+ <div>
62
+ <h2>Knowledge</h2>
63
+ <p>Pick a concept on the left to read or edit it — or create one by hand.</p>
64
+ </div>
65
+ </div>
66
+ <% else %>
67
+ <% new_concept = @selected == "" %>
68
+ <div class="drill-pane-head">
69
+ <div class="title">
70
+ <strong class="mono"><%= new_concept ? "New concept" : @selected %></strong>
71
+ <% if !new_concept && concept_conflict?(@selected_concept) %><span class="pill warn">conflict — resolve by editing below</span><% end %>
72
+ </div>
73
+ <button type="submit" form="concept-form" class="btn primary" data-turbo-submits-with="Saving…">Save concept</button>
74
+ </div>
75
+ <div class="drill-pane-body">
76
+ <% unless new_concept %>
77
+ <form method="post" action="/studio/knowledge/<%= Rack::Utils.escape(@selected) %>/delete<%= @tenant ? "?tenant=#{Rack::Utils.escape(@tenant)}" : '' %>" data-turbo-confirm="Remove concept &quot;<%= @selected %>&quot;? This can't be undone.">
78
+ <%== csrf_tag %>
79
+ <input type="hidden" name="agent" value="<%= @agent %>">
80
+ <div class="actions"><button type="submit" class="ghost danger" data-turbo-submits-with="Removing…">Remove</button></div>
81
+ </form>
82
+ <% end %>
83
+ <form id="concept-form" method="post" action="/studio/knowledge" data-controller="dirty-guard">
84
+ <%== csrf_tag %>
85
+ <input type="hidden" name="agent" value="<%= @agent %>">
86
+ <% if @tenant %><input type="hidden" name="tenant" value="<%= @tenant %>"><% end %>
87
+ <% if new_concept %>
88
+ <label>name <span class="muted">(canonical — same as <code>name:</code> in the frontmatter)</span>
89
+ <input type="text" name="name" placeholder="my-concept" required>
90
+ </label>
91
+ <% else %>
92
+ <input type="hidden" name="name" value="<%= @selected %>">
93
+ <% end %>
94
+ <label>Concept markdown <span class="muted">YAML frontmatter (<code>name</code>, <code>description</code>, <code>type</code>, <code>provenance</code>, <code>confidence</code>, …) + the durable claim</span></label>
95
+ <div data-controller="code-editor" data-code-editor-language-value="markdown" data-code-editor-preview-value="true">
96
+ <textarea name="content" data-code-editor-target="source" rows="18"><%= @concept_content %></textarea>
97
+ </div>
98
+ </form>
99
+
100
+ <% if !new_concept && Array(@concept_versions).any? %>
101
+ <details class="versions">
102
+ <summary><%= @concept_versions.size %> previous version(s)</summary>
103
+ <ul>
104
+ <% @concept_versions.each_with_index do |v, i| %>
105
+ <li>
106
+ <span class="muted mono"><%= v["at"] %></span>
107
+ <form method="post" action="/studio/knowledge/<%= Rack::Utils.escape(@selected) %>/restore">
108
+ <%== csrf_tag %>
109
+ <input type="hidden" name="agent" value="<%= @agent %>">
110
+ <% if @tenant %><input type="hidden" name="tenant" value="<%= @tenant %>"><% end %>
111
+ <input type="hidden" name="version" value="<%= i %>">
112
+ <button type="submit" class="ghost">Restore</button>
113
+ </form>
114
+ </li>
115
+ <% end %>
116
+ </ul>
117
+ </details>
118
+ <% end %>
119
+ </div>
120
+ <% end %>
121
+ </section>
122
+ </div>
123
+ <% end %>
@@ -3,6 +3,11 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <%# Turbo 8 Drive routes same-origin visits through the View Transitions API
7
+ when this meta is present; browsers without support degrade
8
+ to the plain swap. The CSS owns the actual transition (application.css,
9
+ ::view-transition-*), including the prefers-reduced-motion kill switch. %>
10
+ <meta name="view-transition" content="same-origin">
6
11
  <meta name="csp-nonce" content="<%= csp_nonce %>">
7
12
  <title>Insika Studio</title>
8
13
  <link rel="icon" type="image/svg+xml" href="<%= asset_path("favicon.svg") %>">
@@ -10,6 +15,15 @@
10
15
  <script type="module" src="<%= asset_path("application.js") %>" defer></script>
11
16
  </head>
12
17
  <body>
18
+ <%# Chip-icon sprite: one source of truth for transcript event
19
+ chips — the ERB partial renders chip_icon() from the same nav_icons.rb set,
20
+ and live_transcript_controller.js clones from this template, so live and
21
+ replayed transcripts can never drift apart. %>
22
+ <template id="chip-icons" aria-hidden="true">
23
+ <% Studio::NavIcons::CHIP_ICONS.each do |kind, icon| %>
24
+ <span data-kind="<%= kind %>" class="chip-ic"><%== nav_icon(icon) %></span>
25
+ <% end %>
26
+ </template>
13
27
  <% if authenticated? %>
14
28
  <div class="app-shell">
15
29
  <aside class="sidebar">
@@ -60,17 +74,6 @@
60
74
  </aside>
61
75
 
62
76
  <div class="app-main">
63
- <% if restart_needed? %>
64
- <div class="banner" role="status">
65
- <span><strong>Restart recommended.</strong> MCP changes take effect after the runtime restarts.</span>
66
- <form method="post" action="/studio/restart-ack">
67
- <%== csrf_tag %>
68
- <input type="hidden" name="back" value="<%= request.script_name %><%= request.path %>">
69
- <button type="submit" class="ghost">Dismiss</button>
70
- </form>
71
- </div>
72
- <% end %>
73
-
74
77
  <% if flash["error"] %>
75
78
  <div class="flash err" role="alert"><%= flash["error"] %></div>
76
79
  <% end %>