insika 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +325 -17
- data/README.md +52 -11
- data/bin/insika +895 -8
- data/bin/insika-router +87 -0
- data/docs/AGENTS.md +229 -163
- data/docs/API.md +56 -0
- data/docs/ARCHITECTURE.md +3 -2
- data/docs/ARTIFACTS.md +95 -0
- data/docs/BENCHMARK.md +2 -2
- data/docs/CHANNELS.md +111 -21
- data/docs/CONTEXT.md +132 -19
- data/docs/DEMO.md +80 -0
- data/docs/DEPLOY.md +75 -4
- data/docs/EMBEDDING.md +1 -1
- data/docs/EVALS.md +146 -2
- data/docs/FACTS.md +135 -0
- data/docs/HARVEST.md +116 -0
- data/docs/KNOWLEDGE.md +290 -0
- data/docs/LOADTEST.md +16 -10
- data/docs/MEDIA.md +128 -0
- data/docs/OBSERVABILITY.md +65 -6
- data/docs/OUTCOMES.md +137 -0
- data/docs/PLUGINS.md +51 -6
- data/docs/POLICY.md +216 -0
- data/docs/REFINEMENT.md +17 -12
- data/docs/RELEASING.md +27 -0
- data/docs/ROUTER.md +213 -0
- data/docs/RUNNING-LOCAL.md +6 -6
- data/docs/SCHEDULING.md +121 -0
- data/docs/SECURITY.md +79 -2
- data/docs/SKILLS.md +13 -4
- data/docs/SOAK.md +127 -0
- data/docs/TEMPLATES.md +134 -0
- data/docs/TOOLS.md +220 -27
- data/docs/WHY.md +1 -1
- data/docs/WORKFLOWS.md +2 -2
- data/docs/_includes/head_custom.html +5 -0
- data/docs/_includes/title.html +13 -0
- data/docs/_sass/color_schemes/insika.scss +32 -0
- data/docs/_sass/custom/custom.scss +199 -0
- data/docs/_sass/custom/setup.scss +26 -0
- data/docs/assets/img/favicon.svg +7 -0
- data/docs/assets/img/insika-mark.svg +7 -0
- data/docs/core-concepts.md +21 -0
- data/docs/domain.md +115 -0
- data/docs/improve.md +20 -0
- data/docs/index.md +9 -6
- data/docs/integrate.md +20 -0
- data/docs/operate.md +13 -6
- data/docs/prompts/ADD-TOOL.md +118 -0
- data/docs/prompts/DIAGNOSE-TURN.md +65 -0
- data/docs/prompts/GO-LIVE.md +138 -0
- data/docs/prompts/RUN-EXAMPLES.md +70 -0
- data/docs/reference.md +19 -0
- data/docs/ship.md +10 -2
- data/docs/start-here.md +18 -0
- data/lib/insika/agent_profile.rb +202 -10
- data/lib/insika/artifact_signing.rb +82 -0
- data/lib/insika/artifact_store.rb +160 -0
- data/lib/insika/balloon_splitter.rb +102 -0
- data/lib/insika/budget_ledger.rb +34 -3
- data/lib/insika/cache_series_store.rb +49 -0
- data/lib/insika/channel_delivery.rb +119 -14
- data/lib/insika/channels/relay.rb +77 -3
- data/lib/insika/channels/web.rb +2 -2
- data/lib/insika/chat_builder.rb +105 -2
- data/lib/insika/checkpoint_store.rb +16 -0
- data/lib/insika/commands/agent_payload.rb +3 -3
- data/lib/insika/commands/backfill_knowledge.rb +145 -0
- data/lib/insika/commands/cancel_followup.rb +49 -0
- data/lib/insika/commands/delete_artifact.rb +35 -0
- data/lib/insika/commands/delete_concept.rb +34 -0
- data/lib/insika/commands/delete_mcp.rb +6 -2
- data/lib/insika/commands/delete_tenant_data.rb +107 -0
- data/lib/insika/commands/export_customer_memory.rb +48 -0
- data/lib/insika/commands/forget_customer.rb +117 -0
- data/lib/insika/commands/freeze_funnel_baseline.rb +113 -0
- data/lib/insika/commands/gate_harvest.rb +138 -0
- data/lib/insika/commands/gate_refinement.rb +1 -1
- data/lib/insika/commands/judge_shadow_pairs.rb +124 -0
- data/lib/insika/commands/memory_forget_fact.rb +20 -4
- data/lib/insika/commands/memory_put_fact.rb +23 -4
- data/lib/insika/commands/promote_harvest.rb +130 -0
- data/lib/insika/commands/record_outcome.rb +46 -0
- data/lib/insika/commands/record_shadow_reply.rb +68 -0
- data/lib/insika/commands/refresh_mcp_tools.rb +47 -0
- data/lib/insika/commands/reject_harvest.rb +38 -0
- data/lib/insika/commands/resolve_proposal.rb +108 -0
- data/lib/insika/commands/restore_concept.rb +34 -0
- data/lib/insika/commands/revoke_contact.rb +49 -0
- data/lib/insika/commands/rollback_harvest.rb +86 -0
- data/lib/insika/commands/run_distillation.rb +186 -0
- data/lib/insika/commands/run_harvest.rb +393 -0
- data/lib/insika/commands/seed_demo_data.rb +31 -0
- data/lib/insika/commands/send_message.rb +103 -6
- data/lib/insika/commands/session_purge.rb +67 -0
- data/lib/insika/commands/upsert_mcp.rb +6 -3
- data/lib/insika/commands/write_concept.rb +57 -0
- data/lib/insika/contact_store.rb +183 -0
- data/lib/insika/context/builder.rb +21 -3
- data/lib/insika/context/fragment.rb +7 -3
- data/lib/insika/context/priority.rb +5 -0
- data/lib/insika/context/provider.rb +17 -3
- data/lib/insika/context/providers/briefing.rb +96 -0
- data/lib/insika/context/providers/knowledge.rb +108 -0
- data/lib/insika/context/providers/memory.rb +16 -7
- data/lib/insika/context/providers/prompt.rb +57 -23
- data/lib/insika/context/providers/skill.rb +2 -0
- data/lib/insika/context/providers/tool_search.rb +2 -0
- data/lib/insika/context_trace_store.rb +38 -2
- data/lib/insika/cron.rb +189 -0
- data/lib/insika/demo/agent_attrs.rb +43 -0
- data/lib/insika/demo/golden_cases.rb +81 -0
- data/lib/insika/demo/seeder.rb +336 -0
- data/lib/insika/distill.rb +224 -0
- data/lib/insika/distill_engine.rb +169 -0
- data/lib/insika/doctor.rb +889 -11
- data/lib/insika/dsl/definition.rb +3 -2
- data/lib/insika/dsl/runtime.rb +69 -82
- data/lib/insika/dsl/server_boot.rb +92 -1
- data/lib/insika/dsl/system.rb +10 -2
- data/lib/insika/dsl.rb +186 -5
- data/lib/insika/edge_limiter.rb +28 -7
- data/lib/insika/env_schema.rb +25 -3
- data/lib/insika/errors.rb +11 -0
- data/lib/insika/evals/assertions.rb +3 -2
- data/lib/insika/evals/golden.rb +41 -4
- data/lib/insika/evals/judge.rb +47 -2
- data/lib/insika/evals/pairwise.rb +29 -6
- data/lib/insika/evals/persona.rb +98 -0
- data/lib/insika/evals/runner.rb +9 -0
- data/lib/insika/evals/simulator.rb +225 -0
- data/lib/insika/evals/transport.rb +83 -1
- data/lib/insika/event_stream.rb +10 -0
- data/lib/insika/evidence.rb +183 -0
- data/lib/insika/executor.rb +869 -76
- data/lib/insika/followup_engine.rb +207 -0
- data/lib/insika/followup_policy.rb +198 -0
- data/lib/insika/followup_store.rb +306 -0
- data/lib/insika/funnel_declaration.rb +106 -0
- data/lib/insika/funnel_fold.rb +179 -0
- data/lib/insika/funnel_store.rb +163 -0
- data/lib/insika/golden_store.rb +17 -2
- data/lib/insika/grounding/matcher.rb +69 -0
- data/lib/insika/grounding.rb +44 -0
- data/lib/insika/harvest/conversion_gate.rb +159 -0
- data/lib/insika/harvest/criterion.rb +98 -0
- data/lib/insika/harvest/gate.rb +194 -0
- data/lib/insika/harvest/negative_list.rb +199 -0
- data/lib/insika/harvest.rb +241 -0
- data/lib/insika/harvest_engine.rb +193 -0
- data/lib/insika/harvest_store.rb +548 -0
- data/lib/insika/knowledge.rb +680 -0
- data/lib/insika/knowledge_store.rb +140 -0
- data/lib/insika/mcp_client.rb +94 -0
- data/lib/insika/mcp_json.rb +74 -0
- data/lib/insika/mcp_live_tool.rb +43 -0
- data/lib/insika/mcp_store.rb +98 -26
- data/lib/insika/mcp_tool_ingestor.rb +30 -8
- data/lib/insika/mcp_tool_registry.rb +100 -0
- data/lib/insika/media.rb +382 -0
- data/lib/insika/memory_audit_store.rb +85 -0
- data/lib/insika/memory_store.rb +264 -23
- data/lib/insika/message_origin.rb +7 -2
- data/lib/insika/middleware.rb +9 -0
- data/lib/insika/model_visible.rb +87 -0
- data/lib/insika/model_visible_trace_store.rb +66 -0
- data/lib/insika/onboarding.rb +22 -1
- data/lib/insika/outbox_store.rb +42 -4
- data/lib/insika/outcome_store.rb +147 -0
- data/lib/insika/overlay_tool_registry.rb +37 -17
- data/lib/insika/packaging.rb +163 -0
- data/lib/insika/parity/criterion.rb +79 -0
- data/lib/insika/parity/verdict.rb +318 -0
- data/lib/insika/prefix_fingerprint.rb +58 -0
- data/lib/insika/profile_source.rb +29 -1
- data/lib/insika/prompt_catalog.rb +10 -0
- data/lib/insika/proposal_store.rb +271 -0
- data/lib/insika/queue_policy.rb +4 -1
- data/lib/insika/refinement/proposer.rb +1 -1
- data/lib/insika/reliability.rb +32 -6
- data/lib/insika/retention.rb +316 -0
- data/lib/insika/router/app.rb +157 -0
- data/lib/insika/router/backend_pool.rb +98 -0
- data/lib/insika/router/hash_ring.rb +55 -0
- data/lib/insika/router/proxy_body.rb +34 -0
- data/lib/insika/router/session_key.rb +54 -0
- data/lib/insika/router.rb +18 -0
- data/lib/insika/routing.rb +101 -0
- data/lib/insika/safety/config.rb +44 -4
- data/lib/insika/safety/corpus.rb +255 -0
- data/lib/insika/safety/detectors.rb +32 -113
- data/lib/insika/safety/factory.rb +15 -2
- data/lib/insika/safety/grounding_enforcer.rb +59 -0
- data/lib/insika/safety/grounding_validator.rb +49 -0
- data/lib/insika/safety/input_guardrail.rb +1 -1
- data/lib/insika/safety/moderator.rb +1 -1
- data/lib/insika/safety/output_filter.rb +9 -5
- data/lib/insika/safety/output_validator.rb +11 -5
- data/lib/insika/schedule.rb +177 -0
- data/lib/insika/schedule_engine.rb +314 -0
- data/lib/insika/schedule_store.rb +208 -0
- data/lib/insika/schema_guard.rb +35 -0
- data/lib/insika/server/app.rb +247 -17
- data/lib/insika/server/rack_app.rb +21 -1
- data/lib/insika/server/responses.rb +40 -1
- data/lib/insika/session_actor.rb +8 -4
- data/lib/insika/session_store.rb +65 -2
- data/lib/insika/settings_store.rb +10 -0
- data/lib/insika/shadow_pair_store.rb +258 -0
- data/lib/insika/skill_catalog.rb +16 -0
- data/lib/insika/soak/envelope.rb +140 -0
- data/lib/insika/soak/report.rb +392 -0
- data/lib/insika/soak/runner.rb +554 -0
- data/lib/insika/steer_injector.rb +21 -10
- data/lib/insika/store.rb +10 -1
- data/lib/insika/stores/memory.rb +6 -0
- data/lib/insika/stores/sqlite.rb +8 -0
- data/lib/insika/studio/app.rb +1437 -54
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +23 -23
- data/lib/insika/studio/assets/dist/favicon.svg +6 -0
- data/lib/insika/studio/forms.rb +308 -10
- data/lib/insika/studio/nav_icons.rb +19 -1
- data/lib/insika/studio/views/_agent_tab_cache.erb +25 -0
- data/lib/insika/studio/views/_agent_tab_config.erb +514 -0
- data/lib/insika/studio/views/_agent_tab_history.erb +24 -0
- data/lib/insika/studio/views/_agent_tab_loops.erb +54 -0
- data/lib/insika/studio/views/_agent_tab_memory.erb +51 -0
- data/lib/insika/studio/views/_agent_tab_outcomes.erb +31 -0
- data/lib/insika/studio/views/_agent_tab_prompts.erb +108 -0
- data/lib/insika/studio/views/_agent_tab_skills.erb +38 -0
- data/lib/insika/studio/views/_agents_master.erb +44 -0
- data/lib/insika/studio/views/_message.erb +49 -32
- data/lib/insika/studio/views/agent_detail.erb +62 -278
- data/lib/insika/studio/views/agents.erb +71 -54
- data/lib/insika/studio/views/approvals.erb +4 -1
- data/lib/insika/studio/views/artifact.erb +23 -0
- data/lib/insika/studio/views/artifacts.erb +59 -0
- data/lib/insika/studio/views/chats.erb +4 -1
- data/lib/insika/studio/views/customer.erb +94 -0
- data/lib/insika/studio/views/customers.erb +32 -0
- data/lib/insika/studio/views/evals.erb +6 -3
- data/lib/insika/studio/views/facts.erb +133 -0
- data/lib/insika/studio/views/followups.erb +125 -0
- data/lib/insika/studio/views/funnel.erb +106 -0
- data/lib/insika/studio/views/harvest.erb +234 -0
- data/lib/insika/studio/views/home.erb +106 -66
- data/lib/insika/studio/views/knowledge.erb +123 -0
- data/lib/insika/studio/views/layout.erb +15 -11
- data/lib/insika/studio/views/mcp.erb +174 -80
- data/lib/insika/studio/views/parity.erb +147 -0
- data/lib/insika/studio/views/playground.erb +7 -1
- data/lib/insika/studio/views/session.erb +233 -124
- data/lib/insika/studio/views/settings.erb +41 -1
- data/lib/insika/studio/views/skills.erb +1 -2
- data/lib/insika/studio/views/system_files.erb +1 -1
- data/lib/insika/studio/views/task.erb +13 -0
- data/lib/insika/studio/views/tasks.erb +4 -1
- data/lib/insika/studio/views/tools.erb +24 -10
- data/lib/insika/task_store.rb +21 -1
- data/lib/insika/templates/browser-agent/README.md +36 -0
- data/lib/insika/templates/browser-agent/agent.rb +49 -0
- data/lib/insika/templates/daily-digest/README.md +38 -0
- data/lib/insika/templates/daily-digest/agent.rb +77 -0
- data/lib/insika/templates/repo-explorer/README.md +36 -0
- data/lib/insika/templates/repo-explorer/agent.rb +45 -0
- data/lib/insika/templates/research-analyst/README.md +26 -0
- data/lib/insika/templates/research-analyst/agent.rb +58 -0
- data/lib/insika/templates/review-panel/README.md +20 -0
- data/lib/insika/templates/review-panel/agent.rb +50 -0
- data/lib/insika/templates/travel-planner/README.md +35 -0
- data/lib/insika/templates/travel-planner/agent.rb +87 -0
- data/lib/insika/templates.rb +112 -0
- data/lib/insika/testing/store_contract.rb +27 -6
- data/lib/insika/tick.rb +47 -11
- data/lib/insika/timezone.rb +45 -0
- data/lib/insika/tool_definition.rb +17 -7
- data/lib/insika/tool_envelope.rb +69 -0
- data/lib/insika/tool_manifest.rb +5 -1
- data/lib/insika/tools/data_defined_tool.rb +10 -0
- data/lib/insika/tools/generate_image.rb +89 -0
- data/lib/insika/tools/load_knowledge.rb +74 -0
- data/lib/insika/tools/run_persona_eval.rb +328 -0
- data/lib/insika/tools/save_artifact.rb +95 -0
- data/lib/insika/tools/schedule_followup.rb +164 -0
- data/lib/insika/tools/tts.rb +47 -0
- data/lib/insika/tools/update_briefing.rb +126 -0
- data/lib/insika/turn_output.rb +1 -1
- data/lib/insika/turn_state.rb +49 -1
- data/lib/insika/turn_timing.rb +22 -2
- data/lib/insika/version.rb +1 -1
- data/lib/insika/vitals.rb +84 -0
- data/lib/insika/wiring/graph.rb +472 -13
- data/lib/insika/wiring/graph_chat.rb +102 -0
- data/lib/insika.rb +149 -3
- metadata +181 -5
- data/docs/build.md +0 -14
- data/docs/understand.md +0 -10
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
<header class="page-head">
|
|
2
|
+
<div>
|
|
3
|
+
<nav class="crumbs"><span>harvest</span></nav>
|
|
4
|
+
<h1>Harvest<span class="sub"><%= @agent ? "#{@agent}" : "no agent" %></span></h1>
|
|
5
|
+
</div>
|
|
6
|
+
</header>
|
|
7
|
+
|
|
8
|
+
<% if insika[:harvest_store].nil? %>
|
|
9
|
+
<div class="empty">No harvest — the store is not wired.</div>
|
|
10
|
+
<% elsif @agents.empty? %>
|
|
11
|
+
<div class="empty">No agents yet. Add a <span class="mono">harvest:</span> block to an agent's pack to mine its finished traffic.</div>
|
|
12
|
+
<% else %>
|
|
13
|
+
<div class="card">
|
|
14
|
+
<form method="post" action="/studio/harvest" class="actions">
|
|
15
|
+
<%== csrf_tag %>
|
|
16
|
+
<label>
|
|
17
|
+
Agent
|
|
18
|
+
<select name="agent">
|
|
19
|
+
<% @agents.each do |id| %>
|
|
20
|
+
<option value="<%= id %>"<%== ' selected' if id == @agent %>><%= id %></option>
|
|
21
|
+
<% end %>
|
|
22
|
+
</select>
|
|
23
|
+
</label>
|
|
24
|
+
<label title="Ignore the mined markers and re-read the whole window (the ledger dedup still protects).">
|
|
25
|
+
<input type="checkbox" name="full" value="1"> full window
|
|
26
|
+
</label>
|
|
27
|
+
<button type="submit" class="primary">Mine now</button>
|
|
28
|
+
</form>
|
|
29
|
+
<div class="skill-desc muted">
|
|
30
|
+
Reads this agent's finished conversations, asks the configured miner for SKILL
|
|
31
|
+
proposals, and filters them through the negative list and the evidence ledger.
|
|
32
|
+
Nothing is ever applied automatically — a candidate only lands after the double
|
|
33
|
+
gate AND a human approval.
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<% if @agents.size > 1 %>
|
|
38
|
+
<div class="filter-bar">
|
|
39
|
+
<% @agents.each do |id| %>
|
|
40
|
+
<a class="btn" href="/studio/harvest?agent=<%= Rack::Utils.escape(id) %>"<%== ' aria-current="page"' if id == @agent %>><%= id %></a>
|
|
41
|
+
<% end %>
|
|
42
|
+
</div>
|
|
43
|
+
<% end %>
|
|
44
|
+
|
|
45
|
+
<%# The human's inbox. %>
|
|
46
|
+
<% if @candidates.any? %>
|
|
47
|
+
<h2>Awaiting approval<span class="sub">the human's answer</span></h2>
|
|
48
|
+
<% @candidates.each do |cand| %>
|
|
49
|
+
<div class="card">
|
|
50
|
+
<div class="skill-desc">
|
|
51
|
+
<span class="pill ok">gated</span>
|
|
52
|
+
<span class="mono"><%= cand.name %></span>
|
|
53
|
+
<span class="muted">· <%= cand.description %></span>
|
|
54
|
+
</div>
|
|
55
|
+
<% Array(cand.triggers).each do |t| %>
|
|
56
|
+
<span class="pill"><%= t %></span>
|
|
57
|
+
<% end %>
|
|
58
|
+
<div class="skill-desc muted">
|
|
59
|
+
store <span class="mono"><%= cand.agent %></span> · proposed by
|
|
60
|
+
<span class="mono"><%= cand.proposer %></span> · <%= (cand.eval_gate || {})["passed_cases"] %>/<%= (cand.eval_gate || {})["cases"] %> case(s) no regression
|
|
61
|
+
· conversion <%= (cand.conversion_gate || {})["current"] %> vs <%= (cand.conversion_gate || {})["baseline"] %> (threshold <%= (cand.conversion_gate || {})["threshold"] %>)
|
|
62
|
+
</div>
|
|
63
|
+
<% if (excerpt = candidate_excerpt(cand)).any? %>
|
|
64
|
+
<details class="skill-desc">
|
|
65
|
+
<summary>evidence excerpt</summary>
|
|
66
|
+
<% excerpt.each do |sid, idx, text| %>
|
|
67
|
+
<div class="preview muted">
|
|
68
|
+
<a class="mono" href="/studio/sessions/<%= Rack::Utils.escape(sid) %>"><%= sid %></a> [<%= idx %>]: <%= text %>
|
|
69
|
+
</div>
|
|
70
|
+
<% end %>
|
|
71
|
+
</details>
|
|
72
|
+
<% end %>
|
|
73
|
+
<% Array(cand.origin).each do |sid| %>
|
|
74
|
+
<a class="mono muted" href="/studio/sessions/<%= Rack::Utils.escape(sid) %>"><%= sid %></a>
|
|
75
|
+
<% end %>
|
|
76
|
+
<form method="post" action="/studio/harvest/promote" class="actions">
|
|
77
|
+
<%== csrf_tag %>
|
|
78
|
+
<input type="hidden" name="agent" value="<%= cand.agent %>">
|
|
79
|
+
<input type="hidden" name="candidate_id" value="<%= cand.id %>">
|
|
80
|
+
<label>
|
|
81
|
+
Note
|
|
82
|
+
<input type="text" name="note" placeholder="why (optional)">
|
|
83
|
+
</label>
|
|
84
|
+
<button type="submit" class="primary">Promote</button>
|
|
85
|
+
<button type="submit" class="ghost" formaction="/studio/harvest/reject">Reject</button>
|
|
86
|
+
</form>
|
|
87
|
+
<div class="skill-desc muted">
|
|
88
|
+
Promoting snapshots the pre-promotion state, writes the skill into this store's
|
|
89
|
+
scope, enables it on the allowlist and appends the promotion log row. Rollback
|
|
90
|
+
is the one-click mirror.
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<% end %>
|
|
94
|
+
<% end %>
|
|
95
|
+
|
|
96
|
+
<%# Gated-but-blocked: the ruler's hole, named. %>
|
|
97
|
+
<% if @blocked.any? %>
|
|
98
|
+
<h2>Blocked<span class="sub">gated, waiting on the ruler</span></h2>
|
|
99
|
+
<div class="card">
|
|
100
|
+
<ul class="history">
|
|
101
|
+
<% @blocked.each do |cand| %>
|
|
102
|
+
<li>
|
|
103
|
+
<span class="pill err">blocked</span>
|
|
104
|
+
<span class="mono"><%= cand.name %></span>
|
|
105
|
+
<span class="preview muted">
|
|
106
|
+
<% reason = (cand.conversion_gate || {})["reason"] %>
|
|
107
|
+
<% if reason == "no_frozen_baseline" %>
|
|
108
|
+
the store has no frozen funnel baseline — freeze it first on the
|
|
109
|
+
<a href="/studio/funnel">Funnel page</a>
|
|
110
|
+
<% elsif reason %>
|
|
111
|
+
<%= reason %>
|
|
112
|
+
<% else %>
|
|
113
|
+
<%= (cand.eval_gate || {})["reason"] %>
|
|
114
|
+
<% end %>
|
|
115
|
+
</span>
|
|
116
|
+
</li>
|
|
117
|
+
<% end %>
|
|
118
|
+
</ul>
|
|
119
|
+
</div>
|
|
120
|
+
<% end %>
|
|
121
|
+
|
|
122
|
+
<%# Mined, not yet gated. %>
|
|
123
|
+
<% if @pending_gates.any? %>
|
|
124
|
+
<h2>Pending<span class="sub">mined, awaiting the double gate</span></h2>
|
|
125
|
+
<div class="card">
|
|
126
|
+
<ul class="history">
|
|
127
|
+
<% @pending_gates.each do |cand| %>
|
|
128
|
+
<li>
|
|
129
|
+
<span class="pill">pending</span>
|
|
130
|
+
<span class="mono"><%= cand.name %></span>
|
|
131
|
+
<span class="preview muted"><%= cand.description %></span>
|
|
132
|
+
<form method="post" action="/studio/harvest/gate" class="actions inline">
|
|
133
|
+
<%== csrf_tag %>
|
|
134
|
+
<input type="hidden" name="agent" value="<%= cand.agent %>">
|
|
135
|
+
<input type="hidden" name="candidate_id" value="<%= cand.id %>">
|
|
136
|
+
<button type="submit" class="primary">Gate it</button>
|
|
137
|
+
<button type="submit" class="ghost" formaction="/studio/harvest/reject">Reject</button>
|
|
138
|
+
</form>
|
|
139
|
+
</li>
|
|
140
|
+
<% end %>
|
|
141
|
+
</ul>
|
|
142
|
+
</div>
|
|
143
|
+
<% end %>
|
|
144
|
+
|
|
145
|
+
<%# The promotion log — the single ledger, with the rollback mirror. %>
|
|
146
|
+
<% if @promotions.any? %>
|
|
147
|
+
<h2>Promoted<span class="sub">the append-only log</span></h2>
|
|
148
|
+
<div class="card">
|
|
149
|
+
<ul class="history">
|
|
150
|
+
<% @promotions.each do |row| %>
|
|
151
|
+
<li>
|
|
152
|
+
<% if row.rolled_back_at %>
|
|
153
|
+
<span class="pill err">rolled back</span>
|
|
154
|
+
<% else %>
|
|
155
|
+
<span class="pill ok">promoted</span>
|
|
156
|
+
<% end %>
|
|
157
|
+
<span class="mono"><%= row.skill %></span>
|
|
158
|
+
<span class="preview muted">
|
|
159
|
+
<%= row.approver %> · <%= row.at %>
|
|
160
|
+
<% if row.rolled_back_at %>· rolled back <%= row.rolled_back_at %><% end %>
|
|
161
|
+
</span>
|
|
162
|
+
<% unless row.rolled_back_at %>
|
|
163
|
+
<form method="post" action="/studio/harvest/rollback" class="actions inline">
|
|
164
|
+
<%== csrf_tag %>
|
|
165
|
+
<input type="hidden" name="agent" value="<%= row.agent %>">
|
|
166
|
+
<input type="hidden" name="snapshot_ref" value="<%= row.snapshot_ref %>">
|
|
167
|
+
<input type="text" name="reason" placeholder="why (optional)">
|
|
168
|
+
<button type="submit" class="ghost">Rollback</button>
|
|
169
|
+
</form>
|
|
170
|
+
<% end %>
|
|
171
|
+
</li>
|
|
172
|
+
<% end %>
|
|
173
|
+
</ul>
|
|
174
|
+
</div>
|
|
175
|
+
<% end %>
|
|
176
|
+
|
|
177
|
+
<%# The negative list block. %>
|
|
178
|
+
<% if @negative && @negative.rules.any? %>
|
|
179
|
+
<h2>Negative list<span class="sub">the versioned seed, engine-applied</span></h2>
|
|
180
|
+
<div class="card">
|
|
181
|
+
<ul class="history">
|
|
182
|
+
<% @negative.rules.each do |rule| %>
|
|
183
|
+
<li>
|
|
184
|
+
<span class="pill"><%= rule.rule %></span>
|
|
185
|
+
<span class="preview muted"><%= rule.pattern %><%= " — #{rule.note}" if presence(rule.note) %></span>
|
|
186
|
+
<span class="preview muted">rejected: <%= negative_rule_counts[rule.rule] || 0 %></span>
|
|
187
|
+
</li>
|
|
188
|
+
<% end %>
|
|
189
|
+
</ul>
|
|
190
|
+
<div class="skill-desc muted">
|
|
191
|
+
The list is pack data on the profile (hot-editable); the versioned
|
|
192
|
+
rules file seeds it via
|
|
193
|
+
<span class="mono">insika harvest:negative import --agent ID --file F</span>.
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
<% end %>
|
|
197
|
+
|
|
198
|
+
<%# The ruler block. %>
|
|
199
|
+
<% if @criterion %>
|
|
200
|
+
<h2>The conversion criterion<span class="sub">frozen before the first promotion</span></h2>
|
|
201
|
+
<div class="card">
|
|
202
|
+
<div class="skill-desc">
|
|
203
|
+
<span class="pill">metric <%= @criterion.rule.metric %></span>
|
|
204
|
+
<span class="pill">window <%= @criterion.rule.window %></span>
|
|
205
|
+
<span class="pill">not worse than <%= @criterion.rule.threshold %></span>
|
|
206
|
+
<span class="mono muted"><%= @criterion.sha %></span>
|
|
207
|
+
</div>
|
|
208
|
+
<div class="skill-desc muted">
|
|
209
|
+
The ruler is the frozen criterion file, read-only here; the
|
|
210
|
+
store's frozen baseline lives on the <a href="/studio/funnel">Funnel page</a>. A
|
|
211
|
+
store without one can mine, but the gate parks its candidates.
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
<% end %>
|
|
215
|
+
|
|
216
|
+
<% if @runs.any? %>
|
|
217
|
+
<h2>Runs<span class="sub">most recent first</span></h2>
|
|
218
|
+
<div class="card">
|
|
219
|
+
<ul class="history">
|
|
220
|
+
<% @runs.each do |run| %>
|
|
221
|
+
<li>
|
|
222
|
+
<span class="pill <%= run.status == 'failed' ? 'err' : 'ok' %>"><%= run.status %></span>
|
|
223
|
+
<span class="mono"><%= run.id[0, 12] %></span>
|
|
224
|
+
<span class="preview muted">
|
|
225
|
+
<%= run.candidates %> candidate(s) ·
|
|
226
|
+
<% if run.cost && run.cost["spent"].to_i.positive? %><%= run.cost["spent"] %> tokens · <% end %>
|
|
227
|
+
<%= run.started_at %>
|
|
228
|
+
</span>
|
|
229
|
+
</li>
|
|
230
|
+
<% end %>
|
|
231
|
+
</ul>
|
|
232
|
+
</div>
|
|
233
|
+
<% end %>
|
|
234
|
+
<% end %>
|
|
@@ -1,72 +1,112 @@
|
|
|
1
|
-
<%# Overview home
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
<a class="btn primary" href="/studio/playground">Open playground →</a>
|
|
11
|
-
</div>
|
|
12
|
-
</header>
|
|
13
|
-
|
|
14
|
-
<div class="stat-cards">
|
|
15
|
-
<% [["conversations", "Conversations"], ["messages", "Messages"], ["agents", "Agents"],
|
|
16
|
-
["skills", "Skills"], ["tools", "Tools"], ["providers", "LLM providers"]].each do |key, label| %>
|
|
17
|
-
<div class="stat-card">
|
|
18
|
-
<div class="stat-num"><%= @counts[key] %></div>
|
|
19
|
-
<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>
|
|
20
10
|
</div>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
<% n = @activity.size; slot = 700.0 / n; bw = slot * 0.58 %>
|
|
33
|
-
<% @activity.each_with_index do |(d, c), i| %>
|
|
34
|
-
<% h = (c.to_f / max) * 150.0; x = (i * slot) + ((slot - bw) / 2.0); y = 160.0 - h %>
|
|
35
|
-
<rect class="bar<%== ' bar-empty' if c.zero? %>" x="<%= format('%.1f', x) %>" y="<%= format('%.1f', c.zero? ? 157.0 : y) %>"
|
|
36
|
-
width="<%= format('%.1f', bw) %>" height="<%= format('%.1f', c.zero? ? 3.0 : [h, 3.0].max) %>" rx="2">
|
|
37
|
-
<title><%= d.strftime('%b %-d') %> · <%= c %> conversation<%= 's' unless c == 1 %></title>
|
|
38
|
-
</rect>
|
|
39
|
-
<% end %>
|
|
40
|
-
<line class="barchart-axis-line" x1="0" y1="160" x2="700" y2="160" />
|
|
41
|
-
</svg>
|
|
42
|
-
<div class="barchart-axis">
|
|
43
|
-
<span><%= @activity.first&.first&.strftime('%b %-d') %></span>
|
|
44
|
-
<span class="peak">peak <%= max %>/day</span>
|
|
45
|
-
<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>
|
|
46
22
|
</div>
|
|
47
|
-
</
|
|
23
|
+
</header>
|
|
48
24
|
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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 %>
|
|
68
108
|
<% end %>
|
|
69
109
|
</ul>
|
|
70
|
-
|
|
71
|
-
</
|
|
110
|
+
</section>
|
|
111
|
+
</div>
|
|
72
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 "<%= @selected %>"? 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,12 +3,27 @@
|
|
|
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>
|
|
13
|
+
<link rel="icon" type="image/svg+xml" href="<%= asset_path("favicon.svg") %>">
|
|
8
14
|
<link rel="stylesheet" href="<%= asset_path("application.css") %>">
|
|
9
15
|
<script type="module" src="<%= asset_path("application.js") %>" defer></script>
|
|
10
16
|
</head>
|
|
11
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>
|
|
12
27
|
<% if authenticated? %>
|
|
13
28
|
<div class="app-shell">
|
|
14
29
|
<aside class="sidebar">
|
|
@@ -59,17 +74,6 @@
|
|
|
59
74
|
</aside>
|
|
60
75
|
|
|
61
76
|
<div class="app-main">
|
|
62
|
-
<% if restart_needed? %>
|
|
63
|
-
<div class="banner" role="status">
|
|
64
|
-
<span><strong>Restart recommended.</strong> MCP changes take effect after the runtime restarts.</span>
|
|
65
|
-
<form method="post" action="/studio/restart-ack">
|
|
66
|
-
<%== csrf_tag %>
|
|
67
|
-
<input type="hidden" name="back" value="<%= request.script_name %><%= request.path %>">
|
|
68
|
-
<button type="submit" class="ghost">Dismiss</button>
|
|
69
|
-
</form>
|
|
70
|
-
</div>
|
|
71
|
-
<% end %>
|
|
72
|
-
|
|
73
77
|
<% if flash["error"] %>
|
|
74
78
|
<div class="flash err" role="alert"><%= flash["error"] %></div>
|
|
75
79
|
<% end %>
|