Give the AI subsystem one profile table and one collection, read everywhere
This commit is contained in:
@@ -404,6 +404,11 @@ if ($can('conf_lookup')) {
|
|||||||
// Varaverk question as chat produces a confident invention about the user's system, which is
|
// Varaverk question as chat produces a confident invention about the user's system, which is
|
||||||
// precisely what retrieval exists to prevent. The user always knows which contract is in force,
|
// precisely what retrieval exists to prevent. The user always knows which contract is in force,
|
||||||
// and the strict profile is the default.
|
// and the strict profile is the default.
|
||||||
|
//
|
||||||
|
// These stay here rather than in include/ai_profiles.php with the rest of what a profile is.
|
||||||
|
// That file exists to kill duplication, and these prompts have exactly one reader — moving them
|
||||||
|
// would relocate the most delicate text in the subsystem without removing a single copy of
|
||||||
|
// anything. The registry owns the ids; the ids branch here. The trailing else is varaverk.
|
||||||
if ($profile === 'chat') {
|
if ($profile === 'chat') {
|
||||||
// A polite instruction is not a guard. Asked "what does mover_stop.sh do in my setup", the
|
// A polite instruction is not a guard. Asked "what does mover_stop.sh do in my setup", the
|
||||||
// model invented an answer and dressed it in real memory facts so it read as authoritative.
|
// model invented an answer and dressed it in real memory facts so it read as authoritative.
|
||||||
@@ -652,7 +657,7 @@ $messages[] = ['role' => 'user', 'content' => $question];
|
|||||||
// them, which is why the report prints here: it describes the request that is about to be made,
|
// them, which is why the report prints here: it describes the request that is about to be made,
|
||||||
// not a reconstruction of one.
|
// not a reconstruction of one.
|
||||||
if ($explain) {
|
if ($explain) {
|
||||||
$caps = array_keys(array_filter(VV_AI_CAPS, fn($ps) => in_array($profile, $ps, true)));
|
$caps = vv_ai_profile_caps($profile);
|
||||||
$sysChars = strlen($system);
|
$sysChars = strlen($system);
|
||||||
|
|
||||||
echo "QUESTION ", $question, "\n";
|
echo "QUESTION ", $question, "\n";
|
||||||
|
|||||||
@@ -91,19 +91,23 @@ $t = microtime(true);
|
|||||||
// Call vv_api_data() once — result is static-cached for the rest of this process.
|
// Call vv_api_data() once — result is static-cached for the rest of this process.
|
||||||
vv_api_data();
|
vv_api_data();
|
||||||
|
|
||||||
// Must stay in step with api/monitor.php's own block. This file is what the Monitor tab
|
// ── AI ────────────────────────────────────────────────────────────────────────
|
||||||
// normally reads — the endpoint only assembles a payload on a cache miss — so a key added there
|
// One collection, two consumers. vv_ai_stats() is the expensive part of the AI subsystem —
|
||||||
// and not here leaves the card that consumes it loading forever on every ordinary page load,
|
// roughly a second, most of it waiting on Ollama and nvidia-smi — and it is written to its own
|
||||||
// and working on the one request that happens to miss the cache.
|
// cache here so the AI tab's banner, the Scheduler dock and the Monitor row all read the same
|
||||||
|
// numbers from the same moment instead of each paying for their own.
|
||||||
|
//
|
||||||
|
// The monitor block is derived from that same array rather than collected again. Must stay in
|
||||||
|
// step with api/monitor.php's own block: this file is what the Monitor tab normally reads, since
|
||||||
|
// the endpoint only assembles a payload on a cache miss, so a key added there and not here
|
||||||
|
// leaves the card that consumes it loading forever on every ordinary page load and working only
|
||||||
|
// on the one request that happens to miss.
|
||||||
$_vv_ai = null;
|
$_vv_ai = null;
|
||||||
if (vv_ai_ui_on()) {
|
if (vv_ai_ui_on()) {
|
||||||
require_once $_base . '/include/ai.php';
|
require_once $_base . '/include/ai.php';
|
||||||
$_vv_ai = [
|
$_vv_ai_stats = vv_ai_stats();
|
||||||
'model' => vv_ai_config()['model'],
|
vv_cache_write('ai', $_vv_ai_stats);
|
||||||
'runtime' => vv_ai_runtime_stats(),
|
$_vv_ai = vv_ai_monitor_block($_vv_ai_stats);
|
||||||
'index' => vv_ai_index_stats(),
|
|
||||||
'tokens' => vv_ai_token_stats()['today'] ?? null,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$monitor = [
|
$monitor = [
|
||||||
|
|||||||
+16
-13
@@ -102,14 +102,9 @@ header('Content-Type: application/json');
|
|||||||
header('Cache-Control: no-store, no-cache');
|
header('Cache-Control: no-store, no-cache');
|
||||||
require_once dirname(__DIR__) . '/include/ai.php';
|
require_once dirname(__DIR__) . '/include/ai.php';
|
||||||
|
|
||||||
// History depth is per profile, and decided here rather than by the page. Varaverk Assistant
|
// History depth is per profile and still decided server-side rather than by the page — the page
|
||||||
// spends ~2500 of its 16384 on retrieved passages, so it cannot afford deep history; the other
|
// simply no longer carries a second copy of the numbers. include/ai_profiles.php holds them.
|
||||||
// two retrieve nothing and can carry a real conversation. Reasoning is not stored in history,
|
// Reasoning is not stored in history, so it does not compound.
|
||||||
// so it does not compound.
|
|
||||||
// troubleshoot carries a whole log tail into context, so its history is the shallowest of the
|
|
||||||
// four — the evidence for "why did this fail" is the log in front of it, not the conversation.
|
|
||||||
const VV_AI_PROFILES = ['varaverk' => 3, 'chat' => 8, 'code' => 4, 'troubleshoot' => 2];
|
|
||||||
const VV_AI_MAX_TURNS = 3; // fallback when a profile is not recognised
|
|
||||||
const VV_AI_MAX_QUESTION = 4000; // characters
|
const VV_AI_MAX_QUESTION = 4000; // characters
|
||||||
const VV_AI_MAX_HIST_MSG = 4000; // characters per retained message
|
const VV_AI_MAX_HIST_MSG = 4000; // characters per retained message
|
||||||
const VV_AI_JOB_TTL = 3600; // seconds before a job file is reaped
|
const VV_AI_JOB_TTL = 3600; // seconds before a job file is reaped
|
||||||
@@ -153,8 +148,16 @@ if (!vv_ai_enabled()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── stats ─────────────────────────────────────────────────────────────────────
|
// ── stats ─────────────────────────────────────────────────────────────────────
|
||||||
|
// Served from the shared 'ai' cache that Tools/api_cache_writer.sh refreshes every minute, on
|
||||||
|
// the same terms as the monitor and arrs payloads. This action is polled every 30 seconds by
|
||||||
|
// every open tab and used to pay a full collection each time — around a second, most of it spent
|
||||||
|
// waiting on Ollama and nvidia-smi — for numbers that only change when the writer next runs.
|
||||||
|
//
|
||||||
|
// ?live=1 bypasses it, for the case where something was just changed and the point is to see the
|
||||||
|
// result. A missing cache always falls back to collecting, so the cache can never be the reason
|
||||||
|
// the banner fails to render.
|
||||||
if ($action === 'stats') {
|
if ($action === 'stats') {
|
||||||
echo json_encode(['ok' => true, 'stats' => vv_ai_stats()]);
|
echo json_encode(['ok' => true, 'stats' => vv_ai_stats_cached(isset($_GET['live']))]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,7 +235,7 @@ if ($action === 'chat_save') {
|
|||||||
if (!$isPost) { http_response_code(405); echo json_encode(['ok' => false, 'error' => 'POST only']); exit; }
|
if (!$isPost) { http_response_code(405); echo json_encode(['ok' => false, 'error' => 'POST only']); exit; }
|
||||||
|
|
||||||
$profile = trim($_POST['profile'] ?? 'chat');
|
$profile = trim($_POST['profile'] ?? 'chat');
|
||||||
if (!isset(VV_AI_PROFILES[$profile])) {
|
if (!vv_ai_profile_ok($profile)) {
|
||||||
echo json_encode(['ok' => false, 'error' => 'Unknown profile: ' . $profile]); exit;
|
echo json_encode(['ok' => false, 'error' => 'Unknown profile: ' . $profile]); exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,7 +253,7 @@ if ($action === 'chat_save') {
|
|||||||
// saved under one profile can be reopened under another, and the reopened turn is trimmed
|
// saved under one profile can be reopened under another, and the reopened turn is trimmed
|
||||||
// again on the way back out by ask — so storing a little more than any single profile will
|
// again on the way back out by ask — so storing a little more than any single profile will
|
||||||
// send costs nothing and keeps the transcript readable.
|
// send costs nothing and keeps the transcript readable.
|
||||||
$cap = max(VV_AI_PROFILES) * 2;
|
$cap = vv_ai_profiles_max_turns() * 2;
|
||||||
if (count($clean) > $cap) $clean = array_slice($clean, -$cap);
|
if (count($clean) > $cap) $clean = array_slice($clean, -$cap);
|
||||||
|
|
||||||
$r = vv_ai_chat_save(trim($_POST['id'] ?? ''), $profile, $clean);
|
$r = vv_ai_chat_save(trim($_POST['id'] ?? ''), $profile, $clean);
|
||||||
@@ -308,10 +311,10 @@ if ($action === 'ask') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$profile = trim($_POST['profile'] ?? 'varaverk');
|
$profile = trim($_POST['profile'] ?? 'varaverk');
|
||||||
if (!isset(VV_AI_PROFILES[$profile])) {
|
if (!vv_ai_profile_ok($profile)) {
|
||||||
echo json_encode(['ok' => false, 'error' => 'Unknown profile: ' . $profile]); exit;
|
echo json_encode(['ok' => false, 'error' => 'Unknown profile: ' . $profile]); exit;
|
||||||
}
|
}
|
||||||
$maxTurns = VV_AI_PROFILES[$profile] ?? VV_AI_MAX_TURNS;
|
$maxTurns = vv_ai_profile_turns($profile);
|
||||||
|
|
||||||
// Where the caller is standing — "master.conf", "daily_sync_maintenance.sh", a log name.
|
// Where the caller is standing — "master.conf", "daily_sync_maintenance.sh", a log name.
|
||||||
// The scheduler page sends it so a question can say "this setting" and mean something; the
|
// The scheduler page sends it so a question can say "this setting" and mean something; the
|
||||||
|
|||||||
@@ -100,15 +100,14 @@ vv_api_data();
|
|||||||
//
|
//
|
||||||
// Null on any host that is not the AI host or has AI_ENABLED false, which is also what makes the
|
// Null on any host that is not the AI host or has AI_ENABLED false, which is also what makes the
|
||||||
// row absent rather than empty there. Same shape as every other optional subsystem on this page.
|
// row absent rather than empty there. Same shape as every other optional subsystem on this page.
|
||||||
|
// Reads the shared 'ai' cache the writer maintains and only collects on a miss. Landing here at
|
||||||
|
// all already means the monitor cache missed; paying a second full AI collection on top of that
|
||||||
|
// would make the slowest request on this page slower still, for figures a background writer
|
||||||
|
// refreshed under a minute ago.
|
||||||
$_vv_ai = null;
|
$_vv_ai = null;
|
||||||
if (vv_ai_ui_on()) {
|
if (vv_ai_ui_on()) {
|
||||||
require_once dirname(__DIR__) . '/include/ai.php';
|
require_once dirname(__DIR__) . '/include/ai.php';
|
||||||
$_vv_ai = [
|
$_vv_ai = vv_ai_monitor_block(vv_ai_stats_cached(isset($_GET['live'])));
|
||||||
'model' => vv_ai_config()['model'],
|
|
||||||
'runtime' => vv_ai_runtime_stats(),
|
|
||||||
'index' => vv_ai_index_stats(),
|
|
||||||
'tokens' => vv_ai_token_stats()['today'] ?? null,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
|
|||||||
@@ -82,48 +82,10 @@ require_once __DIR__ . '/config.php';
|
|||||||
define('VV_AI_JOB_DIR', '/tmp/varaverk_ai_jobs');
|
define('VV_AI_JOB_DIR', '/tmp/varaverk_ai_jobs');
|
||||||
const VV_AI_KINDS = ['header', 'readme', 'manual', 'template', 'doc'];
|
const VV_AI_KINDS = ['header', 'readme', 'manual', 'template', 'doc'];
|
||||||
|
|
||||||
// ── What each profile is allowed to see and do ───────────────────────────────────────────────
|
// Profiles — what each one is, and what it is allowed to see and do. One table, in one file,
|
||||||
// A profile is a contract plus a set of inputs, and the inputs are the half that has to be
|
// read by everything: this endpoint, the worker, the shared chat include and the Scheduler dock.
|
||||||
// enforced rather than requested. This table is that half, in one place.
|
// vv_ai_profile_can() and friends come from there.
|
||||||
//
|
require_once __DIR__ . '/ai_profiles.php';
|
||||||
// It exists because the alternative already failed. The same permissions used to live as a dozen
|
|
||||||
// `$profile === 'varaverk' || $profile === 'troubleshoot'` conditions spread across the worker,
|
|
||||||
// and answering "may chat ever be shown a log?" meant reading all of them. It could — a gate
|
|
||||||
// added for run-outcome questions granted it by omission, and the chat profile, whose entire
|
|
||||||
// value is that it has NOT been shown this installation, was one phrasing away from being handed
|
|
||||||
// a health sweep and 120 lines of log. Nothing about that was visible at the point of the
|
|
||||||
// mistake. Here it would have been one missing word on one line.
|
|
||||||
//
|
|
||||||
// A capability is permission, not need. varaverk holds 'health' but only attaches it when the
|
|
||||||
// question looks diagnostic; troubleshoot attaches it always. The gates decide whether an input
|
|
||||||
// is warranted, this decides whether it is allowed, and a gate can never widen the grant.
|
|
||||||
//
|
|
||||||
// The ordering is deliberate: chat holds nothing, and that emptiness is a guarantee, not an
|
|
||||||
// oversight. Anything added to it stops being general chat and becomes an assistant that
|
|
||||||
// sometimes lies about this installation.
|
|
||||||
const VV_AI_CAPS = [
|
|
||||||
// retrieval passages from the index, and the kind filter the page exposes for them
|
|
||||||
'retrieve' => ['varaverk', 'troubleshoot'],
|
|
||||||
'kind_filter' => ['varaverk'],
|
|
||||||
// live health sweep measured at question time
|
|
||||||
'health' => ['varaverk', 'troubleshoot'],
|
|
||||||
// run record + log tail for a script named in the question
|
|
||||||
'run_evidence' => ['varaverk', 'troubleshoot'],
|
|
||||||
// log tail for whatever the operator currently has open
|
|
||||||
'scoped_log' => ['troubleshoot'],
|
|
||||||
// operator-written history of what previously went wrong with this thing
|
|
||||||
'incidents' => ['varaverk', 'troubleshoot'],
|
|
||||||
// deterministic "where does this conf key actually live" lookup
|
|
||||||
'conf_lookup' => ['varaverk', 'troubleshoot'],
|
|
||||||
// may file a bug report against Varaverk itself
|
|
||||||
'file_bugs' => ['troubleshoot'],
|
|
||||||
// destructive-operation scan of generated shell
|
|
||||||
'code_scan' => ['code'],
|
|
||||||
];
|
|
||||||
|
|
||||||
function vv_ai_profile_can(string $profile, string $cap): bool {
|
|
||||||
return in_array($profile, VV_AI_CAPS[$cap] ?? [], true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Whether a General Chat message is really about this installation. Shared by the deterministic
|
// Whether a General Chat message is really about this installation. Shared by the deterministic
|
||||||
// backstop and the handoff, so both agree by construction: a question the backstop would have
|
// backstop and the handoff, so both agree by construction: a question the backstop would have
|
||||||
@@ -472,6 +434,43 @@ function vv_ai_stats(): array {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Shared collection ─────────────────────────────────────────────────────────
|
||||||
|
// vv_ai_stats() costs about a second on this host — vv_ai_runtime_stats() alone is 60-480ms
|
||||||
|
// depending on how quickly Ollama and nvidia-smi answer, and it was being paid by the AI tab
|
||||||
|
// every 30 seconds per open tab, plus again by anything else that wanted the same numbers.
|
||||||
|
//
|
||||||
|
// So it is collected once, by Tools/api_cache_writer.php, into the 'ai' cache; every surface
|
||||||
|
// reads that. This is the same arrangement the monitor and arrs payloads already use and for the
|
||||||
|
// same reason — polling faster cannot make the figures newer, it only decides how soon a page
|
||||||
|
// notices the writer's update.
|
||||||
|
//
|
||||||
|
// ?live=1 stays available for the one case that needs it: you changed something and want to see
|
||||||
|
// the result rather than a payload written before you changed it.
|
||||||
|
function vv_ai_stats_cached(bool $live = false): array {
|
||||||
|
if (!$live) {
|
||||||
|
$c = vv_cache_read('ai', 300);
|
||||||
|
if ($c !== null) return $c;
|
||||||
|
}
|
||||||
|
return vv_ai_stats();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Monitor tab's slice of the same collection. Derived rather than collected: taking the AI
|
||||||
|
// row's figures from a second call to vv_ai_runtime_stats() would pay the whole cost twice per
|
||||||
|
// cache write, and — worse — the dashboard and the AI tab could disagree about whether the model
|
||||||
|
// is resident, because they would have asked at different moments.
|
||||||
|
//
|
||||||
|
// Tokens are not part of vv_ai_stats(): that function's shape is the AI tab's banner contract,
|
||||||
|
// and the ledger read is 15ms, so it is fetched here rather than widening the payload everything
|
||||||
|
// else carries.
|
||||||
|
function vv_ai_monitor_block(array $stats): array {
|
||||||
|
return [
|
||||||
|
'model' => $stats['model'] ?? '',
|
||||||
|
'runtime' => $stats['runtime'] ?? [],
|
||||||
|
'index' => $stats['index'] ?? [],
|
||||||
|
'tokens' => vv_ai_token_stats()['today'] ?? null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// Retrieval via AI/lib/cli.js. Returns ['ok'=>bool,'results'=>[],'intents'=>[],'error'=>?string].
|
// Retrieval via AI/lib/cli.js. Returns ['ok'=>bool,'results'=>[],'intents'=>[],'error'=>?string].
|
||||||
function vv_ai_retrieve(string $query, string $kind = '', string $section = '', ?int $k = null): array {
|
function vv_ai_retrieve(string $query, string $kind = '', string $section = '', ?int $k = null): array {
|
||||||
$cfg = vv_ai_config();
|
$cfg = vv_ai_config();
|
||||||
|
|||||||
@@ -57,10 +57,16 @@
|
|||||||
// vv_ai_chat_list_markup($prefix) the stored-conversations list container
|
// vv_ai_chat_list_markup($prefix) the stored-conversations list container
|
||||||
//
|
//
|
||||||
// DEPENDS ON
|
// DEPENDS ON
|
||||||
// api/ai.php ask / poll / clear / chats / chat_get / chat_save / chat_delete
|
// include/ai_profiles.php the profile registry, served to the browser rather than restated
|
||||||
// api/readscript.php source viewer contents
|
// api/ai.php ask / poll / clear / chats / chat_get / chat_save / chat_delete
|
||||||
|
// api/readscript.php source viewer contents
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// Required directly, not assumed. The Monitor tab pulls this file in without include/ai.php,
|
||||||
|
// so relying on something else having loaded the registry first works on the AI tab and fatals
|
||||||
|
// on the dashboard.
|
||||||
|
require_once __DIR__ . '/ai_profiles.php';
|
||||||
|
|
||||||
// Emitted once even if two instances are rendered. A second copy of the script would re-register
|
// Emitted once even if two instances are rendered. A second copy of the script would re-register
|
||||||
// the factories harmlessly but would also install a second Escape handler and a second copy of
|
// the factories harmlessly but would also install a second Escape handler and a second copy of
|
||||||
// every keyframe, so the guard is cheaper than reasoning about whether it matters.
|
// every keyframe, so the guard is cheaper than reasoning about whether it matters.
|
||||||
@@ -145,18 +151,41 @@ function vv_ai_chat_assets(): void {
|
|||||||
.vv-ai-hint { font-size:10px; color:#3a3a3a; margin-left:auto; }
|
.vv-ai-hint { font-size:10px; color:#3a3a3a; margin-left:auto; }
|
||||||
|
|
||||||
/* ── Compact form, for a chat living inside a Monitor card ──────────────── */
|
/* ── Compact form, for a chat living inside a Monitor card ──────────────── */
|
||||||
/* Not a different component — the same markup with the chrome pulled in. The card supplies its
|
/* Not a different component — the same markup with the chrome pulled in. The card supplies the
|
||||||
own heading and border, so the transcript drops its own and the hint text goes away rather
|
heading and the border, so the transcript drops its own and the hint text goes away rather
|
||||||
than wrapping to three lines at this width. */
|
than wrapping to three lines at this width.
|
||||||
.vv-ai-c .vv-ai-chat { border:none; border-radius:0; padding:10px 2px; min-height:0; }
|
|
||||||
|
Surfaces are re-based on the card, not merely un-bordered. The standalone palette paints a
|
||||||
|
#0b0b0b transcript because on the AI tab it sits on the page ground with nothing beside it
|
||||||
|
to compare against; dropped into a .vv-card (#1e1e1e) that same fill reads as a hole cut in
|
||||||
|
the card rather than as part of it. Transparent here, and the remaining controls move to the
|
||||||
|
values the plugin already uses inside a card — .vv-btn-sm is #2a2a2a on #555 — so the whole
|
||||||
|
thing reads as one object.
|
||||||
|
|
||||||
|
This is the second hardcoded dark palette in the plugin, which is exactly one too many. When
|
||||||
|
theming happens it wants tokens (--vv-surface, --vv-line) defined once in varaverk.css, and
|
||||||
|
this block becomes a token swap instead of a second set of literals. */
|
||||||
|
.vv-ai-c .vv-ai-chat { border:none; border-radius:0; padding:10px 2px; min-height:0;
|
||||||
|
background:transparent; }
|
||||||
.vv-ai-c .vv-ai-empty { padding:26px 14px; }
|
.vv-ai-c .vv-ai-empty { padding:26px 14px; }
|
||||||
.vv-ai-c .vv-ai-composer { border:none; padding:8px 0 0; background:none; }
|
.vv-ai-c .vv-ai-composer { border:none; padding:8px 0 0; background:none; }
|
||||||
.vv-ai-c .vv-ai-input { min-height:44px; font-size:12px; padding:7px; }
|
.vv-ai-c .vv-ai-input { min-height:44px; font-size:12px; padding:7px;
|
||||||
.vv-ai-c .vv-ai-prof { padding:3px 9px; font-size:10px; }
|
background:#161616; border-color:#333; }
|
||||||
|
.vv-ai-c .vv-ai-input:focus { border-color:#6495ed; }
|
||||||
|
.vv-ai-c .vv-ai-prof { padding:3px 9px; font-size:10px; background:#2a2a2a;
|
||||||
|
border-color:#555; color:#ccc; }
|
||||||
|
.vv-ai-c .vv-ai-prof:hover { border-color:#888; color:#fff; }
|
||||||
|
.vv-ai-c .vv-ai-prof.active { background:#152238; border-color:#4a7ab0; color:#9bd; }
|
||||||
.vv-ai-c .vv-ai-prof-hint { display:none; }
|
.vv-ai-c .vv-ai-prof-hint { display:none; }
|
||||||
.vv-ai-c .vv-ai-hint { display:none; }
|
.vv-ai-c .vv-ai-hint { display:none; }
|
||||||
.vv-ai-c .vv-ai-btn { padding:4px 11px; font-size:11px; }
|
.vv-ai-c .vv-ai-btn { padding:4px 11px; font-size:11px; }
|
||||||
|
.vv-ai-c .vv-ai-btn.ghost { border-color:#555; color:#ccc; }
|
||||||
.vv-ai-c .vv-ai-msg { margin-bottom:12px; }
|
.vv-ai-c .vv-ai-msg { margin-bottom:12px; }
|
||||||
|
.vv-ai-c .vv-ai-think { background:#161616; border-left-color:#3a3a3a; }
|
||||||
|
.vv-ai-c .vv-ai-body pre { background:#161616; border-color:#3a3a3a; }
|
||||||
|
.vv-ai-c .vv-ai-body code { background:#2a2a2a; }
|
||||||
|
.vv-ai-c .vv-ai-src { border-top-color:#333; }
|
||||||
|
.vv-ai-c .vv-ai-switch { border-top-color:#333; }
|
||||||
|
|
||||||
/* ── Stored conversations ───────────────────────────────────────────────── */
|
/* ── Stored conversations ───────────────────────────────────────────────── */
|
||||||
.vv-ai-clist { display:flex; flex-direction:column; gap:1px; }
|
.vv-ai-clist { display:flex; flex-direction:column; gap:1px; }
|
||||||
@@ -171,6 +200,14 @@ function vv_ai_chat_assets(): void {
|
|||||||
.vv-ai-crow-x { font-size:11px; color:#333; flex-shrink:0; padding:0 2px; visibility:hidden; }
|
.vv-ai-crow-x { font-size:11px; color:#333; flex-shrink:0; padding:0 2px; visibility:hidden; }
|
||||||
.vv-ai-crow:hover .vv-ai-crow-x { visibility:visible; }
|
.vv-ai-crow:hover .vv-ai-crow-x { visibility:visible; }
|
||||||
.vv-ai-crow-x:hover { color:#e57; }
|
.vv-ai-crow-x:hover { color:#e57; }
|
||||||
|
/* Hover lifts off the card rather than sinking into it. #141414 is a highlight against the AI
|
||||||
|
tab's #0e0e0e panel and a shadow against a #1e1e1e card — the same value reads as the
|
||||||
|
opposite gesture depending on what it sits on. */
|
||||||
|
.vv-ai-c .vv-ai-crow:hover { background:#2a2a2a; }
|
||||||
|
.vv-ai-c .vv-ai-crow-t { color:#ccc; }
|
||||||
|
.vv-ai-c .vv-ai-crow-m { color:#666; }
|
||||||
|
.vv-ai-c .vv-ai-crow-x { color:#666; }
|
||||||
|
.vv-ai-c .vv-ai-none { color:#666; }
|
||||||
.vv-ai-chead { display:flex; align-items:center; gap:8px; margin-bottom:5px; }
|
.vv-ai-chead { display:flex; align-items:center; gap:8px; margin-bottom:5px; }
|
||||||
|
|
||||||
/* ── Source overlay ─────────────────────────────────────────────────────── */
|
/* ── Source overlay ─────────────────────────────────────────────────────── */
|
||||||
@@ -195,21 +232,21 @@ function vv_ai_chat_assets(): void {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// The profile registry, served rather than restated. This used to be a literal table in the
|
||||||
|
// script below that had already drifted from the PHP — it knew three profiles where the server
|
||||||
|
// knew four, which is why troubleshoot could not be offered here and the Scheduler dock had to
|
||||||
|
// hand-roll its own labels.
|
||||||
|
vv_ai_profiles_script();
|
||||||
|
?>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
const API = '/plugins/varaverk/api/ai.php';
|
const API = '/plugins/varaverk/api/ai.php';
|
||||||
|
|
||||||
// Server-side is the authority on retrieval depth and history; these are for the UI only, and
|
// The server is the authority. It re-derives depth and capability on every request regardless
|
||||||
// they must not drift from VV_AI_PROFILES in api/ai.php.
|
// of what is here — these values draw buttons and decide whether to show the kind filter, they
|
||||||
const PROFILES = {
|
// do not make policy.
|
||||||
varaverk: { label: 'Varaverk Assistant', turns: 3, kind: true,
|
const PROFILES = window.VvAiProfiles;
|
||||||
hint: 'Answers only from Varaverk\'s own docs, with sources. Says so when they don\'t cover it.' },
|
|
||||||
chat: { label: 'General Chat', turns: 8, kind: false,
|
|
||||||
hint: 'Ordinary conversation. Hands anything about this install to the assistant on its own.' },
|
|
||||||
code: { label: 'Code Sketcher', turns: 4, kind: false,
|
|
||||||
hint: 'Drafts short scripts for Custom Scripts. First drafts — test before trusting.' },
|
|
||||||
};
|
|
||||||
window.VvAiProfiles = PROFILES;
|
|
||||||
|
|
||||||
const esc = s => String(s == null ? '' : s)
|
const esc = s => String(s == null ? '' : s)
|
||||||
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
||||||
@@ -678,9 +715,9 @@ function vv_ai_chat_markup(string $prefix, array $o = []): void {
|
|||||||
<div class="vv-ai-chatwrap<?= $compact ? ' vv-ai-c' : '' ?>" style="display:flex;flex-direction:column;gap:<?= $compact ? '4px' : '12px' ?>;min-width:0;">
|
<div class="vv-ai-chatwrap<?= $compact ? ' vv-ai-c' : '' ?>" style="display:flex;flex-direction:column;gap:<?= $compact ? '4px' : '12px' ?>;min-width:0;">
|
||||||
|
|
||||||
<div class="vv-ai-profiles" id="<?= $p ?>-profiles">
|
<div class="vv-ai-profiles" id="<?= $p ?>-profiles">
|
||||||
<?php foreach (['varaverk' => 'Varaverk Assistant', 'chat' => 'General Chat',
|
<?php foreach (vv_ai_profiles_ui() as $key => $def): ?>
|
||||||
'code' => 'Code Sketcher'] as $key => $label): ?>
|
<button class="vv-ai-prof" data-prof="<?= htmlspecialchars($key, ENT_QUOTES) ?>" type="button"
|
||||||
<button class="vv-ai-prof" data-prof="<?= $key ?>" type="button"><?= $label ?></button>
|
title="<?= htmlspecialchars($def['hint'], ENT_QUOTES) ?>"><?= htmlspecialchars($def['label']) ?></button>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<span class="vv-ai-prof-hint" id="<?= $p ?>-prof-hint"></span>
|
<span class="vv-ai-prof-hint" id="<?= $p ?>-prof-hint"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -705,9 +742,12 @@ function vv_ai_chat_markup(string $prefix, array $o = []): void {
|
|||||||
|
|
||||||
// The stored-conversations container. Rendered separately from the chat because the two live in
|
// The stored-conversations container. Rendered separately from the chat because the two live in
|
||||||
// different cards on Monitor and in different parts of the column on the AI tab.
|
// different cards on Monitor and in different parts of the column on the AI tab.
|
||||||
function vv_ai_chat_list_markup(string $prefix): void {
|
//
|
||||||
|
// Takes compact for the same reason the chat does: these rows are painted against whatever they
|
||||||
|
// sit on, and a card and a panel are not the same ground.
|
||||||
|
function vv_ai_chat_list_markup(string $prefix, bool $compact = false): void {
|
||||||
$p = htmlspecialchars($prefix, ENT_QUOTES);
|
$p = htmlspecialchars($prefix, ENT_QUOTES);
|
||||||
?>
|
?>
|
||||||
<div id="<?= $p ?>-chats"><div class="vv-ai-none">loading…</div></div>
|
<div id="<?= $p ?>-chats"<?= $compact ? ' class="vv-ai-c"' : '' ?>><div class="vv-ai-none">loading…</div></div>
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
<?php
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// PURPOSE
|
||||||
|
// The one definition of what an AI profile is. Every consumer — the endpoint, the worker, the
|
||||||
|
// shared chat include, the Scheduler dock — reads it from here instead of restating it.
|
||||||
|
//
|
||||||
|
// WHY THIS EXISTS
|
||||||
|
// A profile used to be defined in five places: history depth in api/ai.php, capabilities in
|
||||||
|
// include/ai.php, label/hint/depth again in the chat's JavaScript, a prompt branch in the
|
||||||
|
// worker, and a label map in pages/scheduler.php. They had already drifted — the JavaScript
|
||||||
|
// knew three profiles where PHP knew four, so the shared chat could not offer troubleshoot at
|
||||||
|
// all and the Scheduler dock hand-rolled its own labels to compensate. The include carried a
|
||||||
|
// comment telling the next person not to let the two tables diverge, which is a comment doing
|
||||||
|
// a data structure's job.
|
||||||
|
//
|
||||||
|
// WHAT LIVES HERE, AND WHAT DELIBERATELY DOES NOT
|
||||||
|
// Here: anything more than one file needs to agree on — the set of profiles, their labels and
|
||||||
|
// hints, history depth, capabilities, and whether a profile is offered as a button.
|
||||||
|
//
|
||||||
|
// Not here: the system prompts. They are long, delicate, and have exactly one consumer, so
|
||||||
|
// moving them would be churn against the most sensitive text in the subsystem for no reduction
|
||||||
|
// in duplication. Tools/ai_chat_worker.php still owns them; it just keys off ids validated
|
||||||
|
// here rather than an if-chain that invents its own vocabulary.
|
||||||
|
//
|
||||||
|
// CAPABILITIES ARE PER PROFILE, NOT PER CAPABILITY
|
||||||
|
// The old table was inverted — capability => [profiles] — which reads well when adding a
|
||||||
|
// capability and badly when answering the question actually asked at runtime, which is always
|
||||||
|
// "what can this profile do". Same content, turned the right way round.
|
||||||
|
//
|
||||||
|
// A profile is a contract plus a set of inputs, and the inputs are the half that has to be
|
||||||
|
// enforced rather than requested. The caps list is that half.
|
||||||
|
//
|
||||||
|
// It exists because the alternative already failed. The same permissions used to live as a
|
||||||
|
// dozen `$profile === 'varaverk' || $profile === 'troubleshoot'` conditions spread across the
|
||||||
|
// worker, and answering "may chat ever be shown a log?" meant reading all of them. It could —
|
||||||
|
// a gate added for run-outcome questions granted it by omission, and the chat profile, whose
|
||||||
|
// entire value is that it has NOT been shown this installation, was one phrasing away from
|
||||||
|
// being handed a health sweep and 120 lines of log. Nothing about that was visible at the
|
||||||
|
// point of the mistake. Here it would have been one missing word on one line.
|
||||||
|
//
|
||||||
|
// A capability is permission, not need. varaverk holds 'health' but only attaches it when the
|
||||||
|
// question looks diagnostic; troubleshoot attaches it always. The gates decide whether an
|
||||||
|
// input is warranted, this decides whether it is allowed, and a gate can never widen the grant.
|
||||||
|
//
|
||||||
|
// chat holding an empty capability list is a guarantee, not an oversight. Anything added to it
|
||||||
|
// stops being general chat and becomes an assistant that sometimes lies about this
|
||||||
|
// installation.
|
||||||
|
//
|
||||||
|
// EXPORTS
|
||||||
|
// vv_ai_profiles() the whole table
|
||||||
|
// vv_ai_profile($id) one entry, or null
|
||||||
|
// vv_ai_profile_ok($id) is this a real profile
|
||||||
|
// vv_ai_profile_turns($id) history depth in turns
|
||||||
|
// vv_ai_profile_can($id,$cap) capability check
|
||||||
|
// vv_ai_profile_caps($id) every capability a profile holds
|
||||||
|
// vv_ai_profile_label($id) display name, falling back to the id
|
||||||
|
// vv_ai_profiles_ui() those offered as buttons, in order
|
||||||
|
// vv_ai_profiles_max_turns() deepest window of any profile
|
||||||
|
// vv_ai_profiles_client() the subset the browser needs, for json_encode
|
||||||
|
// vv_ai_profiles_script() publishes that subset as window.VvAiProfiles, once per page
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// turns — conversation depth. Varaverk Assistant spends ~2500 of its 16384 on retrieved
|
||||||
|
// passages so it cannot afford much; troubleshoot carries a whole log tail and can
|
||||||
|
// afford least of all; the two that retrieve nothing can hold a real conversation.
|
||||||
|
// caps — see VV_AI_CAP_MEANING below.
|
||||||
|
// ui — appears in the profile bar. troubleshoot is false because it is entered by opening a
|
||||||
|
// log on the Scheduler tab, not by choosing it: it is a context you are in, and a
|
||||||
|
// button offering it from the dashboard would produce a troubleshooting contract with
|
||||||
|
// nothing to troubleshoot.
|
||||||
|
const VV_AI_PROFILES_DEF = [
|
||||||
|
'varaverk' => [
|
||||||
|
'label' => 'Varaverk Assistant',
|
||||||
|
'short' => 'Assistant',
|
||||||
|
'hint' => 'Answers only from Varaverk\'s own docs, with sources. Says so when they don\'t cover it.',
|
||||||
|
'turns' => 3,
|
||||||
|
'ui' => true,
|
||||||
|
'caps' => ['retrieve', 'kind_filter', 'health', 'run_evidence', 'incidents', 'conf_lookup'],
|
||||||
|
],
|
||||||
|
'chat' => [
|
||||||
|
'label' => 'General Chat',
|
||||||
|
'short' => 'Chat',
|
||||||
|
'hint' => 'Ordinary conversation. Hands anything about this install to the assistant on its own.',
|
||||||
|
'turns' => 8,
|
||||||
|
'ui' => true,
|
||||||
|
'caps' => [],
|
||||||
|
],
|
||||||
|
'code' => [
|
||||||
|
'label' => 'Code Sketcher',
|
||||||
|
'short' => 'Code',
|
||||||
|
'hint' => 'Drafts short scripts for Custom Scripts. First drafts — test before trusting.',
|
||||||
|
'turns' => 4,
|
||||||
|
'ui' => true,
|
||||||
|
'caps' => ['code_scan'],
|
||||||
|
],
|
||||||
|
'troubleshoot' => [
|
||||||
|
'label' => 'Troubleshoot',
|
||||||
|
'short' => 'Troubleshoot',
|
||||||
|
'hint' => 'Reasons from the log you have open, then the docs. Files a bug only when the evidence shows one.',
|
||||||
|
'turns' => 2,
|
||||||
|
'ui' => false,
|
||||||
|
'caps' => ['retrieve', 'health', 'run_evidence', 'scoped_log', 'incidents', 'conf_lookup', 'file_bugs'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Documentation only — nothing branches on it. Kept beside the table because a capability name
|
||||||
|
// with no stated meaning is how one ends up granted to a profile that should not have it.
|
||||||
|
const VV_AI_CAP_MEANING = [
|
||||||
|
'retrieve' => 'retrieval passages from the documentation index',
|
||||||
|
'kind_filter' => 'the retrieval kind filter the page exposes',
|
||||||
|
'health' => 'live health sweep measured at question time',
|
||||||
|
'run_evidence' => 'run record and log tail for a script named in the question',
|
||||||
|
'scoped_log' => 'log tail for whatever the operator currently has open',
|
||||||
|
'incidents' => 'operator-written history of what previously went wrong with this thing',
|
||||||
|
'conf_lookup' => 'deterministic "where does this conf key actually live" lookup',
|
||||||
|
'file_bugs' => 'may file a bug report against Varaverk itself',
|
||||||
|
'code_scan' => 'destructive-operation scan of generated shell',
|
||||||
|
];
|
||||||
|
|
||||||
|
function vv_ai_profiles(): array { return VV_AI_PROFILES_DEF; }
|
||||||
|
|
||||||
|
function vv_ai_profile(string $id): ?array {
|
||||||
|
return VV_AI_PROFILES_DEF[$id] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function vv_ai_profile_ok(string $id): bool {
|
||||||
|
return isset(VV_AI_PROFILES_DEF[$id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Falls back rather than throwing. An unknown id reaching here is a caller that failed to
|
||||||
|
// validate, and the shallowest window is the safe way to be wrong: it spends the least context.
|
||||||
|
function vv_ai_profile_turns(string $id): int {
|
||||||
|
return (int)(VV_AI_PROFILES_DEF[$id]['turns'] ?? 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
function vv_ai_profile_caps(string $id): array {
|
||||||
|
return VV_AI_PROFILES_DEF[$id]['caps'] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function vv_ai_profile_can(string $id, string $cap): bool {
|
||||||
|
return in_array($cap, VV_AI_PROFILES_DEF[$id]['caps'] ?? [], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function vv_ai_profile_label(string $id): string {
|
||||||
|
return VV_AI_PROFILES_DEF[$id]['label'] ?? $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insertion order is the button order, and varaverk is first because the strict profile is the
|
||||||
|
// one to land on: a misuse there costs "the docs don't cover that" rather than an invented claim
|
||||||
|
// about the system.
|
||||||
|
function vv_ai_profiles_ui(): array {
|
||||||
|
return array_filter(VV_AI_PROFILES_DEF, fn($p) => !empty($p['ui']));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The cap api/ai.php applies when storing a conversation, which has to hold the deepest window
|
||||||
|
// any profile could later ask for — a chat saved under one profile can be reopened under another.
|
||||||
|
function vv_ai_profiles_max_turns(): int {
|
||||||
|
return max(array_column(VV_AI_PROFILES_DEF, 'turns'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// What the browser needs, and nothing more. The prompts are not here to be sent and the server
|
||||||
|
// re-derives depth and capability on every request regardless — this is for drawing buttons and
|
||||||
|
// deciding whether to show the kind filter, not for the client to make policy with.
|
||||||
|
function vv_ai_profiles_client(): array {
|
||||||
|
$out = [];
|
||||||
|
foreach (VV_AI_PROFILES_DEF as $id => $p) {
|
||||||
|
$out[$id] = [
|
||||||
|
'label' => $p['label'],
|
||||||
|
'short' => $p['short'],
|
||||||
|
'hint' => $p['hint'],
|
||||||
|
'turns' => $p['turns'],
|
||||||
|
'ui' => (bool)$p['ui'],
|
||||||
|
'kind' => in_array('kind_filter', $p['caps'], true),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publishes the registry to the browser as window.VvAiProfiles, once per page however many
|
||||||
|
// surfaces ask for it. Emitted as its own tag so any script block that consumes it stays pure
|
||||||
|
// JavaScript and remains syntax-checkable outside PHP.
|
||||||
|
//
|
||||||
|
// Both the shared chat include and the Scheduler dock call this. Before it existed the dock had
|
||||||
|
// its own literal `{ code: 'Code', troubleshoot: 'Troubleshoot' }` map, which is how a fourth
|
||||||
|
// copy of the profile list came to exist in the first place.
|
||||||
|
function vv_ai_profiles_script(): void {
|
||||||
|
static $done = false;
|
||||||
|
if ($done) return;
|
||||||
|
$done = true;
|
||||||
|
echo '<script>window.VvAiProfiles = '
|
||||||
|
. json_encode(vv_ai_profiles_client(), JSON_UNESCAPED_SLASHES) . ";</script>\n";
|
||||||
|
}
|
||||||
@@ -425,8 +425,12 @@ vv_ai_chat_markup('vv-ai', [
|
|||||||
});
|
});
|
||||||
box.innerHTML = html;
|
box.innerHTML = html;
|
||||||
}
|
}
|
||||||
function loadBanner() {
|
// The 30-second tick reads the shared cache; a completed turn does not. Finishing a turn is
|
||||||
fetch(API + '?action=stats').then(r => r.json())
|
// precisely the event that changes what this banner reports — a cold model becomes resident,
|
||||||
|
// VRAM moves, context is now allocated — so re-rendering it from a payload written before the
|
||||||
|
// turn would show the operator the state they had just watched themselves leave.
|
||||||
|
function loadBanner(live) {
|
||||||
|
fetch(API + '?action=stats' + (live ? '&live=1' : '')).then(r => r.json())
|
||||||
.then(d => { if (d.ok) renderBanner(d.stats); })
|
.then(d => { if (d.ok) renderBanner(d.stats); })
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
@@ -673,7 +677,7 @@ vv_ai_chat_markup('vv-ai', [
|
|||||||
thinkEl: 'vv-ai-think',
|
thinkEl: 'vv-ai-think',
|
||||||
empty: "Ask Varaverk about itself. Answers come only from this installation's own "
|
empty: "Ask Varaverk about itself. Answers come only from this installation's own "
|
||||||
+ 'documentation, with sources.',
|
+ 'documentation, with sources.',
|
||||||
onTurn: () => { loadBanner(); loadTokens(); },
|
onTurn: () => { loadBanner(true); loadTokens(); },
|
||||||
onChats: id => { if (chatList) chatList.setActive(id); },
|
onChats: id => { if (chatList) chatList.setActive(id); },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -317,7 +317,7 @@ if (vv_ai_ui_on()) vv_ai_chat_assets();
|
|||||||
Conversations
|
Conversations
|
||||||
</span>
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
<?php vv_ai_chat_list_markup('vv-mon-ai'); ?>
|
<?php vv_ai_chat_list_markup('vv-mon-ai', true); ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Named for what it is, not "dock" — the Scheduler tab's vv-ai-dock is a different
|
<!-- Named for what it is, not "dock" — the Scheduler tab's vv-ai-dock is a different
|
||||||
@@ -1909,12 +1909,12 @@ function vvRenderAi(ai) {
|
|||||||
h += line('Ollama', 'unreachable', 'color:#e57;');
|
h += line('Ollama', 'unreachable', 'color:#e57;');
|
||||||
} else if (!rt.loaded) {
|
} else if (!rt.loaded) {
|
||||||
h += line('Model', 'not loaded', 'color:#ffb74d;');
|
h += line('Model', 'not loaded', 'color:#ffb74d;');
|
||||||
h += `<div style="font-size:10px;color:#444;margin:-2px 0 7px;">loads on first question</div>`;
|
h += `<div style="font-size:10px;color:#666;margin:-2px 0 7px;">loads on first question</div>`;
|
||||||
} else {
|
} else {
|
||||||
const p = rt.offload_pct;
|
const p = rt.offload_pct;
|
||||||
const full = p === 100;
|
const full = p === 100;
|
||||||
h += line('GPU offload', p === null ? '—' : p + '%', full ? 'color:#6fcf97;' : 'color:#ffb74d;');
|
h += line('GPU offload', p === null ? '—' : p + '%', full ? 'color:#6fcf97;' : 'color:#ffb74d;');
|
||||||
h += `<div style="font-size:10px;color:#444;margin:-2px 0 7px;">`
|
h += `<div style="font-size:10px;color:#666;margin:-2px 0 7px;">`
|
||||||
+ (full ? 'all on GPU' : 'layers on CPU — slow') + `</div>`;
|
+ (full ? 'all on GPU' : 'layers on CPU — slow') + `</div>`;
|
||||||
if (rt.context) h += line('Context', rt.context.toLocaleString());
|
if (rt.context) h += line('Context', rt.context.toLocaleString());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
// api/rsync_standalone.php
|
// api/rsync_standalone.php
|
||||||
require_once dirname(__DIR__) . '/include/scheduler.php';
|
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||||
require_once dirname(__DIR__) . '/include/docs.php';
|
require_once dirname(__DIR__) . '/include/docs.php';
|
||||||
|
require_once dirname(__DIR__) . '/include/ai_profiles.php';
|
||||||
|
|
||||||
// Live values for the `$VAR` markers in pages/readme/*.md. Conf variables, plus the derived
|
// Live values for the `$VAR` markers in pages/readme/*.md. Conf variables, plus the derived
|
||||||
// path constants — those are not conf keys, but they are exactly what a reader needs resolved
|
// path constants — those are not conf keys, but they are exactly what a reader needs resolved
|
||||||
@@ -1059,6 +1060,7 @@ Still the same two servers, two households, the same media stack running itself.
|
|||||||
At rest it is one input row. Answers expand it upward and the views above shrink to
|
At rest it is one input row. Answers expand it upward and the views above shrink to
|
||||||
suit, which is why vvFitRight() subtracts its height. It pushes rather than overlays
|
suit, which is why vvFitRight() subtracts its height. It pushes rather than overlays
|
||||||
so the thing you are asking about stays on screen. Collapsing keeps the conversation. -->
|
so the thing you are asking about stays on screen. Collapsing keeps the conversation. -->
|
||||||
|
<?php vv_ai_profiles_script(); ?>
|
||||||
<div id="vv-ai-dock">
|
<div id="vv-ai-dock">
|
||||||
<div id="vv-ai-dock-body" style="display:none"></div>
|
<div id="vv-ai-dock-body" style="display:none"></div>
|
||||||
<div id="vv-ai-dock-bar">
|
<div id="vv-ai-dock-bar">
|
||||||
@@ -2367,8 +2369,12 @@ function vvAiDockScope(profile, label, target) {
|
|||||||
const had = vvAiHist.length > 0;
|
const had = vvAiHist.length > 0;
|
||||||
vvAiProfile = profile;
|
vvAiProfile = profile;
|
||||||
vvAiScope = target;
|
vvAiScope = target;
|
||||||
|
// Short label from the registry, not a literal map. The map that used to be here was the
|
||||||
|
// fifth place a profile got defined, and it silently fell back to "Assistant" for any id it
|
||||||
|
// had not been told about — so a new profile would have shown up in the chip as the strict one.
|
||||||
|
const _p = (window.VvAiProfiles || {})[profile];
|
||||||
document.getElementById('vv-ai-dock-chip').textContent =
|
document.getElementById('vv-ai-dock-chip').textContent =
|
||||||
({ code: 'Code', troubleshoot: 'Troubleshoot' }[profile] || 'Assistant') + ' · ' + label;
|
(_p ? _p.short : profile) + ' · ' + label;
|
||||||
|
|
||||||
// Transcript stays, history sent to the model resets — the same rule the AI tab's profile
|
// Transcript stays, history sent to the model resets — the same rule the AI tab's profile
|
||||||
// buttons already use. A troubleshooting thread carrying log excerpts must not bleed into a
|
// buttons already use. A troubleshooting thread carrying log excerpts must not bleed into a
|
||||||
|
|||||||
Reference in New Issue
Block a user