mirror of
https://github.com/MervinPraison/PraisonAIUI.git
synced 2026-11-07 05:56:17 -10:00
Compare commits
94
Commits
No files matched your search
@@ -32,4 +32,13 @@ const comment = ciFix.buildCiFixComment({
|
||||
});
|
||||
assert('comment mentions product guardrails', comment.includes('Product guardrails'));
|
||||
|
||||
const skip = ciFix.shouldSkipCiFix({
|
||||
comments: [],
|
||||
headSha: 'abc1234567890abcdef1234567890abcdef12',
|
||||
labels: [],
|
||||
claudeInProgress: false,
|
||||
failedChecks: [{ name: 'python', workflow: 'CI' }],
|
||||
});
|
||||
assert('ci fix without FINAL review allowed', !skip.skip);
|
||||
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -46,10 +46,9 @@ function hasRecentCiFixComment(comments, headSha) {
|
||||
});
|
||||
}
|
||||
|
||||
function shouldSkipCiFix({ comments, headSha, labels, hasFinal, claudeInProgress, failedChecks }) {
|
||||
function shouldSkipCiFix({ comments, headSha, labels, claudeInProgress, failedChecks }) {
|
||||
if (!failedChecks.length) return { skip: true, reason: 'no failed checks' };
|
||||
if (labels.includes(CI_FIX_LABEL)) return { skip: true, reason: 'ci fix pending' };
|
||||
if (!hasFinal) return { skip: true, reason: 'awaiting final claude review trigger' };
|
||||
if (claudeInProgress) return { skip: true, reason: 'claude in progress' };
|
||||
if (hasCiFixCommentForSha(comments, headSha)) {
|
||||
return { skip: true, reason: 'already commented for this sha' };
|
||||
@@ -159,12 +158,14 @@ function buildCiFixComment({ headSha, failedChecks, failureSummaries }) {
|
||||
'',
|
||||
'**Product guardrails (AGENTS.md):**',
|
||||
'- Do not disturb PraisonAIUI contracts or frontend/backend layering to accommodate scope creep.',
|
||||
'- Core agent runtime belongs in praisonaiagents; agent-callable tools belong in PraisonAI-Tools — not here.',
|
||||
'- Prefer existing YAML config, plugins, and feature modules over new server.py/create_app() params or knobs with no live consumer.',
|
||||
'- Tests must continue to guard backward compatibility and UX behaviour — passing CI by lowering test standards is not acceptable.',
|
||||
'- If the feature does not genuinely add product value, recommend reverting or narrowing scope instead of patching around failures.',
|
||||
'',
|
||||
'## What to do',
|
||||
'1. State your verdict: **legitimate fix**, **regression fix**, or **needs human review** — and why (1–3 sentences).',
|
||||
'2. Fix root cause with **minimal changes**; never bloat the Agent class with extra params.',
|
||||
'2. Fix root cause with **minimal changes**; never bloat server.py or create_app() with extra params.',
|
||||
exampleTest
|
||||
? `3. Run failing tests locally, e.g. \`pytest ${exampleTest} -q\`, plus any related tests touched by the PR.`
|
||||
: '3. Run failing tests locally with targeted pytest, plus any related tests touched by the PR.',
|
||||
@@ -221,7 +222,6 @@ async function maybeTriggerCiFixClaude(github, owner, repo, prNumber, core, opts
|
||||
return { skipped: true, reason: 'no failed checks yet (may be pending)' };
|
||||
}
|
||||
|
||||
const hasFinal = mergeGate.hasFinalClaudeReviewTrigger(ctx.comments);
|
||||
const claudeInProgress = await mergeGate.hasInProgressClaudeAssistant(
|
||||
github, owner, repo, prNumber
|
||||
);
|
||||
@@ -229,7 +229,6 @@ async function maybeTriggerCiFixClaude(github, owner, repo, prNumber, core, opts
|
||||
comments: ctx.comments,
|
||||
headSha,
|
||||
labels,
|
||||
hasFinal,
|
||||
claudeInProgress,
|
||||
failedChecks: failedRuns,
|
||||
});
|
||||
|
||||
@@ -21,8 +21,27 @@ module.exports = {
|
||||
'SCOPE: Focus ONLY on PraisonAIUI (src/praisonaiui, src/frontend, tests, docs). '
|
||||
+ 'Do NOT expand into praisonaiagents or the monorepo unless the PR explicitly requires it.',
|
||||
finalClaudeProductValue:
|
||||
'4. Product value: review whether the change genuinely adds PraisonAIUI UX value and correct '
|
||||
+ 'layering (UI/frontend here; core agent logic belongs in praisonaiagents). Reject scope creep.',
|
||||
'4. Product value: review in depth whether the change genuinely adds PraisonAIUI UX value — '
|
||||
+ 'never add features for the sake of adding them. It must strengthen the UI (simpler, '
|
||||
+ 'more user-friendly, robust). If it does not clearly add value, request changes or recommend '
|
||||
+ 'rejecting/closing rather than merging scope creep.\n'
|
||||
+ '5. Layering: UI/frontend/server features here; core agent runtime belongs in praisonaiagents '
|
||||
+ '(PraisonAI monorepo). Agent-callable tools → PraisonAI-Tools. Do not pull monorepo or SDK '
|
||||
+ 'scope into this repo unless the PR explicitly requires it.\n'
|
||||
+ '6. Do not bloat server.py or create_app() with new params — prefer existing YAML config, '
|
||||
+ 'plugins, and feature modules; only add params if absolutely required. Reject knobs/exports '
|
||||
+ 'with no live consumer.',
|
||||
mergeGateProductValue:
|
||||
'Confirm product value gate: change strengthens PraisonAIUI UX (no scope creep). The aim is a '
|
||||
+ 'LIGHTWEIGHT AND POWERFUL package — BLOCK changes that add params/modules/exports duplicating '
|
||||
+ 'existing UI capabilities (YAML config, plugins, feature modules, existing server hooks), or '
|
||||
+ 'add knobs/fields with no live consumer, i.e. scope creep for the sake of it.',
|
||||
mergeGateLayering:
|
||||
'Layering routing: BLOCK if core agent logic was added here instead of praisonaiagents; BLOCK if '
|
||||
+ 'agent-callable tools were added here instead of PraisonAI-Tools.',
|
||||
mergeGateServerBloat:
|
||||
'Do not bloat server.py or create_app() with additional params — only if absolutely required; '
|
||||
+ 'prefer existing config/plugin patterns.',
|
||||
agentPyChecks: false,
|
||||
reviewBotLogins: [
|
||||
'coderabbitai[bot]',
|
||||
|
||||
@@ -205,7 +205,7 @@ const headAfterRebase = conflictIso(-21 * 60 * 1000);
|
||||
const rebaseComments = [conflictTrigger, rebaseDone, finalAfterRebase];
|
||||
assert('conflict blocks before rebase done', mg.hasRecentConflictComment([conflictTrigger], headAfterRebase));
|
||||
assert('conflict clears after rebase + FINAL on HEAD', !mg.hasRecentConflictComment(rebaseComments, headAfterRebase));
|
||||
assert('conflict still blocks without FINAL on HEAD', mg.hasRecentConflictComment(
|
||||
assert('conflict clears after rebase without FINAL on HEAD', !mg.hasRecentConflictComment(
|
||||
[conflictTrigger, rebaseDone],
|
||||
headAfterRebase
|
||||
));
|
||||
|
||||
@@ -67,7 +67,11 @@ function isClaudeTriggerNoise(c) {
|
||||
|
||||
function isClaudeFinalReplyComment(c) {
|
||||
const login = (c.user?.login || '').toLowerCase();
|
||||
if (!login.includes('praisonai-triage')) return false;
|
||||
const fromAutomation =
|
||||
login.includes('praisonai-triage') ||
|
||||
login.includes('github-actions') ||
|
||||
AUTO_ACTORS.some((a) => a.toLowerCase() === login);
|
||||
if (!fromAutomation) return false;
|
||||
return (c.body || '').includes('Claude finished');
|
||||
}
|
||||
|
||||
@@ -89,15 +93,47 @@ function isConflictRebaseTriggerComment(c) {
|
||||
|
||||
function isConflictRebaseCompletionComment(c) {
|
||||
const login = (c.user?.login || '').toLowerCase();
|
||||
if (!login.includes('praisonai-triage') && !login.includes('github-actions')) return false;
|
||||
const fromAutomation =
|
||||
login.includes('praisonai-triage') ||
|
||||
login.includes('github-actions') ||
|
||||
AUTO_ACTORS.some((a) => a.toLowerCase() === login);
|
||||
if (!fromAutomation) return false;
|
||||
const body = (c.body || '').toLowerCase();
|
||||
return (
|
||||
body.includes('rebase complete') ||
|
||||
body.includes('rebase onto') ||
|
||||
body.includes('force-pushed') ||
|
||||
(body.includes('conflict') && body.includes('resolved'))
|
||||
);
|
||||
}
|
||||
|
||||
/** HEAD landed after conflict rebase but no FINAL @claude since that push. */
|
||||
function needsFinalReReviewAfterConflictRebase(comments, headPushedAt) {
|
||||
if (!headPushedAt) return false;
|
||||
const headTime = new Date(headPushedAt).getTime();
|
||||
const conflictTriggers = comments.filter(isConflictRebaseTriggerComment);
|
||||
if (conflictTriggers.length === 0) return false;
|
||||
|
||||
const latestConflict = conflictTriggers.reduce((a, b) =>
|
||||
new Date(a.created_at) > new Date(b.created_at) ? a : b
|
||||
);
|
||||
const conflictTime = new Date(latestConflict.created_at).getTime();
|
||||
if (headTime <= conflictTime) return false;
|
||||
|
||||
const rebaseDone = comments.some(
|
||||
(c) =>
|
||||
new Date(c.created_at).getTime() > conflictTime && isConflictRebaseCompletionComment(c)
|
||||
);
|
||||
if (!rebaseDone) return false;
|
||||
|
||||
const finalsAfterHead = comments.filter(
|
||||
(c) =>
|
||||
isFinalClaudeTriggerComment(c) &&
|
||||
new Date(c.created_at).getTime() >= headTime - 60000
|
||||
);
|
||||
return finalsAfterHead.length === 0;
|
||||
}
|
||||
|
||||
function conflictRebaseQuiescent(comments, headPushedAt) {
|
||||
const conflictTriggers = comments.filter(isConflictRebaseTriggerComment);
|
||||
if (conflictTriggers.length === 0) return true;
|
||||
@@ -113,7 +149,7 @@ function conflictRebaseQuiescent(comments, headPushedAt) {
|
||||
);
|
||||
if (!rebaseDone) return false;
|
||||
|
||||
return finalClaudeCompletedOnSha(comments, headPushedAt);
|
||||
return true;
|
||||
}
|
||||
|
||||
function hasRecentConflictComment(comments, headPushedAt = null) {
|
||||
@@ -238,6 +274,9 @@ function shouldSkipStaleFinalRecovery(comments, headPushedAt, headPusherLogin =
|
||||
if (headPusherLogin && isClaudeAutomationLogin(headPusherLogin)) {
|
||||
return { skip: true, reason: 'head pushed by Claude automation' };
|
||||
}
|
||||
if (needsFinalReReviewAfterConflictRebase(comments, headPushedAt)) {
|
||||
return { skip: false, reason: '' };
|
||||
}
|
||||
if (isPushSoonAfterLatestFinal(comments, headPushedAt)) {
|
||||
return {
|
||||
skip: true,
|
||||
@@ -846,6 +885,7 @@ module.exports = {
|
||||
hasRecentConflictComment,
|
||||
isConflictRebaseTriggerComment,
|
||||
isConflictRebaseCompletionComment,
|
||||
needsFinalReReviewAfterConflictRebase,
|
||||
conflictRebaseQuiescent,
|
||||
hasHumanChangesRequested,
|
||||
hasAnyChangesRequested,
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env node
|
||||
const chain = require('./pr-review-chain.js');
|
||||
|
||||
let failed = 0;
|
||||
function assert(name, cond) {
|
||||
if (!cond) {
|
||||
console.error('FAIL:', name);
|
||||
failed++;
|
||||
} else {
|
||||
console.log('ok:', name);
|
||||
}
|
||||
}
|
||||
|
||||
const coderabbit = {
|
||||
user: { login: 'coderabbitai[bot]' },
|
||||
body: '<!-- summarize by coderabbit.ai --> summary',
|
||||
created_at: '2026-07-02T07:16:00Z',
|
||||
};
|
||||
const greptile = {
|
||||
user: { login: 'greptile-apps[bot]' },
|
||||
body: '<h3>Greptile Summary</h3><p>Looks good</p>',
|
||||
created_at: '2026-07-02T07:19:00Z',
|
||||
};
|
||||
const copilotKick = {
|
||||
user: { login: 'MervinPraison' },
|
||||
body: '@copilot Do a thorough review',
|
||||
created_at: '2026-07-02T07:20:00Z',
|
||||
};
|
||||
const copilotReply = {
|
||||
user: { login: 'copilot-swe-agent' },
|
||||
body: 'Copilot review here',
|
||||
created_at: '2026-07-02T07:22:00Z',
|
||||
};
|
||||
|
||||
assert('prior not ready without greptile', !chain.priorReviewersReady([coderabbit]).ready);
|
||||
assert('prior ready with coderabbit + greptile', chain.priorReviewersReady([coderabbit, greptile]).ready);
|
||||
assert('copilot not ready before trigger', !chain.copilotReviewReady([coderabbit, greptile]).ready);
|
||||
assert(
|
||||
'copilot ready after reply',
|
||||
chain.copilotReviewReady([coderabbit, greptile, copilotKick, copilotReply]).ready
|
||||
);
|
||||
assert(
|
||||
'claude not ready before copilot',
|
||||
!chain.claudeFinalReady([coderabbit, greptile]).ready
|
||||
);
|
||||
assert(
|
||||
'claude ready after full chain',
|
||||
chain.claudeFinalReady([coderabbit, greptile, copilotKick, copilotReply]).ready
|
||||
);
|
||||
assert(
|
||||
'claude timeout fallback when copilot triggered but silent',
|
||||
chain.claudeFinalReady([coderabbit, greptile, copilotKick], [], { allowCopilotTimeout: true }).ready
|
||||
);
|
||||
assert(
|
||||
'no timeout fallback without copilot trigger',
|
||||
!chain.claudeFinalReady([coderabbit, greptile], [], { allowCopilotTimeout: true }).ready
|
||||
);
|
||||
assert(
|
||||
'skipCopilot proceeds after prior reviewers',
|
||||
chain.claudeFinalReady([coderabbit, greptile], [], { skipCopilot: true }).ready
|
||||
);
|
||||
|
||||
const oldPr = new Date(Date.now() - 31 * 60 * 1000).toISOString();
|
||||
assert(
|
||||
'prior not ready without coderabbit before timeout',
|
||||
!chain.priorReviewersReady([greptile], [], { prCreatedAt: new Date().toISOString(), requiredWaitMs: 30 * 60 * 1000 }).ready
|
||||
);
|
||||
assert(
|
||||
'prior timeout when coderabbit silent after 30m',
|
||||
chain.priorReviewersReady([greptile], [], { prCreatedAt: oldPr, requiredWaitMs: 30 * 60 * 1000 }).ready
|
||||
);
|
||||
assert(
|
||||
'claude FINAL timeout when required reviewer silent and no prior FINAL',
|
||||
chain.claudeFinalReady([greptile], [], { prCreatedAt: oldPr, requiredWaitMs: 30 * 60 * 1000 }).ready
|
||||
);
|
||||
assert(
|
||||
'claude FINAL timeout auto-skips copilot without explicit skipCopilot',
|
||||
chain.claudeFinalReady([greptile], [], { prCreatedAt: oldPr, requiredWaitMs: 30 * 60 * 1000 }).copilotSkipped
|
||||
);
|
||||
const finalClaude = {
|
||||
user: { login: 'github-actions[bot]' },
|
||||
body: '@claude You are the FINAL architecture reviewer for this PR.',
|
||||
created_at: '2026-07-02T07:25:00Z',
|
||||
};
|
||||
assert(
|
||||
'claude FINAL not re-posted when already triggered',
|
||||
!chain.claudeFinalReady([greptile, finalClaude], [], { prCreatedAt: oldPr, requiredWaitMs: 0 }).ready
|
||||
);
|
||||
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -16,6 +16,7 @@ const CLAUDE_TRIGGER_LOGINS = new Set(['MervinPraison', 'github-actions[bot]']);
|
||||
const REQUIRED_PRIOR = ['coderabbit', 'greptile'];
|
||||
const OPTIONAL_PRIOR = ['qodo', 'gemini'];
|
||||
const OPTIONAL_PRIOR_WAIT_MS = 20 * 60 * 1000;
|
||||
const REQUIRED_PRIOR_WAIT_MS = 30 * 60 * 1000;
|
||||
|
||||
function loginOf(item) {
|
||||
return (item?.user?.login || '').toLowerCase();
|
||||
@@ -75,11 +76,23 @@ function priorReviewerStatus(comments, reviews = []) {
|
||||
|
||||
function priorReviewersReady(comments, reviews = [], options = {}) {
|
||||
const waitMs = options.optionalWaitMs ?? OPTIONAL_PRIOR_WAIT_MS;
|
||||
const requiredWaitMs = options.requiredWaitMs ?? REQUIRED_PRIOR_WAIT_MS;
|
||||
const prCreatedAt = options.prCreatedAt || null;
|
||||
const status = priorReviewerStatus(comments, reviews);
|
||||
|
||||
const missingRequired = REQUIRED_PRIOR.filter((id) => !status[id]);
|
||||
if (missingRequired.length) {
|
||||
if (prCreatedAt) {
|
||||
const age = Date.now() - new Date(prCreatedAt).getTime();
|
||||
if (age >= requiredWaitMs) {
|
||||
return {
|
||||
ready: true,
|
||||
reason: `required reviewer timeout (${missingRequired.join(', ')} silent after ${Math.round(requiredWaitMs / 60000)}m)`,
|
||||
status,
|
||||
requiredTimeout: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
return { ready: false, reason: `waiting for ${missingRequired.join(', ')}`, status };
|
||||
}
|
||||
|
||||
@@ -157,8 +170,13 @@ function claudeFinalReady(comments, reviews = [], options = {}) {
|
||||
if (!prior.ready) {
|
||||
return { ready: false, reason: prior.reason };
|
||||
}
|
||||
if (isSkipCopilot(options)) {
|
||||
return { ready: true, reason: 'copilot skipped', copilotSkipped: true };
|
||||
if (isSkipCopilot(options) || prior.requiredTimeout) {
|
||||
return {
|
||||
ready: true,
|
||||
reason: prior.requiredTimeout ? prior.reason : 'copilot skipped',
|
||||
copilotSkipped: true,
|
||||
requiredTimeout: prior.requiredTimeout,
|
||||
};
|
||||
}
|
||||
const copilot = copilotReviewReady(comments, reviews);
|
||||
if (copilot.ready) {
|
||||
@@ -283,6 +301,7 @@ module.exports = {
|
||||
isSkipCopilot,
|
||||
REQUIRED_PRIOR,
|
||||
OPTIONAL_PRIOR,
|
||||
REQUIRED_PRIOR_WAIT_MS,
|
||||
COPILOT_REVIEW_BODY,
|
||||
priorReviewerStatus,
|
||||
priorReviewersReady,
|
||||
|
||||
@@ -40,7 +40,7 @@ env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
||||
CLAUDE_UI_SCOPE: 'SCOPE: Focus ONLY on PraisonAIUI (src/praisonaiui, src/frontend, tests, docs). Do not expand into praisonaiagents or the monorepo unless the issue explicitly requires it.'
|
||||
CLAUDE_FINAL_PHASES_COMPACT: "**Phase 1: Review per AGENTS.md** (lazy imports, backward compat, import-time <200ms; SDK/product value gate — genuine UI value, correct UI vs praisonaiagents layering, no scope creep)\n**Phase 2: FIX valid issues** found by prior reviewers — push to THIS branch\n**Phase 3: Final verdict** — approve PR if ready, else request changes"
|
||||
CLAUDE_FINAL_PHASES_DETAILED: "**Phase 1: Review per AGENTS.md**\n1. Protocol-driven: check heavy implementations vs core SDK\n2. Backward compatible: ensure zero feature regressions\n3. Performance: no hot-path regressions (lazy imports, import-time <200ms)\n4. SDK/product value: review whether the change genuinely adds value — stronger PraisonAIUI UX and correct layering (UI/frontend here; core agent logic belongs in praisonaiagents). Reject scope creep; request changes or recommend closing if value is unclear\n\n**Phase 2: FIX Valid Issues**\n5. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n6. Push all code fixes directly to THIS branch (do NOT create a new PR)\n7. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n8. If all issues are resolved, approve the PR / close the Issue\n9. If blocking issues remain, request changes / leave clear action items"
|
||||
CLAUDE_FINAL_PHASES_DETAILED: "**Phase 1: Review per AGENTS.md**\n1. Protocol-driven: check heavy implementations vs core SDK\n2. Backward compatible: ensure zero feature regressions\n3. Performance: no hot-path regressions (lazy imports, import-time <200ms)\n4. Product value: review in depth whether the change genuinely adds PraisonAIUI UX value — never add features for the sake of adding them. It must strengthen the UI (simpler, more user-friendly, robust). Reject scope creep; request changes or recommend closing if value is unclear\n5. Layering: UI/frontend/server features here; core agent runtime belongs in praisonaiagents. Agent-callable tools → PraisonAI-Tools\n6. Do not bloat server.py or create_app() with new params — prefer existing YAML config, plugins, and feature modules; reject knobs/exports with no live consumer\n\n**Phase 2: FIX Valid Issues**\n7. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n8. Push all code fixes directly to THIS branch (do NOT create a new PR)\n9. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n10. If all issues are resolved, approve the PR / close the Issue\n11. If blocking issues remain, request changes / leave clear action items"
|
||||
|
||||
jobs:
|
||||
# ================================================================
|
||||
@@ -565,7 +565,7 @@ jobs:
|
||||
|
||||
const finalBody = mergeGate.FINAL_CLAUDE_REVIEW_BODY;
|
||||
const staleOpts = { allowStaleRepost: true, skipCopilot: true, optionalWaitMs: 0 };
|
||||
const missingOpts = { allowCopilotTimeout: true, optionalWaitMs: 0 };
|
||||
const missingOpts = { skipCopilot: true, optionalWaitMs: 0 };
|
||||
|
||||
if (hasFinal && isStale) {
|
||||
const gate = mergeGate.shouldSkipStaleFinalRecovery(comments, headPushedAt, headPusher);
|
||||
@@ -654,7 +654,7 @@ jobs:
|
||||
|
||||
const opts = hasFinal && isStale
|
||||
? { allowStaleRepost: true, skipCopilot: true, optionalWaitMs: 0 }
|
||||
: { allowCopilotTimeout: true, optionalWaitMs: 0 };
|
||||
: { skipCopilot: true, optionalWaitMs: 0 };
|
||||
const result = await chain.maybeTriggerClaudeFinal(
|
||||
github, owner, repo, pr.number,
|
||||
mergeGate.FINAL_CLAUDE_REVIEW_BODY, core, opts
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
name: Bot PR Recovery
|
||||
|
||||
# Belt-and-braces: kick CodeRabbit/Qodo when pull_request:opened never fired for bot PRs.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '*/15 * * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'Specific PR number to kick (optional)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
recover:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Find and kick stalled bot PRs
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER_INPUT: ${{ github.event.inputs.pr_number }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const path = require('path');
|
||||
const chain = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/bot-pr-review-chain.js'));
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const manual = (process.env.PR_NUMBER_INPUT || '').trim();
|
||||
const prNumber = manual ? parseInt(manual, 10) : null;
|
||||
if (manual && Number.isNaN(prNumber)) {
|
||||
throw new Error(`Invalid pr_number: ${manual}`);
|
||||
}
|
||||
await chain.recoverStalledBotPrs(github, owner, repo, { prNumber }, core);
|
||||
@@ -280,7 +280,7 @@ jobs:
|
||||
model: claude-opus-4-8
|
||||
trigger_phrase: '@claude-merge-gate-internal'
|
||||
direct_prompt: |
|
||||
You are the MERGE GATE reviewer for PraisonAI PR #${{ matrix.pr_number }}.
|
||||
You are the MERGE GATE reviewer for PraisonAIUI PR #${{ matrix.pr_number }}.
|
||||
READ-ONLY: do not modify code, commit, push, rebase, or create branches.
|
||||
|
||||
CRITICAL OUTPUT RULE:
|
||||
@@ -295,12 +295,13 @@ jobs:
|
||||
Review checklist:
|
||||
1. Read AGENTS.md and the PR diff on the current branch.
|
||||
2. Read all PR comments and reviews (FINAL architecture reviewer / Lead Engineer must have run).
|
||||
3. Confirm SDK value gate: change strengthens praisonaiagents (no scope creep).
|
||||
4. Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.
|
||||
5. If agent.py adds >100 lines or new Agent __init__ params, respond MERGE_GATE_VERDICT: BLOCK (manual review required).
|
||||
6. BLOCK if labels security/breaking-change/needs-manual-review/release, sensitive paths (.github/workflows, auth, pyproject.toml), secrets in diff, PR >800 lines or >30 files, or SDK changes without tests. Label `merge-gate-ci-only` exempts CI-only changes under `.github/workflows/`, `.github/actions/`, or merge-gate scripts.
|
||||
7. Confirm no CHANGES_REQUESTED reviews and CI green on HEAD ${{ matrix.head_sha }}.
|
||||
8. If ready to merge: MERGE_GATE_VERDICT: APPROVE. Otherwise: MERGE_GATE_VERDICT: BLOCK with reasons.
|
||||
3. Confirm product value gate: change strengthens PraisonAIUI UX (no scope creep). The aim is a LIGHTWEIGHT AND POWERFUL package — BLOCK changes that add params/modules/exports duplicating existing UI capabilities (YAML config, plugins, feature modules, existing server hooks), or add knobs/fields with no live consumer, i.e. scope creep for the sake of it.
|
||||
4. Layering routing: BLOCK if core agent logic was added here instead of praisonaiagents; BLOCK if agent-callable tools were added here instead of PraisonAI-Tools.
|
||||
5. Do not bloat server.py or create_app() with additional params — only if absolutely required; prefer existing config/plugin patterns.
|
||||
6. If server.py or create_app() adds >100 lines or new top-level params without a clear live consumer, respond MERGE_GATE_VERDICT: BLOCK (manual review required).
|
||||
7. BLOCK if labels security/breaking-change/needs-manual-review/release, sensitive paths (.github/workflows, auth, pyproject.toml), secrets in diff, PR >800 lines or >30 files, or product code changes without tests. Label `merge-gate-ci-only` exempts CI-only changes under `.github/workflows/`, `.github/actions/`, or merge-gate scripts.
|
||||
8. Confirm no CHANGES_REQUESTED reviews and CI green on HEAD ${{ matrix.head_sha }}.
|
||||
9. If ready to merge: MERGE_GATE_VERDICT: APPROVE. Otherwise: MERGE_GATE_VERDICT: BLOCK with reasons.
|
||||
|
||||
Do not approve or merge via GitHub UI/API — only post the verdict comment.
|
||||
disallowed_tools: |
|
||||
|
||||
@@ -349,6 +349,9 @@ jobs:
|
||||
SCOPE: src/praisonaiui, src/frontend, tests, docs only.
|
||||
|
||||
**Phase 1 — Review** architecture, security, tests, backward compat, import-time budget.
|
||||
Product value gate: change must strengthen PraisonAIUI UX (no scope creep). BLOCK params/modules/exports
|
||||
duplicating existing UI capabilities or knobs with no live consumer. Core agent logic belongs in
|
||||
praisonaiagents; agent-callable tools in PraisonAI-Tools — not here.
|
||||
**Phase 2 — FIX (mandatory)** implement every VALID issue on THIS branch. Push commits before Phase 3.
|
||||
Do NOT ask the author to fix. Do NOT open a new PR. Do NOT claim fork restrictions unless is_fork is true.
|
||||
**Phase 3 — Verdict** approve only after fixes are pushed and tests pass; else request changes with specifics.
|
||||
@@ -403,6 +406,11 @@ jobs:
|
||||
STEP 1 — READ GUIDELINES
|
||||
═══════════════════════════════════════════════════════════════
|
||||
Read `AGENTS.md` and any `docs/principles.md`. Key invariants:
|
||||
• AIM: This package must stay LIGHTWEIGHT AND POWERFUL. Do a CRITICAL REVIEW at each stage
|
||||
(issue triage, fix planning, PR review/implementation) and reject scope creep for the sake
|
||||
of adding features. Prefer existing YAML config, plugins, feature modules, and server hooks
|
||||
over new server.py/create_app() params or exports. Even when fixing a raised issue, keep the
|
||||
fix minimal — do NOT expand surface area or add knobs with no live consumer.
|
||||
• Cold import of `praisonaiui` must stay under 200 ms; module count must not grow.
|
||||
• Heavy / optional deps (`mcp`, `langchain`, `llama_index`, `semantic_kernel`,
|
||||
`slack_sdk`, `discord.py`, `botbuilder.core`, `openai`, `anthropic`, `mistralai`,
|
||||
@@ -438,6 +446,12 @@ jobs:
|
||||
|
||||
CONCEPTUAL CHECKS (if ANY fails — add a comment, do NOT open a PR):
|
||||
• Does it duplicate an existing public symbol in `src/praisonaiui/__init__.py`?
|
||||
• Does it duplicate existing UI capability via YAML config, plugins, or feature modules
|
||||
instead of adding new server params? → Reject as scope creep; prefer existing patterns.
|
||||
• Does it add core agent runtime logic here instead of praisonaiagents, or agent-callable
|
||||
tools here instead of PraisonAI-Tools? → Route externally; do not implement here.
|
||||
• Does it bloat server.py or create_app() with new params without a clear live consumer?
|
||||
→ Prefer existing config/plugin hooks; only add params if absolutely required.
|
||||
• Does it add new class-first message types (`class Ask*Message`, `class *Message`
|
||||
with a verb in the name)? → Use a verb-first helper instead.
|
||||
• Does it add a new hard dependency to `pyproject.toml` `[project] dependencies`?
|
||||
|
||||
@@ -12,7 +12,7 @@ on:
|
||||
- cron: '0 */6 * * *'
|
||||
|
||||
concurrency:
|
||||
group: merge-conflict-scan-${{ github.repository }}
|
||||
group: merge-conflict-scan-${{ github.repository }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
@@ -42,7 +42,9 @@ jobs:
|
||||
'@claude this PR has merge conflicts with `main`.',
|
||||
'Please rebase onto latest `main`, resolve conflicts (keep this PR\'s intent, merge in newer main logic),',
|
||||
'run targeted tests, and force-push with `--force-with-lease`.',
|
||||
'Comment which files you resolved.'
|
||||
'Comment which files you resolved.',
|
||||
'Do not bloat server.py or create_app() with new params — prefer existing YAML config, plugins, and feature modules unless absolutely required.',
|
||||
'Keep the resolution minimal — the aim is a lightweight and powerful UI package; do not introduce scope creep while resolving conflicts.',
|
||||
].join(' ');
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
@@ -83,16 +85,6 @@ jobs:
|
||||
};
|
||||
}
|
||||
|
||||
function hasFinalClaudeReviewTrigger(comments) {
|
||||
return comments.some((c) => {
|
||||
const body = (c.body || '').toLowerCase();
|
||||
if (!AUTO_ACTORS.includes(c.user.login)) return false;
|
||||
if (!body.includes('@claude')) return false;
|
||||
if (body.includes('merge conflict')) return false;
|
||||
return body.includes('final architecture reviewer') || body.includes('lead engineer');
|
||||
});
|
||||
}
|
||||
|
||||
function hasRecentAutoComment(comments) {
|
||||
const cutoff = Date.now() - COOLDOWN_MS;
|
||||
return comments.some((c) => {
|
||||
@@ -143,10 +135,6 @@ jobs:
|
||||
return { skipped: true, reason: 'recent auto comment' };
|
||||
}
|
||||
|
||||
if (!hasFinalClaudeReviewTrigger(comments)) {
|
||||
return { skipped: true, reason: 'awaiting final claude review trigger' };
|
||||
}
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# Knowledge Embedding Model Configuration
|
||||
|
||||
Knowledge / RAG search uses vector **embeddings**. The default embedding model is
|
||||
`text-embedding-3-small`. Many OpenAI keys or projects do not have access to that
|
||||
model (organization tier, model allowlist, or regional restrictions), which caused
|
||||
knowledge search to silently degrade to local substring matching.
|
||||
|
||||
PraisonAIUI now makes this failure mode configurable and observable.
|
||||
|
||||
## Configure the embedding model
|
||||
|
||||
Set an accessible model via environment variable:
|
||||
|
||||
```bash
|
||||
export AIUI_EMBEDDING_MODEL=text-embedding-ada-002
|
||||
```
|
||||
|
||||
`OPENAI_EMBEDDING_MODEL` is also honoured as a fallback if `AIUI_EMBEDDING_MODEL`
|
||||
is not set.
|
||||
|
||||
## Fallback chain
|
||||
|
||||
The OpenAI-compatible `/v1/embeddings` endpoint tries the requested (or default)
|
||||
model first, then each model in the fallback chain if the primary returns a
|
||||
`model_not_found` / 403 access error:
|
||||
|
||||
```bash
|
||||
export AIUI_EMBEDDING_FALLBACK_MODELS="text-embedding-ada-002,text-embedding-3-small"
|
||||
```
|
||||
|
||||
If every candidate is inaccessible, the endpoint returns `503` with an actionable
|
||||
`model_not_found` error instead of a generic `500`.
|
||||
|
||||
## Observing degraded search
|
||||
|
||||
`GET /api/knowledge/status` reports embedding availability:
|
||||
|
||||
```json
|
||||
{
|
||||
"total": 12,
|
||||
"backend": "SDKKnowledgeManager",
|
||||
"status": "degraded",
|
||||
"embedding": {"available": false, "error": "Project does not have access to model `text-embedding-3-small`"},
|
||||
"warnings": ["Embedding model unavailable; knowledge search degraded to local text matching. Set AIUI_EMBEDDING_MODEL to an accessible model."]
|
||||
}
|
||||
```
|
||||
|
||||
`POST /api/knowledge/search` indicates whether results came from vector search or a
|
||||
degraded local fallback:
|
||||
|
||||
```json
|
||||
{
|
||||
"results": [],
|
||||
"count": 0,
|
||||
"search_mode": "fallback",
|
||||
"warning": "Vector search unavailable: Project does not have access to model text-embedding-3-small. Set AIUI_EMBEDDING_MODEL=text-embedding-ada-002 or enable embedding models in your provider project."
|
||||
}
|
||||
```
|
||||
|
||||
A dashboard can show a warning banner whenever `search_mode == "fallback"` or
|
||||
`embedding.available == false`.
|
||||
|
||||
## Model reference
|
||||
|
||||
| Model | Notes |
|
||||
|-------|-------|
|
||||
| `text-embedding-3-small` | Default — best cost/quality when available |
|
||||
| `text-embedding-3-large` | Higher quality — may also be restricted |
|
||||
| `text-embedding-ada-002` | Legacy — widely available on older projects |
|
||||
@@ -0,0 +1,69 @@
|
||||
# Application Design Protocol (ADP)
|
||||
|
||||
Declarative UI design standard for describing application layout, navigation, components, and theming from YAML or JSON manifests.
|
||||
|
||||
**Status:** v1alpha1 research draft — see [implementation-status.md](./implementation-status.md) for what is implemented today.
|
||||
|
||||
## Documents
|
||||
|
||||
| Document | Description |
|
||||
|----------|-------------|
|
||||
| [Research report](../application-design-protocol-report.md) | 46 standards compared; YAML vs JSON debate; design principles |
|
||||
| [spec.md](./spec.md) | Normative specification (draft) |
|
||||
| [validation-rules.md](./validation-rules.md) | Cross-reference rules R1–R30 |
|
||||
| [aiui-mapping.md](./aiui-mapping.md) | PraisonAIUI field mapping |
|
||||
| [implementation-status.md](./implementation-status.md) | Feature implementation tracker |
|
||||
|
||||
## Schemas
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| [schema/application.schema.json](./schema/application.schema.json) | Root Application kind |
|
||||
| [schema/component-catalog.schema.json](./schema/component-catalog.schema.json) | Catalog meta-schema |
|
||||
| [schema/theme.schema.json](./schema/theme.schema.json) | Theme preset or DTCG tokens |
|
||||
| [catalogs/default.catalog.json](./catalogs/default.catalog.json) | Default component and layout catalog |
|
||||
|
||||
## Examples
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| [examples/minimal.application.yaml](./examples/minimal.application.yaml) | Smallest valid app |
|
||||
| [examples/docs-site.application.yaml](./examples/docs-site.application.yaml) | Full docs site |
|
||||
| [examples/landing-page.application.yaml](./examples/landing-page.application.yaml) | Zone-based layout |
|
||||
|
||||
## Validate locally
|
||||
|
||||
```bash
|
||||
pip install jsonschema pyyaml
|
||||
pytest tests/unit/test_adp_schemas.py -v
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```yaml
|
||||
schemaVersion: 1
|
||||
apiVersion: adp.io/v1alpha1
|
||||
kind: Application
|
||||
|
||||
metadata:
|
||||
name: my-app
|
||||
version: "1.0.0"
|
||||
|
||||
spec:
|
||||
site:
|
||||
title: "My App"
|
||||
theme:
|
||||
preset: zinc
|
||||
darkMode: true
|
||||
templates:
|
||||
docs:
|
||||
layout: TwoColumnLayout
|
||||
slots:
|
||||
main: { type: DocContent }
|
||||
routes:
|
||||
- match: "/docs/**"
|
||||
template: docs
|
||||
content:
|
||||
docs:
|
||||
dir: "./docs"
|
||||
```
|
||||
@@ -0,0 +1,138 @@
|
||||
# PraisonAIUI → ADP Field Mapping
|
||||
|
||||
Field-by-field mapping from current `aiui.template.yaml` (`Config` model in `src/praisonaiui/schema/models.py`) to ADP `Application` spec.
|
||||
|
||||
**Compatibility:** Flat `aiui.template.yaml` files work with the PraisonAIUI compiler today. ADP envelope format is **planned** — see [implementation-status.md](./implementation-status.md).
|
||||
|
||||
---
|
||||
|
||||
## Envelope (new in ADP)
|
||||
|
||||
| ADP field | PraisonAIUI today | Status | Change |
|
||||
|-----------|-------------------|--------|--------|
|
||||
| `schemaVersion: 1` | `schemaVersion: 1` | Implemented | Compatible |
|
||||
| `apiVersion: adp.io/v1alpha1` | Not present | Planned | Add when envelope parser lands |
|
||||
| `kind: Application` | Not present | Planned | Add when envelope parser lands |
|
||||
| `metadata.name` | Not present | Planned | Derive from `site.title` slug |
|
||||
| `metadata.version` | Not present | Planned | Recommend SemVer |
|
||||
|
||||
## Top-level → spec wrapper
|
||||
|
||||
| PraisonAIUI (flat) | ADP (envelope) | Status | Change |
|
||||
|--------------------|----------------|--------|--------|
|
||||
| `site:` | `spec.site:` | Planned | Wrap under `spec` |
|
||||
| `site.theme` | `spec.theme:` | Planned | Promote theme to spec level |
|
||||
| `navigation:` | `spec.navigation:` | Partial | Tabs compiled; not rendered |
|
||||
| `navbar:` | `spec.navbar:` | Partial | Compiled; not rendered |
|
||||
| `footer:` | `spec.footer:` | Partial | Compiled; not rendered |
|
||||
| `search:` | `spec.search:` | Partial | Compiled; not rendered |
|
||||
| `content:` | `spec.content:` | Implemented | Same |
|
||||
| `components:` | `spec.components:` | Implemented | Same |
|
||||
| `templates:` | `spec.templates:` | Implemented | Same |
|
||||
| `routes:` | `spec.routes:` | Partial | Compiled; routing not honoured |
|
||||
| `dependencies:` | `spec.dependencies:` | Implemented | Same |
|
||||
| `logo:` (top-level) | Future ADP field | Implemented | Top-level on `Config`, not under `site` |
|
||||
| `style:`, `chat:`, `auth:`, `dashboard:` | Not in ADP v1alpha1 | N/A | Separate kinds in future |
|
||||
|
||||
## Navigation fields
|
||||
|
||||
| Field | PraisonAIUI | ADP | Status | Notes |
|
||||
|-------|-------------|-----|--------|-------|
|
||||
| `navigation.tabs[]` | Mintlify-style tabs | `spec.navigation.tabs` | Partial | Compiled, not rendered |
|
||||
| `content.docs.nav.mode` | `NavConfig.mode` | Same | Gap | Defined but unused by compiler |
|
||||
| Top-level `navigation.mode` | Not present | Removed from ADP | N/A | Use `content.docs.nav.mode` instead |
|
||||
|
||||
## Theme fields
|
||||
|
||||
| Field | PraisonAIUI | ADP | Status |
|
||||
|-------|-------------|-----|--------|
|
||||
| `site.theme.preset` | Implemented | `spec.theme.preset` | Planned (envelope) |
|
||||
| `site.theme.radius` | Implemented | Same | Same |
|
||||
| `site.theme.darkMode` | Implemented | Same | Same |
|
||||
| `theme.tokens` | Not present | DTCG file ref | Planned |
|
||||
|
||||
## Component fields
|
||||
|
||||
| Field | PraisonAIUI | ADP | Status |
|
||||
|-------|-------------|-----|--------|
|
||||
| `components.{id}.type` | String literal | Catalog allowlist | Planned (R6) |
|
||||
| `components.{id}.props` | `dict[str, Any]` | Catalog schema | Planned (R7) |
|
||||
|
||||
## Template fields
|
||||
|
||||
| Field | PraisonAIUI | ADP | Status |
|
||||
|-------|-------------|-----|--------|
|
||||
| `templates.{id}.layout` | String | Catalog enum | Same values |
|
||||
| `templates.{id}.slots` | `{ ref \| type }` | Same | Same |
|
||||
| `templates.{id}.zones` | Widget arrays | Same | Same |
|
||||
| Slots + zones on same template | Allowed | R13: one or the other | Stricter in ADP |
|
||||
|
||||
## Route fields
|
||||
|
||||
| Field | PraisonAIUI | ADP | Status |
|
||||
|-------|-------------|-----|--------|
|
||||
| `routes[].match` | Glob string | Same | Same |
|
||||
| `routes[].template` | Template key | Same | Same |
|
||||
| Runtime route resolution | Not implemented | R28 required | Gap |
|
||||
|
||||
## Migration example
|
||||
|
||||
### Today (PraisonAIUI)
|
||||
|
||||
```yaml
|
||||
schemaVersion: 1
|
||||
|
||||
site:
|
||||
title: "My Docs"
|
||||
theme:
|
||||
preset: zinc
|
||||
|
||||
templates:
|
||||
docs:
|
||||
layout: TwoColumnLayout
|
||||
slots:
|
||||
main: { type: DocContent }
|
||||
|
||||
routes:
|
||||
- match: "/docs/**"
|
||||
template: docs
|
||||
```
|
||||
|
||||
### Target (ADP)
|
||||
|
||||
```yaml
|
||||
schemaVersion: 1
|
||||
apiVersion: adp.io/v1alpha1
|
||||
kind: Application
|
||||
|
||||
metadata:
|
||||
name: my-docs
|
||||
version: "1.0.0"
|
||||
|
||||
spec:
|
||||
site:
|
||||
title: "My Docs"
|
||||
theme:
|
||||
preset: zinc
|
||||
templates:
|
||||
docs:
|
||||
layout: TwoColumnLayout
|
||||
slots:
|
||||
main: { type: DocContent }
|
||||
routes:
|
||||
- match: "/docs/**"
|
||||
template: docs
|
||||
content:
|
||||
docs:
|
||||
dir: "./docs"
|
||||
```
|
||||
|
||||
## Breaking vs compatible changes
|
||||
|
||||
| Change | Breaking? | Mitigation |
|
||||
|--------|-----------|------------|
|
||||
| Add envelope fields | No | Envelope parser normalises to flat `Config` |
|
||||
| Catalog validation | Soft break | Warn first, error in strict mode |
|
||||
| Route runtime resolution | No (fix) | Frontend change |
|
||||
| R13 slots/zones exclusivity | Soft break | Warn on violation |
|
||||
| Remove chat/dashboard from ADP | No | Separate kinds in future |
|
||||
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"name": "adp-default-catalog",
|
||||
"version": "1.0.0",
|
||||
"description": "Default ADP component and layout catalog for PraisonAIUI",
|
||||
"components": {
|
||||
"Header": {
|
||||
"description": "Site header with logo and navigation",
|
||||
"props": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"logoText": { "type": "string" },
|
||||
"logoHref": { "type": "string", "default": "/" }
|
||||
},
|
||||
"required": ["logoText"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"aria": { "role": "banner" },
|
||||
"slots": ["header"]
|
||||
},
|
||||
"Footer": {
|
||||
"description": "Site footer",
|
||||
"props": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": { "type": "string" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"aria": { "role": "contentinfo" },
|
||||
"slots": ["footer"]
|
||||
},
|
||||
"DocsSidebar": {
|
||||
"description": "Documentation sidebar navigation",
|
||||
"props": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"source": { "type": "string", "enum": ["docs-nav", "manual"], "default": "docs-nav" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"aria": { "role": "navigation" },
|
||||
"slots": ["left", "leftSidebar"]
|
||||
},
|
||||
"DocContent": {
|
||||
"description": "Main documentation content area",
|
||||
"props": { "type": "object", "additionalProperties": false },
|
||||
"aria": { "role": "main" },
|
||||
"slots": ["main"]
|
||||
},
|
||||
"Toc": {
|
||||
"description": "Table of contents for current page",
|
||||
"props": { "type": "object", "additionalProperties": false },
|
||||
"aria": { "role": "navigation" },
|
||||
"slots": ["right", "rightSidebar"]
|
||||
},
|
||||
"HeroBanner": {
|
||||
"description": "Hero banner with title and CTA",
|
||||
"props": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": { "type": "string" },
|
||||
"subtitle": { "type": "string" },
|
||||
"ctaLabel": { "type": "string" },
|
||||
"ctaHref": { "type": "string" }
|
||||
},
|
||||
"required": ["title"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"slots": ["hero"]
|
||||
},
|
||||
"SocialLinks": {
|
||||
"description": "Social media link row",
|
||||
"props": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"links": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"platform": { "type": "string" },
|
||||
"href": { "type": "string" }
|
||||
},
|
||||
"required": ["platform", "href"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"slots": ["footer"]
|
||||
},
|
||||
"Copyright": {
|
||||
"description": "Copyright notice",
|
||||
"props": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": { "type": "string" }
|
||||
},
|
||||
"required": ["text"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"slots": ["footer"]
|
||||
}
|
||||
},
|
||||
"layouts": {
|
||||
"ThreeColumnLayout": {
|
||||
"description": "Header + left sidebar + main + right TOC + footer",
|
||||
"slots": ["header", "left", "main", "right", "footer"]
|
||||
},
|
||||
"TwoColumnLayout": {
|
||||
"description": "Sidebar + main content",
|
||||
"slots": ["left", "main"]
|
||||
},
|
||||
"CenteredLayout": {
|
||||
"description": "Centred single column",
|
||||
"slots": ["main"]
|
||||
},
|
||||
"FullWidthLayout": {
|
||||
"description": "Full-width content",
|
||||
"slots": ["header", "main", "footer"]
|
||||
},
|
||||
"FlexibleLayout": {
|
||||
"description": "WordPress-style zone grid",
|
||||
"zones": ["header", "topNav", "hero", "leftSidebar", "main", "rightSidebar", "bottomNav", "footer"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
schemaVersion: 1
|
||||
apiVersion: adp.io/v1alpha1
|
||||
kind: Application
|
||||
|
||||
metadata:
|
||||
name: praisonaiui-docs
|
||||
version: "1.0.0"
|
||||
labels:
|
||||
generator: praisonaiui
|
||||
|
||||
spec:
|
||||
site:
|
||||
title: "PraisonAIUI Docs"
|
||||
description: "Documentation for PraisonAIUI - YAML-driven website generator"
|
||||
ui: shadcn
|
||||
|
||||
theme:
|
||||
preset: zinc
|
||||
radius: md
|
||||
darkMode: true
|
||||
|
||||
navigation:
|
||||
tabs:
|
||||
- tab: "Documentation"
|
||||
groups:
|
||||
- group: "Getting Started"
|
||||
icon: rocket
|
||||
prefix: getting-started
|
||||
pages:
|
||||
- index
|
||||
- getting-started/installation
|
||||
- getting-started/quickstart
|
||||
- group: "Core Concepts"
|
||||
icon: lightbulb
|
||||
prefix: concepts
|
||||
pages:
|
||||
- concepts/architecture
|
||||
- concepts/configuration
|
||||
- concepts/templates
|
||||
- tab: "API Reference"
|
||||
groups:
|
||||
- group: "API Reference"
|
||||
icon: code
|
||||
prefix: api
|
||||
pages:
|
||||
- api/cli
|
||||
- api/python
|
||||
|
||||
navbar:
|
||||
primary:
|
||||
type: github
|
||||
href: "https://github.com/MervinPraison/PraisonAIUI"
|
||||
links:
|
||||
- label: "Submit Issues"
|
||||
href: "https://github.com/MervinPraison/PraisonAIUI/issues"
|
||||
|
||||
footer:
|
||||
socials:
|
||||
github: "https://github.com/MervinPraison/PraisonAI"
|
||||
links:
|
||||
- header: "Resources"
|
||||
items:
|
||||
- label: "GitHub"
|
||||
href: "https://github.com/MervinPraison/PraisonAI"
|
||||
|
||||
search:
|
||||
enabled: true
|
||||
provider: fusejs
|
||||
|
||||
content:
|
||||
docs:
|
||||
dir: "./docs"
|
||||
include:
|
||||
- "**/*.md"
|
||||
- "**/*.mdx"
|
||||
nav:
|
||||
mode: manual
|
||||
|
||||
components:
|
||||
header_main:
|
||||
type: Header
|
||||
props:
|
||||
logoText: "PraisonAIUI"
|
||||
footer_main:
|
||||
type: Footer
|
||||
props:
|
||||
text: "© 2026 PraisonAI"
|
||||
sidebar_docs:
|
||||
type: DocsSidebar
|
||||
props:
|
||||
source: docs-nav
|
||||
|
||||
templates:
|
||||
docs:
|
||||
layout: ThreeColumnLayout
|
||||
slots:
|
||||
header: { ref: header_main }
|
||||
left: { ref: sidebar_docs }
|
||||
main: { type: DocContent }
|
||||
footer: { ref: footer_main }
|
||||
|
||||
routes:
|
||||
- match: "/docs/**"
|
||||
template: docs
|
||||
@@ -0,0 +1,58 @@
|
||||
schemaVersion: 1
|
||||
apiVersion: adp.io/v1alpha1
|
||||
kind: Application
|
||||
|
||||
metadata:
|
||||
name: colorful-landing
|
||||
version: "1.0.0"
|
||||
|
||||
spec:
|
||||
site:
|
||||
title: "Colorful Docs"
|
||||
description: "Zone-based landing page with hero and footer widgets"
|
||||
ui: shadcn
|
||||
|
||||
theme:
|
||||
preset: rose
|
||||
radius: xl
|
||||
darkMode: true
|
||||
|
||||
content:
|
||||
docs:
|
||||
dir: "./docs"
|
||||
nav:
|
||||
mode: auto
|
||||
collapsible: true
|
||||
|
||||
templates:
|
||||
docs:
|
||||
layout: FlexibleLayout
|
||||
zones:
|
||||
hero:
|
||||
- type: HeroBanner
|
||||
props:
|
||||
title: "Colorful Documentation"
|
||||
subtitle: "Vibrant, playful, and beautifully designed"
|
||||
ctaLabel: "Get Started"
|
||||
ctaHref: "/docs/getting-started/quickstart"
|
||||
rightSidebar:
|
||||
- type: Toc
|
||||
footer:
|
||||
- type: SocialLinks
|
||||
props:
|
||||
links:
|
||||
- platform: Twitter
|
||||
href: "https://twitter.com"
|
||||
- platform: GitHub
|
||||
href: "https://github.com"
|
||||
- type: Copyright
|
||||
props:
|
||||
text: "Made with love by Colorful Team"
|
||||
|
||||
routes:
|
||||
- match: "/docs/**"
|
||||
template: docs
|
||||
|
||||
a11y:
|
||||
skipToContent: true
|
||||
reduceMotion: false
|
||||
@@ -0,0 +1,32 @@
|
||||
schemaVersion: 1
|
||||
apiVersion: adp.io/v1alpha1
|
||||
kind: Application
|
||||
|
||||
metadata:
|
||||
name: minimal-docs
|
||||
version: "1.0.0"
|
||||
|
||||
spec:
|
||||
site:
|
||||
title: "Minimal Docs"
|
||||
description: "Smallest valid ADP Application manifest"
|
||||
ui: shadcn
|
||||
|
||||
theme:
|
||||
preset: zinc
|
||||
radius: md
|
||||
darkMode: true
|
||||
|
||||
content:
|
||||
docs:
|
||||
dir: "./docs"
|
||||
|
||||
templates:
|
||||
docs:
|
||||
layout: TwoColumnLayout
|
||||
slots:
|
||||
main: { type: DocContent }
|
||||
|
||||
routes:
|
||||
- match: "/docs/**"
|
||||
template: docs
|
||||
@@ -0,0 +1,36 @@
|
||||
# ADP Implementation Status
|
||||
|
||||
Honest status of ADP v1alpha1 features against the PraisonAIUI reference implementation.
|
||||
|
||||
**Last updated:** August 2026
|
||||
|
||||
| Feature | Spec reference | Status | Notes |
|
||||
|---------|----------------|--------|-------|
|
||||
| JSON Schema validation in CLI | spec §6 | Planned | Schemas published; no `aiui validate --adp` yet |
|
||||
| ADP envelope parsing (`apiVersion/kind/spec`) | spec §3 | Planned | Flat `aiui.template.yaml` works today |
|
||||
| Component catalog validation (R6–R7) | spec §5 | Planned | Catalog split to `catalogs/default.catalog.json` |
|
||||
| `$yaml:` / `$json:` file includes | spec §7 | Planned | Not implemented in compiler |
|
||||
| Strict mode unknown-field rejection (R4) | validation-rules R4 | Planned | Pydantic allows extra fields on some models |
|
||||
| Route-driven template resolution (R28) | validation-rules R28 | Gap | Frontend hardcodes `templates.docs` |
|
||||
| Sidebar nav mode (R29) | validation-rules R29 | Gap | Filesystem-only sidebar today |
|
||||
| Catalog ARIA roles at render (R30) | validation-rules R30 | Gap | No catalog-driven a11y in renderer |
|
||||
| `navigation.tabs` rendering | spec §4.3 | Gap | Compiled to manifest but not consumed by React |
|
||||
| DTCG token file validation (R26) | validation-rules R26 | Planned | Preset shorthand works via Tailwind |
|
||||
| Published `$schema` URLs | README | Partial | Local schemas only; `https://adp.io/` is placeholder |
|
||||
|
||||
## What works today
|
||||
|
||||
- Flat `aiui.template.yaml` → Pydantic `Config` → compiler → JSON manifests → React SPA
|
||||
- Components, templates, routes, theme presets, content scanning
|
||||
- ADP example manifests validate against `application.schema.json` (standalone)
|
||||
- Unit tests in `tests/unit/test_adp_schemas.py`
|
||||
|
||||
## Migration path
|
||||
|
||||
1. Add envelope normalisation layer before `Config.model_validate`
|
||||
2. Wire `route-manifest.json` into frontend routing (R28)
|
||||
3. Honour `navigation.tabs` and `content.docs.nav.mode` (R29)
|
||||
4. Add `aiui validate --schema adp` using published JSON Schema
|
||||
5. Apply catalog ARIA hints at render time (R30)
|
||||
|
||||
See [aiui-mapping.md](./aiui-mapping.md) for field-level compatibility.
|
||||
@@ -0,0 +1,449 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "application.schema.json",
|
||||
"title": "ADP Application",
|
||||
"description": "Application Design Protocol — Application kind manifest",
|
||||
"type": "object",
|
||||
"required": ["schemaVersion", "apiVersion", "kind", "metadata", "spec"],
|
||||
"additionalProperties": false,
|
||||
"patternProperties": {
|
||||
"^x-adp-": {}
|
||||
},
|
||||
"properties": {
|
||||
"schemaVersion": {
|
||||
"type": "integer",
|
||||
"const": 1,
|
||||
"description": "ADP envelope version"
|
||||
},
|
||||
"apiVersion": {
|
||||
"type": "string",
|
||||
"const": "adp.io/v1alpha1"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"const": "Application"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"required": ["name"],
|
||||
"additionalProperties": false,
|
||||
"patternProperties": {
|
||||
"^x-adp-": {}
|
||||
},
|
||||
"properties": {
|
||||
"name": { "type": "string", "minLength": 1 },
|
||||
"version": { "type": "string" },
|
||||
"labels": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "string" }
|
||||
},
|
||||
"annotations": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"type": "object",
|
||||
"required": ["site", "templates", "routes"],
|
||||
"additionalProperties": false,
|
||||
"patternProperties": {
|
||||
"^x-adp-": {}
|
||||
},
|
||||
"properties": {
|
||||
"site": { "$ref": "#/$defs/site" },
|
||||
"theme": { "$ref": "#/$defs/theme" },
|
||||
"navigation": { "$ref": "#/$defs/navigation" },
|
||||
"navbar": { "$ref": "#/$defs/navbar" },
|
||||
"footer": { "$ref": "#/$defs/footer" },
|
||||
"search": { "$ref": "#/$defs/search" },
|
||||
"components": { "$ref": "#/$defs/components" },
|
||||
"templates": { "$ref": "#/$defs/templates" },
|
||||
"routes": { "$ref": "#/$defs/routes" },
|
||||
"content": { "$ref": "#/$defs/content" },
|
||||
"dependencies": { "$ref": "#/$defs/dependencies" },
|
||||
"seo": { "$ref": "#/$defs/seo" },
|
||||
"i18n": { "$ref": "#/$defs/i18n" },
|
||||
"a11y": { "$ref": "#/$defs/a11y" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"site": {
|
||||
"type": "object",
|
||||
"required": ["title"],
|
||||
"additionalProperties": false,
|
||||
"patternProperties": {
|
||||
"^x-adp-": {}
|
||||
},
|
||||
"properties": {
|
||||
"title": { "type": "string" },
|
||||
"description": { "type": "string" },
|
||||
"ui": {
|
||||
"type": "string",
|
||||
"enum": ["shadcn", "mui", "chakra"],
|
||||
"default": "shadcn"
|
||||
},
|
||||
"routeBaseDocs": { "type": "string", "default": "/docs" },
|
||||
"customCss": { "type": "string" },
|
||||
"plugins": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"theme": {
|
||||
"oneOf": [
|
||||
{
|
||||
"title": "Preset theme",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"preset": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"zinc", "slate", "stone", "gray", "neutral",
|
||||
"red", "orange", "amber", "yellow", "lime", "green",
|
||||
"emerald", "teal", "cyan", "sky", "blue", "indigo",
|
||||
"violet", "purple", "fuchsia", "pink", "rose"
|
||||
]
|
||||
},
|
||||
"radius": {
|
||||
"type": "string",
|
||||
"enum": ["none", "sm", "md", "lg", "xl"]
|
||||
},
|
||||
"darkMode": { "type": "boolean" }
|
||||
},
|
||||
"required": ["preset"]
|
||||
},
|
||||
{
|
||||
"title": "DTCG token reference",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"tokens": {
|
||||
"type": "string",
|
||||
"description": "Path or URL to DTCG 2025.10 compliant tokens.json"
|
||||
},
|
||||
"darkMode": { "type": "boolean" }
|
||||
},
|
||||
"required": ["tokens"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"slotRef": {
|
||||
"oneOf": [
|
||||
{ "type": "null" },
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["ref"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"ref": { "type": "string" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["type"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": { "type": "string" },
|
||||
"props": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"widget": {
|
||||
"type": "object",
|
||||
"required": ["type"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": { "type": "string" },
|
||||
"props": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"required": ["type"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": { "type": "string" },
|
||||
"props": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"type": "object",
|
||||
"required": ["layout"],
|
||||
"allOf": [
|
||||
{
|
||||
"oneOf": [
|
||||
{ "required": ["slots"] },
|
||||
{ "required": ["zones"] }
|
||||
]
|
||||
}
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"layout": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ThreeColumnLayout",
|
||||
"TwoColumnLayout",
|
||||
"CenteredLayout",
|
||||
"FullWidthLayout",
|
||||
"FlexibleLayout"
|
||||
]
|
||||
},
|
||||
"slots": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "$ref": "#/$defs/slotRef" }
|
||||
},
|
||||
"zones": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/widget" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"templates": {
|
||||
"type": "object",
|
||||
"minProperties": 1,
|
||||
"additionalProperties": { "$ref": "#/$defs/template" }
|
||||
},
|
||||
"route": {
|
||||
"type": "object",
|
||||
"required": ["match", "template"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"match": { "type": "string" },
|
||||
"template": { "type": "string" },
|
||||
"slots": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "$ref": "#/$defs/slotRef" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"routes": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "$ref": "#/$defs/route" }
|
||||
},
|
||||
"navGroup": {
|
||||
"type": "object",
|
||||
"required": ["group"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"group": { "type": "string" },
|
||||
"icon": { "type": "string" },
|
||||
"prefix": { "type": "string" },
|
||||
"pages": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"navTab": {
|
||||
"type": "object",
|
||||
"required": ["tab"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"tab": { "type": "string" },
|
||||
"url": { "type": "string" },
|
||||
"groups": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/navGroup" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"tabs": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/navTab" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"navbar": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"primary": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": { "type": "string", "enum": ["button", "github"] },
|
||||
"label": { "type": "string" },
|
||||
"href": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["label", "href"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"label": { "type": "string" },
|
||||
"href": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"socials": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "string" }
|
||||
},
|
||||
"links": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["header"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"header": { "type": "string" },
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["label", "href"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"label": { "type": "string" },
|
||||
"href": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"provider": {
|
||||
"type": "string",
|
||||
"enum": ["fusejs", "lunrjs", "pagefind"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"shadcn": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"contentSource": {
|
||||
"type": "object",
|
||||
"required": ["dir"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"dir": { "type": "string" },
|
||||
"include": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"exclude": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"indexFiles": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"nav": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"mode": { "type": "string", "enum": ["auto", "manual"] },
|
||||
"sort": { "type": "string", "enum": ["filesystem", "alpha", "date"] },
|
||||
"collapsible": { "type": "boolean" },
|
||||
"maxDepth": { "type": "integer", "minimum": 1, "maximum": 10 }
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": { "mode": { "const": "manual" } },
|
||||
"required": ["mode"]
|
||||
},
|
||||
"then": {
|
||||
"description": "Manual sidebar mode requires navigation.tabs at spec level"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"docs": { "$ref": "#/$defs/contentSource" },
|
||||
"blog": { "$ref": "#/$defs/contentSource" }
|
||||
}
|
||||
},
|
||||
"seo": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"titleTemplate": { "type": "string" },
|
||||
"defaultImage": { "type": "string" },
|
||||
"twitter": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"i18n": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"defaultLocale": { "type": "string" },
|
||||
"locales": { "type": "array", "items": { "type": "string" } },
|
||||
"rtlLocales": { "type": "array", "items": { "type": "string" } },
|
||||
"fallbackLocale": { "type": "string" },
|
||||
"translationsDir": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"a11y": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"skipToContent": { "type": "boolean" },
|
||||
"focusVisible": { "type": "boolean" },
|
||||
"reduceMotion": { "type": "boolean" },
|
||||
"ariaLabels": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "component-catalog.schema.json",
|
||||
"title": "ADP Component Catalog",
|
||||
"description": "Allowlisted component types for ADP Application manifests",
|
||||
"type": "object",
|
||||
"required": ["name", "version", "components"],
|
||||
"additionalProperties": false,
|
||||
"patternProperties": {
|
||||
"^x-adp-": {}
|
||||
},
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"version": { "type": "string" },
|
||||
"description": { "type": "string" },
|
||||
"components": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "$ref": "#/$defs/componentType" }
|
||||
},
|
||||
"layouts": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "$ref": "#/$defs/layoutType" }
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"componentType": {
|
||||
"type": "object",
|
||||
"required": ["description", "props"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"description": { "type": "string" },
|
||||
"props": { "type": "object" },
|
||||
"aria": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"role": { "type": "string" },
|
||||
"label": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"slots": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"deprecated": { "type": "boolean" },
|
||||
"x-sunset": { "type": "string", "format": "date" }
|
||||
}
|
||||
},
|
||||
"layoutType": {
|
||||
"oneOf": [
|
||||
{
|
||||
"title": "Slot-based layout",
|
||||
"type": "object",
|
||||
"required": ["description", "slots"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"description": { "type": "string" },
|
||||
"slots": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"minItems": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Zone-based layout",
|
||||
"type": "object",
|
||||
"required": ["description", "zones"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"description": { "type": "string" },
|
||||
"zones": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"minItems": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "theme.schema.json",
|
||||
"title": "ADP Theme",
|
||||
"description": "Theme configuration — preset shorthand or DTCG 2025.10 token reference",
|
||||
"oneOf": [
|
||||
{
|
||||
"title": "Preset theme",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"preset": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"zinc", "slate", "stone", "gray", "neutral",
|
||||
"red", "orange", "amber", "yellow", "lime", "green",
|
||||
"emerald", "teal", "cyan", "sky", "blue", "indigo",
|
||||
"violet", "purple", "fuchsia", "pink", "rose"
|
||||
]
|
||||
},
|
||||
"radius": {
|
||||
"type": "string",
|
||||
"enum": ["none", "sm", "md", "lg", "xl"],
|
||||
"default": "md"
|
||||
},
|
||||
"darkMode": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"required": ["preset"]
|
||||
},
|
||||
{
|
||||
"title": "DTCG token reference",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"tokens": {
|
||||
"type": "string",
|
||||
"description": "Path or URL to DTCG 2025.10 compliant tokens.json"
|
||||
},
|
||||
"darkMode": { "type": "boolean" }
|
||||
},
|
||||
"required": ["tokens"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
# Application Design Protocol (ADP) — Normative Specification
|
||||
|
||||
**Version:** v1alpha1 (draft)
|
||||
**Status:** Draft — not yet normative
|
||||
**Date:** August 2026
|
||||
|
||||
This document uses [RFC 2119](https://datatracker.ietf.org/doc/html/rfc2119) keywords: MUST, MUST NOT, SHOULD, MAY.
|
||||
|
||||
---
|
||||
|
||||
## 1. Scope
|
||||
|
||||
ADP defines a declarative format for describing **application UI design**: site metadata, theming, navigation, reusable components, page templates, and URL routing. It does NOT define:
|
||||
|
||||
- Server-side APIs or data models
|
||||
- Agent orchestration or workflow graphs
|
||||
- Infrastructure or deployment configuration
|
||||
- Executable code or templating logic in manifests
|
||||
|
||||
Implementations MUST treat ADP manifests as **data**, not code.
|
||||
|
||||
---
|
||||
|
||||
## 2. Terminology
|
||||
|
||||
| Term | Definition |
|
||||
|------|------------|
|
||||
| **Manifest** | A YAML or JSON document conforming to this spec |
|
||||
| **Application** | The root `kind: Application` manifest describing a complete UI |
|
||||
| **Component** | A named, reusable UI block with a `type` and `props` |
|
||||
| **Template** | A layout definition assigning components to slots or zones |
|
||||
| **Slot** | A named region within a layout (e.g. `header`, `main`) |
|
||||
| **Zone** | An ordered list of widgets in a slot (WordPress-style) |
|
||||
| **Route** | A URL pattern mapping to a template |
|
||||
| **Catalog** | Published JSON Schema defining allowed component types |
|
||||
| **Compiler** | Tool that validates and emits runtime JSON manifests |
|
||||
| **Renderer** | Runtime that consumes compiled manifests and renders UI |
|
||||
|
||||
---
|
||||
|
||||
## 3. Document envelope
|
||||
|
||||
Every ADP manifest MUST include:
|
||||
|
||||
```yaml
|
||||
schemaVersion: 1 # Integer; ADP envelope version
|
||||
apiVersion: adp.io/v1alpha1
|
||||
kind: Application # Application | Theme | ComponentCatalog (future)
|
||||
|
||||
metadata:
|
||||
name: string # Required; unique identifier
|
||||
version: string # SHOULD use SemVer
|
||||
labels: object # MAY contain key-value pairs
|
||||
annotations: object # MAY contain tooling metadata
|
||||
|
||||
spec: object # Required; kind-specific payload
|
||||
```
|
||||
|
||||
### 3.1 Versioning policy
|
||||
|
||||
- `schemaVersion` — integer bumped on breaking envelope changes
|
||||
- `apiVersion` — string group/version (e.g. `adp.io/v1alpha1`, `adp.io/v1`)
|
||||
- `metadata.version` — application SemVer (independent of protocol version)
|
||||
- Breaking changes MUST increment `apiVersion` (e.g. v1alpha1 → v1)
|
||||
- Deprecated fields MUST include `deprecated: true` and SHOULD include `x-sunset` date
|
||||
|
||||
### 3.2 Authoring formats
|
||||
|
||||
Manifests MAY be authored as YAML (`.application.yaml`) or JSON (`.application.json`). Semantics MUST be identical. Runtimes MUST NOT consume YAML directly; compilers MUST normalise to JSON before validation.
|
||||
|
||||
---
|
||||
|
||||
## 4. Application spec
|
||||
|
||||
An `Application` spec MUST contain at minimum:
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `site.title` | YES | Application display name |
|
||||
| `templates` | YES | At least one template |
|
||||
| `routes` | YES | At least one route |
|
||||
|
||||
All other fields are OPTIONAL.
|
||||
|
||||
### 4.1 Site (`spec.site`)
|
||||
|
||||
```yaml
|
||||
site:
|
||||
title: string # Required
|
||||
description: string # Optional
|
||||
ui: shadcn # UI framework hint; default shadcn
|
||||
routeBaseDocs: /docs # Base path for documentation routes
|
||||
```
|
||||
|
||||
### 4.2 Theme (`spec.theme`)
|
||||
|
||||
Theme MAY use preset shorthand or reference a DTCG token file:
|
||||
|
||||
```yaml
|
||||
theme:
|
||||
preset: zinc # Tailwind-style preset name
|
||||
radius: md # none | sm | md | lg | xl
|
||||
darkMode: true
|
||||
# OR
|
||||
tokens: $yaml: ./theme.tokens.json # DTCG 2025.10 file
|
||||
```
|
||||
|
||||
### 4.3 Navigation
|
||||
|
||||
ADP separates **top tab bar** from **sidebar source**:
|
||||
|
||||
```yaml
|
||||
navigation:
|
||||
tabs: # Top tab bar (Mintlify-style)
|
||||
- tab: Documentation
|
||||
groups:
|
||||
- group: Getting Started
|
||||
pages: [index, getting-started/installation]
|
||||
|
||||
content:
|
||||
docs:
|
||||
dir: ./docs
|
||||
nav:
|
||||
mode: auto # auto | manual — sidebar source
|
||||
sort: filesystem
|
||||
collapsible: true
|
||||
maxDepth: 4
|
||||
```
|
||||
|
||||
- **`navigation.tabs`** — top-level tab bar with grouped pages
|
||||
- **`content.docs.nav.mode: auto`** — sidebar generated from filesystem scan
|
||||
- **`content.docs.nav.mode: manual`** — sidebar driven by `navigation.tabs` groups
|
||||
- Renderers MUST honour `content.docs.nav.mode` when building sidebar (R29)
|
||||
|
||||
### 4.4 Components (`spec.components`)
|
||||
|
||||
```yaml
|
||||
components:
|
||||
header_main:
|
||||
type: Header # MUST exist in component catalog
|
||||
props:
|
||||
logoText: PraisonAIUI
|
||||
```
|
||||
|
||||
Rules:
|
||||
- Component IDs MUST be unique within the manifest
|
||||
- `type` MUST match an entry in the published component catalog
|
||||
- `props` MUST validate against the catalog JSON Schema for that type
|
||||
|
||||
### 4.5 Templates (`spec.templates`)
|
||||
|
||||
```yaml
|
||||
templates:
|
||||
docs:
|
||||
layout: ThreeColumnLayout
|
||||
slots:
|
||||
header: { ref: header_main }
|
||||
left: { ref: sidebar_docs }
|
||||
main: { type: DocContent }
|
||||
right: null # Explicitly hide slot
|
||||
|
||||
landing:
|
||||
layout: FlexibleLayout
|
||||
zones:
|
||||
hero:
|
||||
- type: HeroBanner
|
||||
props: { title: "Welcome" }
|
||||
```
|
||||
|
||||
Rules:
|
||||
- `layout` MUST be a known layout type in the catalog
|
||||
- Each slot value MUST be `{ ref: id }`, `{ type: ComponentType }`, or `null`
|
||||
- Zones MUST use zone names valid for `FlexibleLayout`
|
||||
- A template MUST declare either `slots` or `zones`, not both (R13)
|
||||
|
||||
### 4.6 Routes (`spec.routes`)
|
||||
|
||||
```yaml
|
||||
routes:
|
||||
- match: /docs/changelog
|
||||
template: changelog
|
||||
slots: # Optional per-route slot overrides
|
||||
right: null
|
||||
- match: /docs/**
|
||||
template: docs
|
||||
```
|
||||
|
||||
Rules:
|
||||
- Routes MUST be ordered most-specific-first
|
||||
- `match` MUST be a glob pattern
|
||||
- `template` MUST reference a key in `spec.templates`
|
||||
- Renderers MUST resolve routes at runtime using compiled `route-manifest.json`
|
||||
|
||||
### 4.7 Content (`spec.content`)
|
||||
|
||||
```yaml
|
||||
content:
|
||||
docs:
|
||||
dir: ./docs
|
||||
include: ["**/*.md", "**/*.mdx"]
|
||||
exclude: []
|
||||
nav:
|
||||
mode: auto
|
||||
sort: filesystem
|
||||
collapsible: true
|
||||
maxDepth: 4
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Component catalog
|
||||
|
||||
Implementations MUST publish a `component-catalog.schema.json` (meta-schema) and a catalog instance such as [`catalogs/default.catalog.json`](./catalogs/default.catalog.json):
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "adp-default-catalog",
|
||||
"version": "1.0.0",
|
||||
"components": {
|
||||
"Header": {
|
||||
"description": "Site header with logo and navigation",
|
||||
"props": {
|
||||
"type": "object",
|
||||
"properties": { "logoText": { "type": "string" } },
|
||||
"required": ["logoText"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"aria": { "role": "banner" },
|
||||
"slots": ["header"]
|
||||
}
|
||||
},
|
||||
"layouts": {
|
||||
"ThreeColumnLayout": {
|
||||
"description": "Header + sidebar + main + TOC + footer",
|
||||
"slots": ["header", "left", "main", "right", "footer"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Custom component types MUST be registered via plugin schema layers. Unknown types MUST cause validation failure in strict mode.
|
||||
|
||||
---
|
||||
|
||||
## 6. Compilation pipeline
|
||||
|
||||
A conforming compiler MUST:
|
||||
|
||||
1. Parse YAML or JSON manifest
|
||||
2. Normalise to JSON
|
||||
3. Validate against `application.schema.json` (JSON Schema 2020-12)
|
||||
4. Validate cross-reference rules (see [validation-rules.md](./validation-rules.md))
|
||||
5. Validate component props against catalog schema
|
||||
6. Emit runtime manifests:
|
||||
- `ui-config.json` — site, components, templates, theme, navigation
|
||||
- `route-manifest.json` — ordered routes with template refs
|
||||
- `docs-nav.json` — filesystem-derived navigation (if mode auto)
|
||||
|
||||
---
|
||||
|
||||
## 7. File composition
|
||||
|
||||
Manifests MAY include external files using Backstage-style placeholders:
|
||||
|
||||
```yaml
|
||||
theme:
|
||||
$yaml: ./theme.yaml
|
||||
|
||||
navigation:
|
||||
$yaml: ./navigation.yaml
|
||||
```
|
||||
|
||||
Rules:
|
||||
- Paths MUST be relative to the containing manifest
|
||||
- Path traversal (`../../`) MUST be rejected
|
||||
- Supported placeholders: `$yaml:`, `$json:`, `$text:`
|
||||
|
||||
---
|
||||
|
||||
## 8. Extensions
|
||||
|
||||
Vendor extensions MUST use the `x-adp-` prefix:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
site:
|
||||
x-adp-customAnalytics: true
|
||||
```
|
||||
|
||||
Implementations MUST preserve unknown `x-adp-*` fields. Non-prefixed unknown fields MUST be rejected in strict mode.
|
||||
|
||||
---
|
||||
|
||||
## 9. Conformance
|
||||
|
||||
An implementation is **ADP-conformant** if and only if it:
|
||||
|
||||
1. Accepts manifests with `apiVersion: adp.io/v1alpha1` and `kind: Application`
|
||||
2. Validates all MUST-level requirements in this spec and validation-rules.md
|
||||
3. Rejects manifests with unknown fields in strict mode
|
||||
4. Emits runtime JSON manifests from valid Application specs
|
||||
5. Resolves routes to templates at render time
|
||||
|
||||
---
|
||||
|
||||
## 10. Related documents
|
||||
|
||||
- [Research report](../application-design-protocol-report.md)
|
||||
- [Implementation status](./implementation-status.md)
|
||||
- [PraisonAIUI mapping](./aiui-mapping.md)
|
||||
- [JSON Schema: application](./schema/application.schema.json)
|
||||
- [JSON Schema: component catalog](./schema/component-catalog.schema.json)
|
||||
- [JSON Schema: theme](./schema/theme.schema.json)
|
||||
- [Examples](./examples/)
|
||||
@@ -0,0 +1,115 @@
|
||||
# ADP Validation Rules
|
||||
|
||||
Cross-reference validation rules for Application Design Protocol manifests. These constraints cannot be expressed in JSON Schema alone and MUST be enforced by conforming compilers.
|
||||
|
||||
Rules use RFC 2119 keywords: MUST, MUST NOT, SHOULD, MAY.
|
||||
|
||||
Inspired by AWP validation rules R1–R32 pattern.
|
||||
|
||||
---
|
||||
|
||||
## Envelope rules
|
||||
|
||||
| Rule | Level | Description |
|
||||
|------|-------|-------------|
|
||||
| **R1** | MUST | `schemaVersion` MUST equal `1` for `apiVersion: adp.io/v1alpha1` |
|
||||
| **R2** | MUST | `kind` MUST be `Application` for root manifests |
|
||||
| **R3** | MUST | `metadata.name` MUST be unique within a deployment scope |
|
||||
| **R4** | MUST NOT | Unknown top-level fields outside `x-adp-*` namespace in strict mode |
|
||||
|
||||
## Component rules
|
||||
|
||||
| Rule | Level | Description |
|
||||
|------|-------|-------------|
|
||||
| **R5** | MUST | Every `spec.components` entry MUST have a unique ID |
|
||||
| **R6** | MUST | Every component `type` MUST exist in the published component catalog |
|
||||
| **R7** | MUST | Component `props` MUST validate against catalog JSON Schema for that type |
|
||||
| **R8** | MUST NOT | A component ID be referenced by `ref:` unless it exists in `spec.components` |
|
||||
| **R9** | SHOULD | Unused components (defined but never referenced) SHOULD emit a warning |
|
||||
|
||||
## Template rules
|
||||
|
||||
| Rule | Level | Description |
|
||||
|------|-------|-------------|
|
||||
| **R10** | MUST | Every `spec.templates` entry MUST declare a valid `layout` from the catalog |
|
||||
| **R11** | MUST | Slot names in `templates.*.slots` MUST be valid for the declared layout |
|
||||
| **R12** | MUST | Zone names in `templates.*.zones` MUST be valid for `FlexibleLayout` |
|
||||
| **R13** | MUST | Each template MUST declare either `slots` or `zones`, not both |
|
||||
| **R14** | MUST | Inline slot `{ type: X }` types MUST exist in the component catalog |
|
||||
| **R15** | MUST | Slot `{ ref: id }` MUST resolve to a component in `spec.components` |
|
||||
|
||||
## Route rules
|
||||
|
||||
| Rule | Level | Description |
|
||||
|------|-------|-------------|
|
||||
| **R16** | MUST | Every route `template` MUST reference a key in `spec.templates` |
|
||||
| **R17** | MUST | Routes MUST be ordered most-specific-first (longer/more precise patterns before wildcards) |
|
||||
| **R18** | MUST | Route slot overrides MUST use valid slot names for the target template's layout |
|
||||
| **R19** | SHOULD | At least one route SHOULD match the site root or docs base path |
|
||||
|
||||
## Content and navigation rules
|
||||
|
||||
| Rule | Level | Description |
|
||||
|------|-------|-------------|
|
||||
| **R20** | MUST | If `content.docs.dir` is declared, the directory MUST exist at compile time |
|
||||
| **R21** | MUST | If `content.docs.nav.mode` is `manual`, `navigation.tabs` MUST be non-empty |
|
||||
| **R22** | SHOULD | If `content.docs.nav.mode` is `auto`, a content source MUST be declared |
|
||||
|
||||
Navigation model:
|
||||
|
||||
- **`navigation.tabs`** — top tab bar (Mintlify-style)
|
||||
- **`content.docs.nav.mode`** — sidebar source: `auto` (filesystem scan) or `manual` (use `navigation.tabs` groups)
|
||||
|
||||
## File include rules
|
||||
|
||||
| Rule | Level | Description |
|
||||
|------|-------|-------------|
|
||||
| **R23** | MUST NOT | `$yaml:`, `$json:`, or `$text:` paths traverse outside the project root |
|
||||
| **R24** | MUST | Included files MUST parse as valid YAML/JSON/text respectively |
|
||||
| **R25** | MUST NOT | Circular file includes (A includes B includes A) |
|
||||
|
||||
## Theme rules
|
||||
|
||||
| Rule | Level | Description |
|
||||
|------|-------|-------------|
|
||||
| **R26** | MUST | If `theme.tokens` is declared, the referenced file MUST exist and validate against DTCG 2025.10 |
|
||||
| **R27** | MUST NOT | Both `theme.preset` and `theme.tokens` be declared simultaneously |
|
||||
|
||||
## Renderer conformance (target — not yet implemented in PraisonAIUI)
|
||||
|
||||
These rules define the target behaviour for ADP-conformant renderers. See [implementation-status.md](./implementation-status.md).
|
||||
|
||||
| Rule | Level | Description |
|
||||
|------|-------|-------------|
|
||||
| **R28** | MUST | Renderers MUST resolve the active template from `route-manifest.json` at runtime |
|
||||
| **R29** | MUST | Renderers MUST honour `content.docs.nav.mode` when building sidebar |
|
||||
| **R30** | MUST | Renderers MUST apply ARIA roles from catalog for each rendered component |
|
||||
|
||||
---
|
||||
|
||||
## Validation pipeline order
|
||||
|
||||
```
|
||||
1. Parse YAML/JSON
|
||||
2. Resolve $yaml/$json/$text includes (R23–R25)
|
||||
3. JSON Schema validate (application.schema.json)
|
||||
4. Cross-ref rules R1–R27
|
||||
5. Component props validate against catalog (R6–R7)
|
||||
6. Emit runtime manifests
|
||||
7. Runtime conformance R28–R30 (renderer responsibility)
|
||||
```
|
||||
|
||||
## Error format
|
||||
|
||||
Validation failures MUST report:
|
||||
|
||||
```json
|
||||
{
|
||||
"rule": "R8",
|
||||
"path": "spec.templates.docs.slots.header.ref",
|
||||
"message": "Component ref 'missing_header' not found in spec.components",
|
||||
"severity": "error"
|
||||
}
|
||||
```
|
||||
|
||||
Warnings (R9) use `"severity": "warning"`.
|
||||
@@ -0,0 +1,427 @@
|
||||
# Application Design Protocol (ADP) — Research Report
|
||||
|
||||
**Version:** 1.0 (draft)
|
||||
**Date:** August 2026
|
||||
**Scope:** UI, layout, navigation, theming — declarative application design from text manifests
|
||||
**Status:** Research and recommendation; not yet a published standard
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive summary
|
||||
|
||||
There is no single existing standard for **designing full application UI from a declarative text file**. Instead, the problem is addressed in fragments:
|
||||
|
||||
- **Docs-site generators** (Mintlify, PraisonAIUI, Hugo) declare navigation and layout in config files.
|
||||
- **Form libraries** (JSON Forms, RJSF) declare layout trees from JSON Schema.
|
||||
- **Agent UI protocols** (A2UI, AG-UI) declare streaming component surfaces for LLM-generated UI.
|
||||
- **Catalog standards** (OpenUI, Backstage) declare component metadata without runtime layout.
|
||||
- **Design token standards** (DTCG 2025.10) declare theming without structure.
|
||||
|
||||
**Recommendation:** Define an **Application Design Protocol (ADP)** that synthesises proven patterns from 46 surveyed standards into a UI-only manifest format.
|
||||
|
||||
PraisonAIUI already implements approximately 60% of this model. ADP formalises, extends, and publishes what exists today.
|
||||
|
||||
---
|
||||
|
||||
## 2. Problem statement
|
||||
|
||||
### 2.1 What we need
|
||||
|
||||
A protocol that lets a developer (or LLM) describe an application's visual design — pages, navigation, components, themes, routes — in **one or more text files**, without writing application code.
|
||||
|
||||
### 2.2 What exists today (fragmented)
|
||||
|
||||
| Need | Who solves it | Gap |
|
||||
|------|---------------|-----|
|
||||
| Docs site from config | Mintlify, PraisonAIUI, Docusaurus | No shared spec; Mintlify uses JSON, others use YAML or JS |
|
||||
| Component catalog | OpenUI, A2UI | No layout or routing |
|
||||
| Layout trees | JSON Forms uiSchema | Forms only, not app shells |
|
||||
| Theming | DTCG, Material 3 | Tokens only, no pages |
|
||||
| Agent-generated UI | A2UI | Runtime streaming, not human-authored static sites |
|
||||
| Software metadata | Backstage | Catalog entities, not UI composition |
|
||||
|
||||
### 2.3 Design goals for ADP
|
||||
|
||||
1. **Human-readable** — editable without IDE plugins
|
||||
2. **LLM-safe** — structural schema rejects unknown fields
|
||||
3. **Portable** — same manifest renders on any ADP-compliant runtime
|
||||
4. **Validatable** — fail fast at build time, not in the browser
|
||||
5. **Extensible** — plugins add catalog entries, not inline code
|
||||
6. **Accessible** — ARIA hints required in component catalog
|
||||
|
||||
---
|
||||
|
||||
## 3. Standards landscape — 46 protocols in 10 categories
|
||||
|
||||
Each entry describes **how UI/layout/navigation is declared** — the primary comparison dimension.
|
||||
|
||||
### Category A: Web platform baseline (5)
|
||||
|
||||
| Standard | Format | How UI/layout/navigation is declared | Schema | Versioning |
|
||||
|----------|--------|--------------------------------------|--------|------------|
|
||||
| **HTML + CSS** | `.html` + `.css` | Semantic elements; CSS Flexbox/Grid; `<nav>` + links | Living spec grammar | Browser Baseline |
|
||||
| **Web Components + DSD** | HTML + JS | Custom element tree; slots; Declarative Shadow DOM | DOM IDL | Platform flags |
|
||||
| **WAI-ARIA + APG** | HTML attributes | Landmarks, widget roles, keyboard patterns | WAI-ARIA 1.2 | Rec track |
|
||||
| **DTCG Design Tokens** | JSON | Token groups: `$value`, `$type`, `{alias}` refs — **no layout** | DTCG 2025.10 | Date editions |
|
||||
| **OpenUI** | YAML/JSON | Component catalog: `components{id: props}` — **no layout tree** | Prop definitions | Per-library version |
|
||||
|
||||
### Category B: Schema-driven forms and layout (3)
|
||||
|
||||
| Standard | Format | How UI/layout/navigation is declared | Schema | Versioning |
|
||||
|----------|--------|--------------------------------------|--------|------------|
|
||||
| **JSON Forms** | JSON Schema + UI Schema | `VerticalLayout`, `Control`, `Categorization` with `scope` pointers | JSON Schema + uiSchema | Package SemVer |
|
||||
| **RJSF** | JSON Schema + uiSchema | Schema-tree mirroring; `ui:widget`, `ui:options` | React convention | Package SemVer |
|
||||
| **Google A2UI** | JSONL stream | Flat adjacency-list; `createSurface`; catalog-constrained types | JSON Schema catalog | v0.9.1 / v1.0 RC |
|
||||
|
||||
### Category C: Docs-site generators (6)
|
||||
|
||||
| Standard | Format | How UI/layout/navigation is declared | Schema | Versioning |
|
||||
|----------|--------|--------------------------------------|--------|------------|
|
||||
| **Mintlify** | JSON (`docs.json`) | `navigation.groups[].pages[]`; `$schema` URL; `$ref` splitting | Published JSON Schema URL | Platform |
|
||||
| **PraisonAIUI** | YAML | `components` → `templates{layout, slots, zones}` → `routes` | Pydantic (internal) | `schemaVersion: 1` |
|
||||
| **Docusaurus** | JS config | `themeConfig.navbar`, plugin sidebar config | TypeScript types | Package SemVer |
|
||||
| **Hugo** | TOML/YAML | `menu.main[]`, theme selection | Hugo schema | Release train |
|
||||
| **Jekyll** | YAML | Frontmatter per page; `_data/` nav | Conventions | SemVer |
|
||||
| **Gatsby** | JS/TS | `siteMetadata` + `plugins[]` | JS object | Major versions |
|
||||
|
||||
### Category D: Native declarative UI — reference only (5)
|
||||
|
||||
| Standard | Format | Serialised wire format? | Navigation model |
|
||||
|----------|--------|-------------------------|------------------|
|
||||
| **Jetpack Compose** | Kotlin | No | `NavHost` + routes |
|
||||
| **SwiftUI** | Swift | No | `NavigationStack` |
|
||||
| **Flutter** | Dart | No | `Router` / `Navigator` |
|
||||
| **Qt QML** | `.qml` | Yes | `StackView`, bindings |
|
||||
| **XAML** | XML | Yes (dialect-fragmented) | `Frame`, `NavigationView` |
|
||||
|
||||
### Category E: Design systems (2)
|
||||
|
||||
| Standard | Format | Theming model |
|
||||
|----------|--------|---------------|
|
||||
| **Material Design 3** | DTCG JSON | reference → system → component token tiers |
|
||||
| **Bootstrap 5.3** | SCSS → CSS vars | Sass maps → `--bs-*` custom properties |
|
||||
|
||||
### Category F: Design-to-code bridges — not source-of-truth (3)
|
||||
|
||||
| Standard | Output | Why not ADP source |
|
||||
|----------|--------|-------------------|
|
||||
| **Figma MCP** | React+Tailwind context | Proprietary; bridge only |
|
||||
| **Webflow export** | HTML/CSS/JS ZIP | No schema; paid export |
|
||||
| **Framer export** | React bundles | Platform lock-in |
|
||||
|
||||
### Category G: Component documentation (1)
|
||||
|
||||
| Standard | Format | Navigation |
|
||||
|----------|--------|------------|
|
||||
| **Storybook CSF 3** | TS modules | Sidebar via `title` path hierarchy |
|
||||
|
||||
### Category H: Configuration and packaging (12)
|
||||
|
||||
| Standard | Format | Composition mechanism | ADP borrow |
|
||||
|----------|--------|----------------------|------------|
|
||||
| **Kubernetes CRD** | YAML | `apiVersion/kind/spec`; structural OpenAPI | Structural validation |
|
||||
| **Helm** | YAML + templates | Subcharts; values merge; `values.schema.json` | Defaults + overrides |
|
||||
| **Backstage catalog** | YAML | `$yaml:`/`$json:` includes; relation graph | Envelope + includes |
|
||||
| **OpenAPI 3.1** | YAML/JSON | `$ref`, `components` registry | JSON Schema foundation |
|
||||
| **AsyncAPI 3.x** | YAML/JSON | Channels → messages | Future event UI |
|
||||
| **JSON Schema 2020-12** | JSON | `$ref`, `$defs`, conditionals | Validation dialect |
|
||||
| **CloudEvents** | JSON envelope | Fixed envelope + opaque payload | Future event wrapper |
|
||||
| **Docker Compose** | YAML | Multi-file merge; profiles | Override stacking |
|
||||
| **CUE** | CUE | Unify schema + data | Power-user compile layer |
|
||||
| **Jsonnet** | Jsonnet → JSON | `import` + merge | Theme variants |
|
||||
| **Pkl** | `.pkl` | `amends`, mixins | Typed config model |
|
||||
| **OCI Artifact** | JSON manifest | Multi-layer blobs | Bundle packaging |
|
||||
|
||||
### Category I: Agent and workflow protocols (7)
|
||||
|
||||
| Standard | Format | UI/state declaration | ADP borrow |
|
||||
|----------|--------|---------------------|------------|
|
||||
| **MCP** | JSON-RPC | Tool schemas — no UI layout | Future widget actions |
|
||||
| **AG-UI** | Streaming JSON | EventType enum; snapshots + deltas | Streaming model |
|
||||
| **A2A** | Proto → JSON-RPC | Agent Cards | Transport |
|
||||
| **AWP** | Multi-file YAML | 7 layers; rules R1–R32 | Layer + validation model |
|
||||
| **LangGraph** | workflow.yaml | Graph nodes + edges | Layout graph |
|
||||
| **AutoGen** | ComponentModel JSON | `{provider, type, version, config}` | Pluggable components |
|
||||
| **CrewAI** | JSONC/YAML | Agent arrays — weak schema | Anti-pattern |
|
||||
|
||||
### Category J: Application packaging (2)
|
||||
|
||||
| Standard | Format | Application declaration |
|
||||
|----------|--------|------------------------|
|
||||
| **OAM** | YAML | `Application` with `components[]`, `traits[]` |
|
||||
| **CNAB** | bundle.json | Parameters, credentials, actions |
|
||||
|
||||
---
|
||||
|
||||
## 4. How they do it — mechanism comparison matrix
|
||||
|
||||
| Mechanism | Standards | How it works | ADP |
|
||||
|-----------|-----------|--------------|-----|
|
||||
| **Envelope document** | K8s, Backstage, OAM, AWP | `apiVersion` + `kind` + `metadata` + `spec` | Adopt |
|
||||
| **Component registry + ref** | PraisonAIUI, OpenUI | Named map; templates use `ref:` | Adopt |
|
||||
| **Layout + slots** | PraisonAIUI, JSON Forms | Layout type + named regions | Adopt (unified) |
|
||||
| **Route → template** | PraisonAIUI, Flutter | URL glob → template | Adopt |
|
||||
| **Navigation tree** | Mintlify, Storybook | Hierarchical groups → pages | Adopt |
|
||||
| **Design token file** | DTCG, Material 3 | Token JSON with aliases | Adopt |
|
||||
| **Catalog allowlist** | A2UI, OpenUI | Closed component vocabulary | Adopt |
|
||||
| **Schema + uiSchema split** | JSON Forms, RJSF | Data vs presentation | Partial |
|
||||
| **Streaming surfaces** | A2UI, AG-UI | Incremental JSON updates | Future |
|
||||
| **Layered spec** | AWP, Helm | Separate files merged at compile | Adopt |
|
||||
| **File includes** | Backstage, Mintlify | `$yaml:` / `$ref` external files | Adopt |
|
||||
| **Structural pruning** | K8s CRD | Reject unknown fields | Adopt |
|
||||
| **Validation rules R1–Rn** | AWP | Cross-ref beyond JSON Schema | Adopt |
|
||||
| **JS executable config** | Docusaurus, Gatsby | Code config | Reject |
|
||||
| **Visual canvas** | Figma, Webflow | Proprietary designer | Reject |
|
||||
| **Code-only UI** | Compose, SwiftUI | No wire format | Out of scope |
|
||||
|
||||
---
|
||||
|
||||
## 5. Side-by-side: four closest analogues
|
||||
|
||||
| Dimension | PraisonAIUI | Mintlify | A2UI | Backstage |
|
||||
|-----------|-------------|----------|------|-----------|
|
||||
| **Authoring format** | YAML | JSON | JSON (stream) | YAML |
|
||||
| **Envelope** | `schemaVersion` only | Flat JSON | Message `version` | `apiVersion/kind/metadata/spec` |
|
||||
| **Navigation** | `navigation.tabs[]` + filesystem | `navigation.groups[]` | N/A | N/A (metadata catalog) |
|
||||
| **Components** | `components{id: type, props}` | N/A | Catalog + adjacency list | Entity kinds |
|
||||
| **Layout** | `templates{layout, slots, zones}` | Theme only | Surfaces | N/A |
|
||||
| **Routes** | `routes{match, template}` | Page paths in nav | N/A | N/A |
|
||||
| **Schema published** | Pydantic only | `$schema` URL | JSON Schema catalog | JSON Schema per kind |
|
||||
| **Runtime** | React SPA | Hosted platform | Any renderer | Catalog API |
|
||||
| **Primary use** | Docs sites | Docs sites | Agent-generated UI | Software inventory |
|
||||
|
||||
**Synthesis:** ADP combines PraisonAIUI's composition model, Mintlify's schema publishing, A2UI's catalog allowlist, and Backstage's envelope and file includes.
|
||||
|
||||
---
|
||||
|
||||
## 6. YAML vs alternatives — structured debate
|
||||
|
||||
### 6.1 The assumption
|
||||
|
||||
YAML is the best authoring format for application design manifests.
|
||||
|
||||
### 6.2 Counter-evidence
|
||||
|
||||
**Mintlify** — the closest commercial docs-site analogue — uses **JSON** (`docs.json`) with a published `$schema` URL for IDE autocomplete. This works well for flat-to-moderately-nested config but becomes verbose for deep layout trees.
|
||||
|
||||
**A2UI** uses **JSON** as both authoring and wire format because LLMs emit structured JSON reliably and ambiguity is unacceptable at runtime.
|
||||
|
||||
**Docusaurus and Gatsby** use **JavaScript/TypeScript** config for programmatic power — but this sacrifices portability and LLM generation safety.
|
||||
|
||||
### 6.3 Format comparison
|
||||
|
||||
| Format | Strengths | Weaknesses for ADP |
|
||||
|--------|-----------|-------------------|
|
||||
| **YAML** | Comments; readable nesting; LLM-friendly; PraisonAIUI continuity | Ambiguous typing; merge key footguns; no native DRY |
|
||||
| **JSON** | Unambiguous; `$schema` IDE support; Mintlify-proven | No comments; verbose for layout trees |
|
||||
| **TOML** | Clear flat tables (Hugo) | Poor for nested templates/zones |
|
||||
| **CUE / Jsonnet / Pkl** | Typed composition without templating | Learning curve; not LLM-default |
|
||||
| **HCL** | Modules, expressions | Ecosystem-bound; overkill for UI-only |
|
||||
| **JS/TS** | Full programmatic power | Not portable; not LLM-safe |
|
||||
|
||||
### 6.4 Recommendation
|
||||
|
||||
```
|
||||
Author (human/LLM) → YAML (default) or JSON (alternate)
|
||||
↓
|
||||
ADP compiler/validator (JSON Schema 2020-12)
|
||||
↓
|
||||
Normalised JSON manifest (runtime)
|
||||
↓
|
||||
UI renderer (catalog-constrained)
|
||||
```
|
||||
|
||||
- **Primary authoring:** YAML — best for nested `templates` / `zones` trees
|
||||
- **Alternate authoring:** JSON — for teams preferring Mintlify-style `$schema` tooling
|
||||
- **Never at runtime:** YAML — always compile to JSON before serve
|
||||
- **Composition:** `$yaml:` file includes (Backstage), not Helm Go templates
|
||||
- **Power users:** Optional CUE/Jsonnet compiles to YAML before validation
|
||||
|
||||
**Verdict:** YAML remains the recommended default, but ADP MUST accept JSON as an equivalent authoring format with identical semantics.
|
||||
|
||||
---
|
||||
|
||||
## 7. ADP design principles
|
||||
|
||||
1. **Declarative, not executable** — manifests describe intent; runtimes render; no inline code or templating logic in core spec.
|
||||
2. **Schema-first** — JSON Schema 2020-12 is canonical; YAML is authoring convenience.
|
||||
3. **Structural strictness** — unknown fields rejected in strict mode (K8s CRD pattern).
|
||||
4. **Catalog allowlist** — component `type` must exist in published catalog (A2UI pattern).
|
||||
5. **Envelope consistency** — every document uses `apiVersion`, `kind`, `metadata`, `spec`.
|
||||
6. **Separation of concerns** — theme, navigation, components, templates, routes are distinct spec sections.
|
||||
7. **Compile, don't interpret** — YAML → validated JSON manifest → static assets at build time.
|
||||
8. **Accessibility by default** — catalog entries include ARIA role hints (WAI-ARIA pattern).
|
||||
9. **Versioned protocol** — `schemaVersion` integer + `apiVersion` string; breaking changes bump version.
|
||||
10. **Extension namespace** — `x-adp-*` prefix reserved; plugins register catalog layers.
|
||||
11. **File composition** — `$yaml:` includes for modular configs (Backstage pattern).
|
||||
12. **Route-driven rendering** — URL resolution selects template; no hardcoded layout in renderer.
|
||||
|
||||
---
|
||||
|
||||
## 8. Proposed ADP document model
|
||||
|
||||
```yaml
|
||||
schemaVersion: 1
|
||||
apiVersion: adp.io/v1alpha1
|
||||
kind: Application
|
||||
|
||||
metadata:
|
||||
name: my-docs-site
|
||||
version: "1.0.0"
|
||||
labels: {}
|
||||
|
||||
spec:
|
||||
site:
|
||||
title: "My Site"
|
||||
description: "..."
|
||||
theme:
|
||||
preset: zinc # or DTCG token ref
|
||||
radius: md
|
||||
darkMode: true
|
||||
navigation:
|
||||
mode: auto # auto | manual
|
||||
tabs: [...]
|
||||
components:
|
||||
header_main:
|
||||
type: Header
|
||||
props: { logoText: "..." }
|
||||
templates:
|
||||
docs:
|
||||
layout: ThreeColumnLayout
|
||||
slots:
|
||||
header: { ref: header_main }
|
||||
main: { type: DocContent }
|
||||
routes:
|
||||
- match: "/docs/**"
|
||||
template: docs
|
||||
content:
|
||||
docs:
|
||||
dir: "./docs"
|
||||
```
|
||||
|
||||
### Data flow
|
||||
|
||||
```
|
||||
application.yaml → Parse → JSON Schema validate → Cross-ref rules (R1–R30)
|
||||
→ Compile → ui-config.json + route-manifest.json + docs-nav.json
|
||||
→ React / static renderer
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. PraisonAIUI alignment and gaps
|
||||
|
||||
### Already aligned
|
||||
|
||||
| PraisonAIUI today | ADP equivalent |
|
||||
|-------------------|----------------|
|
||||
| `schemaVersion: 1` | `schemaVersion` + `apiVersion` |
|
||||
| `components` + `ref:` | `spec.components` + slot refs |
|
||||
| `templates.layout` + `slots` | `spec.templates` |
|
||||
| `routes` | `spec.routes` |
|
||||
| `site.theme` | `spec.theme` |
|
||||
| Compiler → JSON manifests | ADP compile pipeline |
|
||||
| Pydantic validation | JSON Schema + cross-ref rules |
|
||||
|
||||
### Gaps to close
|
||||
|
||||
| Gap | ADP fix |
|
||||
|-----|---------|
|
||||
| No published JSON Schema | Publish `application.schema.json` |
|
||||
| `navigation.tabs` compiled but not rendered | Mandate runtime consumption in spec |
|
||||
| `route-manifest.json` not used for routing | Mandate route-driven template resolution |
|
||||
| Dual slots + zones models | Unified: zones = ordered slot arrays |
|
||||
| Closed hardcoded component types | Published `component-catalog.schema.json` |
|
||||
| No envelope (`kind`, `metadata`) | Add Backstage-style envelope |
|
||||
|
||||
---
|
||||
|
||||
## 10. Migration path
|
||||
|
||||
Existing `aiui.template.yaml` files remain valid. Migration steps:
|
||||
|
||||
1. Add `apiVersion: adp.io/v1alpha1` and `kind: Application` (optional during transition).
|
||||
2. Wrap top-level keys under `spec:` (compiler accepts both shapes during v1alpha1).
|
||||
3. Rename `site` fields unchanged; map `navigation`, `templates`, `routes` as-is.
|
||||
4. Publish catalog; register custom component types via plugin schema layers.
|
||||
|
||||
See [adp/aiui-mapping.md](./adp/aiui-mapping.md) for field-by-field mapping.
|
||||
|
||||
---
|
||||
|
||||
## 11. Open questions
|
||||
|
||||
1. **Should ADP standardise layout type names** (`ThreeColumnLayout`) or allow runtime-defined layouts?
|
||||
2. **Should navigation support both filesystem-auto and manual tabs** in one spec, or require a mode switch?
|
||||
3. **Should agent-driven surfaces (A2UI) be a separate `kind: Surface`** or embedded in Application?
|
||||
4. **Should theme reference DTCG token files directly** or use simplified preset shorthand?
|
||||
|
||||
---
|
||||
|
||||
## 12. Appendix A — Standards index
|
||||
|
||||
| # | Standard | URL |
|
||||
|---|----------|-----|
|
||||
| 1 | A2UI | https://a2ui.org/ |
|
||||
| 2 | AG-UI | https://docs.ag-ui.com/ |
|
||||
| 3 | AWP | https://github.com/veegee82/agent-workflow-protocol |
|
||||
| 4 | AsyncAPI | https://www.asyncapi.com/ |
|
||||
| 5 | Backstage catalog | https://backstage.io/docs/features/software-catalog/descriptor-format |
|
||||
| 6 | Bootstrap | https://getbootstrap.com/ |
|
||||
| 7 | CloudEvents | https://cloudevents.io/ |
|
||||
| 8 | CNAB | https://cnab.io/ |
|
||||
| 9 | CrewAI | https://docs.crewai.com/ |
|
||||
| 10 | CUE | https://cuelang.org/ |
|
||||
| 11 | Docusaurus | https://docusaurus.io/ |
|
||||
| 12 | DTCG Design Tokens | https://www.designtokens.org/ |
|
||||
| 13 | Figma Code Connect | https://developers.figma.com/docs/code-connect/ |
|
||||
| 14 | Framer | https://www.framer.com/ |
|
||||
| 15 | Gatsby | https://www.gatsbyjs.com/ |
|
||||
| 16 | Helm | https://helm.sh/ |
|
||||
| 17 | Hugo | https://gohugo.io/ |
|
||||
| 18 | Jekyll | https://jekyllrb.com/ |
|
||||
| 19 | JSON Forms | https://jsonforms.io/ |
|
||||
| 20 | JSON Schema | https://json-schema.org/ |
|
||||
| 21 | Kubernetes CRD | https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/ |
|
||||
| 22 | LangGraph | https://langchain-ai.github.io/langgraph/ |
|
||||
| 23 | Material Design 3 | https://m3.material.io/ |
|
||||
| 24 | MCP | https://modelcontextprotocol.io/ |
|
||||
| 25 | Mintlify | https://mintlify.com/docs |
|
||||
| 26 | OAM | https://oam.dev/ |
|
||||
| 27 | OCI Artifact | https://opencontainers.org/ |
|
||||
| 28 | OpenAPI | https://www.openapis.org/ |
|
||||
| 29 | OpenUI | https://openuispec.org/ |
|
||||
| 30 | Pkl | https://pkl-lang.org/ |
|
||||
| 31 | PraisonAIUI | https://github.com/MervinPraison/PraisonAIUI |
|
||||
| 32 | RJSF | https://rjsf-team.github.io/react-jsonschema-form/ |
|
||||
| 33 | Storybook CSF | https://storybook.js.org/docs/api/csf |
|
||||
| 34 | W3C ARIA APG | https://www.w3.org/WAI/ARIA/apg/ |
|
||||
| 35 | Web Components | https://web.dev/articles/declarative-shadow-dom |
|
||||
| 36 | Webflow | https://webflow.com/ |
|
||||
| 37 | AutoGen | https://microsoft.github.io/autogen/ |
|
||||
| 38 | A2A | https://google.github.io/A2A/ |
|
||||
| 39 | Jsonnet | https://jsonnet.org/ |
|
||||
| 40 | Docker Compose | https://docs.docker.com/compose/ |
|
||||
| 41 | Qt QML | https://doc.qt.io/qt-6/qmlapplications.html |
|
||||
| 42 | XAML | https://learn.microsoft.com/en-us/windows/apps/develop/platform/xaml/xaml-overview |
|
||||
| 43 | Jetpack Compose | https://developer.android.com/develop/ui/compose |
|
||||
| 44 | SwiftUI | https://developer.apple.com/xcode/swiftui/ |
|
||||
| 45 | HTML + CSS | https://html.spec.whatwg.org/ |
|
||||
| 46 | Flutter | https://flutter.dev/ |
|
||||
|
||||
---
|
||||
|
||||
## 13. Conclusion
|
||||
|
||||
ADP is feasible and largely prefigured by PraisonAIUI's existing architecture. The research across 46 standards yields a clear synthesis:
|
||||
|
||||
- **Envelope** from Backstage/K8s
|
||||
- **Composition** from PraisonAIUI (components → templates → routes)
|
||||
- **Catalog** from A2UI/OpenUI
|
||||
- **Theming** from DTCG
|
||||
- **Validation** from AWP (structural schema + R-rules)
|
||||
- **Authoring** in YAML (default) with JSON alternate (Mintlify pattern)
|
||||
|
||||
**Status:** Research draft — see [adp/implementation-status.md](./adp/implementation-status.md) for implementation gaps.
|
||||
|
||||
The draft spec, JSON Schema files, and example manifests are published alongside this report in [`docs/protocols/adp/`](./adp/).
|
||||
@@ -25,6 +25,7 @@ from praisonaiui.server import create_app
|
||||
|
||||
# Use shared seed data helper
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
from _shared.console import safe_print as print
|
||||
from _shared.seed_data import seed_demo_data
|
||||
|
||||
seed_demo_data()
|
||||
|
||||
@@ -20,6 +20,7 @@ import sys
|
||||
|
||||
# Use shared seed data helper
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
from _shared.console import safe_print as print
|
||||
from _shared.seed_data import seed_demo_data
|
||||
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import time
|
||||
import praisonaiui as aiui
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '_shared'))
|
||||
from console import safe_print as print
|
||||
from stream_bridge import StreamBridge
|
||||
|
||||
try:
|
||||
|
||||
@@ -4,9 +4,15 @@ Demonstrates ALL 16 PraisonAIUI features with real API calls.
|
||||
Run: aiui run app.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from starlette.testclient import TestClient
|
||||
from praisonaiui.server import create_app
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
from _shared.console import safe_print as print
|
||||
|
||||
|
||||
def test_all_features():
|
||||
"""Test all 16 feature APIs with real data."""
|
||||
|
||||
@@ -44,15 +44,18 @@ import sys
|
||||
# ── Import praisonaiui ──────────────────────────────────────
|
||||
import praisonaiui as aiui
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
from _shared.console import icon, safe_print
|
||||
|
||||
# ── Check gateway availability ──────────────────────────────
|
||||
try:
|
||||
from praisonaiui.integration import AIUIGateway
|
||||
from praisonaiagents import Agent
|
||||
GATEWAY_OK = True
|
||||
except ImportError as e:
|
||||
print(f"⚠️ Gateway dependencies missing: {e}")
|
||||
print(" Install with: pip install praisonai praisonaiagents")
|
||||
print(" Falling back to standalone mode.")
|
||||
safe_print(f"{icon('⚠️', '[WARN]')} Gateway dependencies missing: {e}")
|
||||
safe_print(" Install with: pip install praisonai praisonaiagents")
|
||||
safe_print(" Falling back to standalone mode.")
|
||||
GATEWAY_OK = False
|
||||
|
||||
# ── Set dashboard style ─────────────────────────────────────
|
||||
@@ -149,15 +152,15 @@ def _register_agents_in_dashboard():
|
||||
"model": agent_def["model"],
|
||||
"icon": agent_def["icon"],
|
||||
})
|
||||
print(f" ✓ Dashboard: {agent_def['icon']} {agent_def['name']}")
|
||||
safe_print(f" {icon('✓', '[OK]')} Dashboard: {agent_def['name']}")
|
||||
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────
|
||||
async def main():
|
||||
host = os.getenv("HOST", "127.0.0.1")
|
||||
if GATEWAY_OK:
|
||||
print("🚀 Starting with Gateway mode (full execution + streaming)")
|
||||
print()
|
||||
safe_print(f"{icon('🚀', '[START]')} Starting with Gateway mode (full execution + streaming)")
|
||||
safe_print()
|
||||
|
||||
# Create the gateway
|
||||
gateway = AIUIGateway(
|
||||
@@ -174,22 +177,22 @@ async def main():
|
||||
self_reflect=False,
|
||||
)
|
||||
gateway.register_agent(agent, agent_id=agent_def["agent_id"])
|
||||
print(f" ✓ Gateway: {agent_def['icon']} {agent_def['name']} ({agent_def['model']})")
|
||||
safe_print(f" {icon('✓', '[OK]')} Gateway: {agent_def['name']} ({agent_def['model']})")
|
||||
|
||||
# ── Register agents in dashboard CRUD storage ───────
|
||||
_register_agents_in_dashboard()
|
||||
|
||||
print()
|
||||
print(f" Dashboard: http://localhost:8083")
|
||||
print(f" WebSocket: ws://localhost:8083/ws")
|
||||
print(f" Inspector: http://localhost:8083 → 🔍 Feature Inspector")
|
||||
print()
|
||||
safe_print()
|
||||
safe_print(" Dashboard: http://localhost:8083")
|
||||
safe_print(" WebSocket: ws://localhost:8083/ws")
|
||||
safe_print(f" Inspector: http://localhost:8083 {icon('→', '->')} {icon('🔍', '')} Feature Inspector")
|
||||
safe_print()
|
||||
|
||||
# Start the gateway — this runs the full server
|
||||
await gateway.start()
|
||||
else:
|
||||
# Fallback to standalone mode (same as example 15)
|
||||
print("📦 Starting in standalone mode (CRUD only, no agent execution)")
|
||||
safe_print(f"{icon('📦', '[INFO]')} Starting in standalone mode (CRUD only, no agent execution)")
|
||||
from praisonaiui.server import create_app
|
||||
import uvicorn
|
||||
|
||||
|
||||
@@ -26,15 +26,18 @@ import sys
|
||||
|
||||
import praisonaiui as aiui
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
from _shared.console import icon, safe_print
|
||||
|
||||
# ── Check gateway availability ──────────────────────────────
|
||||
try:
|
||||
from praisonaiui.integration import AIUIGateway
|
||||
from praisonaiagents import Agent
|
||||
GATEWAY_OK = True
|
||||
except ImportError as e:
|
||||
print(f"⚠️ Gateway dependencies not found: {e}")
|
||||
print(" Install with: pip install praisonai praisonaiagents")
|
||||
print(" Falling back to standalone mode (REST APIs only).\n")
|
||||
safe_print(f"{icon('⚠️', '[WARN]')} Gateway dependencies not found: {e}")
|
||||
safe_print(" Install with: pip install praisonai praisonaiagents")
|
||||
safe_print(" Falling back to standalone mode (REST APIs only).\n")
|
||||
GATEWAY_OK = False
|
||||
|
||||
aiui.set_style("dashboard")
|
||||
@@ -115,7 +118,7 @@ def _register_agents_in_dashboard():
|
||||
"model": agent_def["model"],
|
||||
"icon": agent_def["icon"],
|
||||
})
|
||||
print(f" ✓ Agent: {agent_def['icon']} {agent_def['name']}")
|
||||
safe_print(f" {icon('✓', '[OK]')} Agent: {agent_def['name']}")
|
||||
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────
|
||||
@@ -125,8 +128,8 @@ async def main():
|
||||
host = os.getenv("HOST", "127.0.0.1")
|
||||
|
||||
if GATEWAY_OK:
|
||||
print("🚀 Starting with Gateway mode (full execution + streaming)")
|
||||
print()
|
||||
safe_print(f"{icon('🚀', '[START]')} Starting with Gateway mode (full execution + streaming)")
|
||||
safe_print()
|
||||
|
||||
gateway = AIUIGateway(host=host, port=port)
|
||||
|
||||
@@ -139,32 +142,32 @@ async def main():
|
||||
reflection=False,
|
||||
)
|
||||
gateway.register_agent(agent, agent_id=agent_def["agent_id"])
|
||||
print(f" ✓ Gateway: {agent_def['icon']} {agent_def['name']} ({agent_def['model']})")
|
||||
safe_print(f" {icon('✓', '[OK]')} Gateway: {agent_def['name']} ({agent_def['model']})")
|
||||
|
||||
# Also register in dashboard CRUD
|
||||
_register_agents_in_dashboard()
|
||||
|
||||
print()
|
||||
print(f" Dashboard: http://localhost:{port}")
|
||||
print(f" Explorer: http://localhost:{port} → 🔬 Feature Explorer")
|
||||
print(f" WebSocket: ws://localhost:{port}/ws")
|
||||
print()
|
||||
safe_print()
|
||||
safe_print(f" Dashboard: http://localhost:{port}")
|
||||
safe_print(f" Explorer: http://localhost:{port} {icon('→', '->')} {icon('🔬', '')} Feature Explorer")
|
||||
safe_print(f" WebSocket: ws://localhost:{port}/ws")
|
||||
safe_print()
|
||||
|
||||
await gateway.start()
|
||||
else:
|
||||
# Standalone: REST APIs only, no agent execution
|
||||
print("📦 Starting in standalone mode (REST APIs only, no agent execution)")
|
||||
safe_print(f"{icon('📦', '[INFO]')} Starting in standalone mode (REST APIs only, no agent execution)")
|
||||
from praisonaiui.server import create_app
|
||||
import uvicorn
|
||||
|
||||
_register_agents_in_dashboard()
|
||||
|
||||
app = create_app()
|
||||
print()
|
||||
print(f" Dashboard: http://localhost:{port}")
|
||||
print(f" Explorer: http://localhost:{port} → 🔬 Feature Explorer")
|
||||
print(f" Note: POST endpoints may fail without gateway")
|
||||
print()
|
||||
safe_print()
|
||||
safe_print(f" Dashboard: http://localhost:{port}")
|
||||
safe_print(f" Explorer: http://localhost:{port} {icon('→', '->')} {icon('🔬', '')} Feature Explorer")
|
||||
safe_print(" Note: POST endpoints may fail without gateway")
|
||||
safe_print()
|
||||
|
||||
config = uvicorn.Config(app, host=host, port=port, log_level="info")
|
||||
server = uvicorn.Server(config)
|
||||
|
||||
@@ -25,9 +25,13 @@ Usage:
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
import praisonaiui as aiui
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
from _shared.console import icon, safe_print
|
||||
|
||||
# ── Check gateway availability ──────────────────────────────
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
@@ -35,9 +39,9 @@ try:
|
||||
from praisonaiui.integration import AIUIGateway
|
||||
GATEWAY_OK = True
|
||||
except ImportError as e:
|
||||
print(f"⚠️ Gateway dependencies not found: {e}")
|
||||
print(" Install with: pip install praisonai praisonaiagents")
|
||||
print(" Falling back to standalone mode (REST APIs only).\n")
|
||||
safe_print(f"{icon('⚠️', '[WARN]')} Gateway dependencies not found: {e}")
|
||||
safe_print(" Install with: pip install praisonai praisonaiagents")
|
||||
safe_print(" Falling back to standalone mode (REST APIs only).\n")
|
||||
GATEWAY_OK = False
|
||||
|
||||
aiui.set_style("dashboard")
|
||||
@@ -127,7 +131,7 @@ def _register_agents_in_dashboard():
|
||||
"model": agent_def["model"],
|
||||
"icon": agent_def["icon"],
|
||||
})
|
||||
print(f" ✓ Agent: {agent_def['icon']} {agent_def['name']}")
|
||||
safe_print(f" {icon('✓', '[OK]')} Agent: {agent_def['name']}")
|
||||
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────
|
||||
@@ -137,8 +141,8 @@ async def main():
|
||||
host = os.getenv("HOST", "127.0.0.1")
|
||||
|
||||
if GATEWAY_OK:
|
||||
print("🚀 Starting with Gateway mode (full execution + streaming)")
|
||||
print()
|
||||
safe_print(f"{icon('🚀', '[START]')} Starting with Gateway mode (full execution + streaming)")
|
||||
safe_print()
|
||||
|
||||
gateway = AIUIGateway(host=host, port=port)
|
||||
|
||||
@@ -151,21 +155,21 @@ async def main():
|
||||
reflection=False,
|
||||
)
|
||||
gateway.register_agent(agent, agent_id=agent_def["agent_id"])
|
||||
print(f" ✓ Gateway: {agent_def['icon']} {agent_def['name']} ({agent_def['model']})")
|
||||
safe_print(f" {icon('✓', '[OK]')} Gateway: {agent_def['name']} ({agent_def['model']})")
|
||||
|
||||
# Also register in dashboard CRUD
|
||||
_register_agents_in_dashboard()
|
||||
|
||||
print()
|
||||
print(f" Dashboard: http://localhost:{port}")
|
||||
print(f" Explorer: http://localhost:{port} → 🔬 Feature Explorer")
|
||||
print(f" WebSocket: ws://localhost:{port}/ws")
|
||||
print()
|
||||
safe_print()
|
||||
safe_print(f" Dashboard: http://localhost:{port}")
|
||||
safe_print(f" Explorer: http://localhost:{port} {icon('→', '->')} {icon('🔬', '')} Feature Explorer")
|
||||
safe_print(f" WebSocket: ws://localhost:{port}/ws")
|
||||
safe_print()
|
||||
|
||||
await gateway.start()
|
||||
else:
|
||||
# Standalone: REST APIs only, no agent execution
|
||||
print("📦 Starting in standalone mode (REST APIs only, no agent execution)")
|
||||
safe_print(f"{icon('📦', '[INFO]')} Starting in standalone mode (REST APIs only, no agent execution)")
|
||||
import uvicorn
|
||||
|
||||
from praisonaiui.server import create_app
|
||||
@@ -173,11 +177,11 @@ async def main():
|
||||
_register_agents_in_dashboard()
|
||||
|
||||
app = create_app()
|
||||
print()
|
||||
print(f" Dashboard: http://localhost:{port}")
|
||||
print(f" Explorer: http://localhost:{port} → 🔬 Feature Explorer")
|
||||
print(" Note: POST endpoints may fail without gateway")
|
||||
print()
|
||||
safe_print()
|
||||
safe_print(f" Dashboard: http://localhost:{port}")
|
||||
safe_print(f" Explorer: http://localhost:{port} {icon('→', '->')} {icon('🔬', '')} Feature Explorer")
|
||||
safe_print(" Note: POST endpoints may fail without gateway")
|
||||
safe_print()
|
||||
|
||||
config = uvicorn.Config(app, host=host, port=port, log_level="info")
|
||||
server = uvicorn.Server(config)
|
||||
|
||||
@@ -32,6 +32,9 @@ import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
from _shared.console import icon, safe_print
|
||||
|
||||
# ── Check prerequisites ─────────────────────────────────────────────
|
||||
|
||||
try:
|
||||
@@ -40,8 +43,8 @@ try:
|
||||
SDK_OK = True
|
||||
except ImportError:
|
||||
SDK_OK = False
|
||||
print("⚠️ Missing dependencies. Install with:")
|
||||
print(" pip install praisonai[email] praisonaiagents")
|
||||
safe_print(f"{icon('⚠️', '[WARN]')} Missing dependencies. Install with:")
|
||||
safe_print(" pip install praisonai[email] praisonaiagents")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -68,8 +71,8 @@ agent = Agent(
|
||||
async def run_simple():
|
||||
"""Start email bot with automatic env var resolution."""
|
||||
bot = Bot("email", agent=agent)
|
||||
print(f"📧 Starting email bot for: {os.getenv('EMAIL_ADDRESS', '(not set)')}")
|
||||
print(" Listening for new emails... (Ctrl+C to stop)")
|
||||
safe_print(f"{icon('📧', '[EMAIL]')} Starting email bot for: {os.getenv('EMAIL_ADDRESS', '(not set)')}")
|
||||
safe_print(" Listening for new emails... (Ctrl+C to stop)")
|
||||
await bot.start()
|
||||
|
||||
|
||||
@@ -96,9 +99,9 @@ async def run_with_handlers():
|
||||
@bot.on_message
|
||||
async def log_email(message):
|
||||
subject = message.metadata.get("subject", "(no subject)")
|
||||
print(f"📨 From: {message.sender.username}")
|
||||
print(f" Subject: {subject}")
|
||||
print(f" Preview: {message.text[:100]}...")
|
||||
safe_print(f"{icon('📨', '[MSG]')} From: {message.sender.username}")
|
||||
safe_print(f" Subject: {subject}")
|
||||
safe_print(f" Preview: {message.text[:100]}...")
|
||||
|
||||
# Custom command: emails with subject starting with "/status"
|
||||
@bot.on_command("status")
|
||||
@@ -118,11 +121,11 @@ async def run_with_handlers():
|
||||
reply_to=message.message_id,
|
||||
)
|
||||
|
||||
print(f"📧 Starting email bot with custom handlers")
|
||||
print(f" Email: {bot._email_address}")
|
||||
print(f" Poll interval: {config.polling_interval}s")
|
||||
print(f" Allowed senders: {'all' if not config.allowed_users else config.allowed_users}")
|
||||
print(" Listening... (Ctrl+C to stop)")
|
||||
safe_print(f"{icon('📧', '[EMAIL]')} Starting email bot with custom handlers")
|
||||
safe_print(f" Email: {bot._email_address}")
|
||||
safe_print(f" Poll interval: {config.polling_interval}s")
|
||||
safe_print(f" Allowed senders: {'all' if not config.allowed_users else config.allowed_users}")
|
||||
safe_print(" Listening... (Ctrl+C to stop)")
|
||||
await bot.start()
|
||||
|
||||
|
||||
@@ -131,17 +134,17 @@ async def run_with_handlers():
|
||||
async def run_probe():
|
||||
"""Test email server connectivity."""
|
||||
bot = Bot("email", agent=agent)
|
||||
print("🔍 Testing email server connectivity...")
|
||||
safe_print(f"{icon('🔍', '[PROBE]')} Testing email server connectivity...")
|
||||
result = await bot.probe()
|
||||
|
||||
print(f"\n Connection: {'✅ OK' if result.ok else '❌ FAILED'}")
|
||||
print(f" Platform: {result.platform}")
|
||||
safe_print(f"\n Connection: {icon('✅', '[OK]') + ' OK' if result.ok else icon('❌', '[FAIL]') + ' FAILED'}")
|
||||
safe_print(f" Platform: {result.platform}")
|
||||
if result.bot_username:
|
||||
print(f" Email: {result.bot_username}")
|
||||
safe_print(f" Email: {result.bot_username}")
|
||||
if result.elapsed_ms:
|
||||
print(f" Latency: {result.elapsed_ms:.0f}ms")
|
||||
safe_print(f" Latency: {result.elapsed_ms:.0f}ms")
|
||||
if result.error:
|
||||
print(f" Error: {result.error}")
|
||||
safe_print(f" Error: {result.error}")
|
||||
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -42,7 +42,9 @@ import sys
|
||||
# ── Imports ─────────────────────────────────────────────────────────
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "src"))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from _shared.console import icon, safe_print
|
||||
from praisonaiui.server import create_app
|
||||
import uvicorn
|
||||
|
||||
@@ -91,11 +93,11 @@ def seed_email_channel():
|
||||
})
|
||||
if r.status_code in (200, 201):
|
||||
channel_id = r.json().get("id", "unknown")
|
||||
print(f" ✅ Email channel registered: {channel_id}")
|
||||
safe_print(f" {icon('✅', '[OK]')} Email channel registered: {channel_id}")
|
||||
else:
|
||||
print(f" ⚠️ Channel registration: {r.status_code} — {r.json()}")
|
||||
safe_print(f" {icon('⚠️', '[WARN]')} Channel registration: {r.status_code} - {r.json()}")
|
||||
else:
|
||||
print(" ✓ Email channel already exists")
|
||||
safe_print(f" {icon('✓', '[OK]')} Email channel already exists")
|
||||
|
||||
# Also seed other channels for comparison in dashboard
|
||||
for name, platform in [("Discord #general", "discord"), ("Telegram Support", "telegram")]:
|
||||
@@ -106,7 +108,7 @@ def seed_email_channel():
|
||||
"platform": platform,
|
||||
"config": {},
|
||||
})
|
||||
print(f" ✓ Seeded {platform} channel")
|
||||
safe_print(f" {icon('✓', '[OK]')} Seeded {platform} channel")
|
||||
|
||||
|
||||
# ── Verify email platform support ──────────────────────────────────
|
||||
@@ -122,8 +124,8 @@ def verify_email_support():
|
||||
if r.status_code == 200:
|
||||
platforms = r.json().get("platforms", [])
|
||||
email_supported = "email" in platforms
|
||||
print(f" Supported platforms: {platforms}")
|
||||
print(f" Email supported: {'✅ Yes' if email_supported else '❌ No — add email to SUPPORTED_PLATFORMS'}")
|
||||
safe_print(f" Supported platforms: {platforms}")
|
||||
safe_print(f" Email supported: {icon('✅', '[OK]') + ' Yes' if email_supported else icon('❌', '[X]') + ' No - add email to SUPPORTED_PLATFORMS'}")
|
||||
return email_supported
|
||||
return False
|
||||
|
||||
@@ -137,51 +139,51 @@ def test_channel_api():
|
||||
app = create_app()
|
||||
client = TestClient(app)
|
||||
|
||||
print("\n📋 Channel API Test")
|
||||
print("=" * 40)
|
||||
safe_print(f"\n{icon('📋', '[TEST]')} Channel API Test")
|
||||
safe_print("=" * 40)
|
||||
|
||||
# List channels
|
||||
r = client.get("/api/channels")
|
||||
channels = r.json().get("channels", [])
|
||||
print(f" GET /api/channels: {len(channels)} channels")
|
||||
safe_print(f" GET /api/channels: {len(channels)} channels")
|
||||
for ch in channels:
|
||||
icon = "📧" if ch.get("platform") == "email" else "💬"
|
||||
print(f" {icon} {ch.get('name', '?')} ({ch.get('platform', '?')})")
|
||||
ch_icon = icon("📧", "[email]") if ch.get("platform") == "email" else icon("💬", "[chat]")
|
||||
safe_print(f" {ch_icon} {ch.get('name', '?')} ({ch.get('platform', '?')})")
|
||||
|
||||
# Get email channel status
|
||||
email_ch = next((ch for ch in channels if ch.get("platform") == "email"), None)
|
||||
if email_ch:
|
||||
ch_id = email_ch.get("id")
|
||||
r = client.get(f"/api/channels/{ch_id}/status")
|
||||
print(f" GET /api/channels/{ch_id}/status: {r.json()}")
|
||||
safe_print(f" GET /api/channels/{ch_id}/status: {r.json()}")
|
||||
|
||||
print()
|
||||
safe_print()
|
||||
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────────────
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("📧 PraisonAIUI — Email Channel Integration")
|
||||
print("=" * 50)
|
||||
print()
|
||||
safe_print(f"{icon('📧', '[EMAIL]')} PraisonAIUI - Email Channel Integration")
|
||||
safe_print("=" * 50)
|
||||
safe_print()
|
||||
|
||||
if "--test" in sys.argv:
|
||||
# Test mode: verify APIs + seed data
|
||||
print("🔍 Verifying email platform support...")
|
||||
safe_print(f"{icon('🔍', '[VERIFY]')} Verifying email platform support...")
|
||||
verify_email_support()
|
||||
print()
|
||||
print("🌱 Seeding email channel...")
|
||||
safe_print()
|
||||
safe_print(f"{icon('🌱', '[SEED]')} Seeding email channel...")
|
||||
seed_email_channel()
|
||||
test_channel_api()
|
||||
else:
|
||||
# Run the dashboard
|
||||
print("🌱 Seeding channels...")
|
||||
safe_print(f"{icon('🌱', '[SEED]')} Seeding channels...")
|
||||
seed_email_channel()
|
||||
print()
|
||||
safe_print()
|
||||
|
||||
app = create_app()
|
||||
print(f"✅ Dashboard at http://localhost:8084")
|
||||
print(f" Channels: http://localhost:8084/api/channels")
|
||||
print(f" Platforms: http://localhost:8084/api/channels/platforms")
|
||||
safe_print(f"{icon('✅', '[OK]')} Dashboard at http://localhost:8084")
|
||||
safe_print(" Channels: http://localhost:8084/api/channels")
|
||||
safe_print(" Platforms: http://localhost:8084/api/channels/platforms")
|
||||
host = os.getenv("HOST", "127.0.0.1")
|
||||
uvicorn.run(app, host=host, port=8084, log_level="info")
|
||||
@@ -33,6 +33,9 @@ import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
from _shared.console import icon, safe_print
|
||||
|
||||
# ── Check prerequisites ─────────────────────────────────────────────
|
||||
|
||||
try:
|
||||
@@ -41,8 +44,8 @@ try:
|
||||
SDK_OK = True
|
||||
except ImportError:
|
||||
SDK_OK = False
|
||||
print("⚠️ Missing dependencies. Install with:")
|
||||
print(" pip install praisonai agentmail praisonaiagents")
|
||||
safe_print(f"{icon('⚠️', '[WARN]')} Missing dependencies. Install with:")
|
||||
safe_print(" pip install praisonai agentmail praisonaiagents")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -69,9 +72,9 @@ agent = Agent(
|
||||
async def run_simple():
|
||||
"""Start agentmail bot with automatic env var resolution."""
|
||||
bot = Bot("agentmail", agent=agent)
|
||||
print(f"📩 Starting AgentMail bot")
|
||||
print(f" API key: {'✅ set' if os.getenv('AGENTMAIL_API_KEY') else '❌ not set'}")
|
||||
print(" Listening for new emails... (Ctrl+C to stop)")
|
||||
safe_print(f"{icon('📩', '[MAIL]')} Starting AgentMail bot")
|
||||
safe_print(f" API key: {icon('✅', '[OK]') + ' set' if os.getenv('AGENTMAIL_API_KEY') else icon('❌', '[X]') + ' not set'}")
|
||||
safe_print(" Listening for new emails... (Ctrl+C to stop)")
|
||||
await bot.start()
|
||||
|
||||
|
||||
@@ -99,9 +102,9 @@ async def run_with_handlers():
|
||||
@bot.on_message
|
||||
async def log_email(message):
|
||||
subject = message.metadata.get("subject", "(no subject)")
|
||||
print(f"📨 From: {message.sender.username}")
|
||||
print(f" Subject: {subject}")
|
||||
print(f" Preview: {message.text[:100]}...")
|
||||
safe_print(f"{icon('📨', '[MSG]')} From: {message.sender.username}")
|
||||
safe_print(f" Subject: {subject}")
|
||||
safe_print(f" Preview: {message.text[:100]}...")
|
||||
|
||||
# Custom command: emails with subject starting with "/status"
|
||||
@bot.on_command("status")
|
||||
@@ -121,10 +124,10 @@ async def run_with_handlers():
|
||||
reply_to=message.message_id,
|
||||
)
|
||||
|
||||
print(f"📩 Starting AgentMail bot with custom handlers")
|
||||
print(f" Email: {bot.email_address or '(will be assigned on start)'}")
|
||||
print(f" Poll interval: {config.polling_interval}s")
|
||||
print(" Listening... (Ctrl+C to stop)")
|
||||
safe_print(f"{icon('📩', '[MAIL]')} Starting AgentMail bot with custom handlers")
|
||||
safe_print(f" Email: {bot.email_address or '(will be assigned on start)'}")
|
||||
safe_print(f" Poll interval: {config.polling_interval}s")
|
||||
safe_print(" Listening... (Ctrl+C to stop)")
|
||||
await bot.start()
|
||||
|
||||
|
||||
@@ -136,30 +139,30 @@ async def run_inbox_demo():
|
||||
token=os.environ.get("AGENTMAIL_API_KEY", ""),
|
||||
)
|
||||
|
||||
print("📩 AgentMail Inbox Lifecycle Demo")
|
||||
print("=" * 50)
|
||||
safe_print(f"{icon('📩', '[MAIL]')} AgentMail Inbox Lifecycle Demo")
|
||||
safe_print("=" * 50)
|
||||
|
||||
# Create inbox
|
||||
print("\n1. Creating inbox...")
|
||||
safe_print("\n1. Creating inbox...")
|
||||
inbox = await bot.create_inbox(domain=os.getenv("AGENTMAIL_DOMAIN"))
|
||||
print(f" ✅ Created: {inbox.get('email_address', 'unknown')}")
|
||||
print(f" ID: {inbox.get('id', 'unknown')}")
|
||||
safe_print(f" {icon('✅', '[OK]')} Created: {inbox.get('email_address', 'unknown')}")
|
||||
safe_print(f" ID: {inbox.get('id', 'unknown')}")
|
||||
|
||||
# List inboxes
|
||||
print("\n2. Listing inboxes...")
|
||||
safe_print("\n2. Listing inboxes...")
|
||||
inboxes = await bot.list_inboxes()
|
||||
print(f" Found: {len(inboxes)} inbox(es)")
|
||||
safe_print(f" Found: {len(inboxes)} inbox(es)")
|
||||
for ib in inboxes:
|
||||
print(f" 📧 {ib.get('email_address', '?')} (id: {ib.get('id', '?')})")
|
||||
safe_print(f" {icon('📧', '[EMAIL]')} {ib.get('email_address', '?')} (id: {ib.get('id', '?')})")
|
||||
|
||||
# Delete inbox
|
||||
inbox_id = inbox.get("id", "")
|
||||
if inbox_id:
|
||||
print(f"\n3. Deleting inbox {inbox_id}...")
|
||||
safe_print(f"\n3. Deleting inbox {inbox_id}...")
|
||||
deleted = await bot.delete_inbox(inbox_id)
|
||||
print(f" {'✅ Deleted' if deleted else '❌ Failed to delete'}")
|
||||
safe_print(f" {icon('✅', '[OK]') + ' Deleted' if deleted else icon('❌', '[X]') + ' Failed to delete'}")
|
||||
|
||||
print("\n✅ Inbox lifecycle complete")
|
||||
safe_print(f"\n{icon('✅', '[OK]')} Inbox lifecycle complete")
|
||||
|
||||
|
||||
# ── Probe: Test connectivity ────────────────────────────────────────
|
||||
@@ -167,17 +170,17 @@ async def run_inbox_demo():
|
||||
async def run_probe():
|
||||
"""Test AgentMail API connectivity."""
|
||||
bot = Bot("agentmail", agent=agent)
|
||||
print("🔍 Testing AgentMail API connectivity...")
|
||||
safe_print(f"{icon('🔍', '[PROBE]')} Testing AgentMail API connectivity...")
|
||||
result = await bot.probe()
|
||||
|
||||
print(f"\n Connection: {'✅ OK' if result.ok else '❌ FAILED'}")
|
||||
print(f" Platform: {result.platform}")
|
||||
safe_print(f"\n Connection: {icon('✅', '[OK]') + ' OK' if result.ok else icon('❌', '[FAIL]') + ' FAILED'}")
|
||||
safe_print(f" Platform: {result.platform}")
|
||||
if result.bot_username:
|
||||
print(f" Email: {result.bot_username}")
|
||||
safe_print(f" Email: {result.bot_username}")
|
||||
if result.elapsed_ms:
|
||||
print(f" Latency: {result.elapsed_ms:.0f}ms")
|
||||
safe_print(f" Latency: {result.elapsed_ms:.0f}ms")
|
||||
if result.error:
|
||||
print(f" Error: {result.error}")
|
||||
safe_print(f" Error: {result.error}")
|
||||
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -40,7 +40,9 @@ import sys
|
||||
# ── Imports ─────────────────────────────────────────────────────────
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "src"))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from _shared.console import icon, safe_print
|
||||
from praisonaiui.server import create_app
|
||||
import uvicorn
|
||||
|
||||
@@ -88,11 +90,11 @@ def seed_agentmail_channel():
|
||||
})
|
||||
if r.status_code in (200, 201):
|
||||
channel_id = r.json().get("id", "unknown")
|
||||
print(f" ✅ AgentMail channel registered: {channel_id}")
|
||||
safe_print(f" {icon('✅', '[OK]')} AgentMail channel registered: {channel_id}")
|
||||
else:
|
||||
print(f" ⚠️ Channel registration: {r.status_code} — {r.json()}")
|
||||
safe_print(f" {icon('⚠️', '[WARN]')} Channel registration: {r.status_code} - {r.json()}")
|
||||
else:
|
||||
print(" ✓ AgentMail channel already exists")
|
||||
safe_print(f" {icon('✓', '[OK]')} AgentMail channel already exists")
|
||||
|
||||
# Also seed email + telegram for comparison in dashboard
|
||||
for name, platform in [("Support Email", "email"), ("Telegram Support", "telegram")]:
|
||||
@@ -103,7 +105,7 @@ def seed_agentmail_channel():
|
||||
"platform": platform,
|
||||
"config": {},
|
||||
})
|
||||
print(f" ✓ Seeded {platform} channel")
|
||||
safe_print(f" {icon('✓', '[OK]')} Seeded {platform} channel")
|
||||
|
||||
|
||||
# ── Verify agentmail platform support ──────────────────────────────
|
||||
@@ -120,9 +122,9 @@ def verify_agentmail_support():
|
||||
platforms = r.json().get("platforms", [])
|
||||
am_supported = "agentmail" in platforms
|
||||
email_supported = "email" in platforms
|
||||
print(f" Supported platforms: {platforms}")
|
||||
print(f" email supported: {'✅ Yes' if email_supported else '❌ No'}")
|
||||
print(f" agentmail supported: {'✅ Yes' if am_supported else '❌ No'}")
|
||||
safe_print(f" Supported platforms: {platforms}")
|
||||
safe_print(f" email supported: {icon('✅', '[OK]') + ' Yes' if email_supported else icon('❌', '[X]') + ' No'}")
|
||||
safe_print(f" agentmail supported: {icon('✅', '[OK]') + ' Yes' if am_supported else icon('❌', '[X]') + ' No'}")
|
||||
return am_supported
|
||||
return False
|
||||
|
||||
@@ -136,51 +138,51 @@ def test_channel_api():
|
||||
app = create_app()
|
||||
client = TestClient(app)
|
||||
|
||||
print("\n📋 Channel API Test")
|
||||
print("=" * 40)
|
||||
safe_print(f"\n{icon('📋', '[TEST]')} Channel API Test")
|
||||
safe_print("=" * 40)
|
||||
|
||||
# List channels
|
||||
r = client.get("/api/channels")
|
||||
channels = r.json().get("channels", [])
|
||||
print(f" GET /api/channels: {len(channels)} channels")
|
||||
safe_print(f" GET /api/channels: {len(channels)} channels")
|
||||
for ch in channels:
|
||||
icon = {"email": "📧", "agentmail": "📩"}.get(ch.get("platform", ""), "💬")
|
||||
print(f" {icon} {ch.get('name', '?')} ({ch.get('platform', '?')})")
|
||||
ch_icon = {"email": icon("📧", "[email]"), "agentmail": icon("📩", "[mail]")}.get(ch.get("platform", ""), icon("💬", "[chat]"))
|
||||
safe_print(f" {ch_icon} {ch.get('name', '?')} ({ch.get('platform', '?')})")
|
||||
|
||||
# Get agentmail channel status
|
||||
am_ch = next((ch for ch in channels if ch.get("platform") == "agentmail"), None)
|
||||
if am_ch:
|
||||
ch_id = am_ch.get("id")
|
||||
r = client.get(f"/api/channels/{ch_id}/status")
|
||||
print(f" GET /api/channels/{ch_id}/status: {r.json()}")
|
||||
safe_print(f" GET /api/channels/{ch_id}/status: {r.json()}")
|
||||
|
||||
print()
|
||||
safe_print()
|
||||
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────────────
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("📩 PraisonAIUI — AgentMail Channel Integration")
|
||||
print("=" * 50)
|
||||
print()
|
||||
safe_print(f"{icon('📩', '[MAIL]')} PraisonAIUI - AgentMail Channel Integration")
|
||||
safe_print("=" * 50)
|
||||
safe_print()
|
||||
|
||||
if "--test" in sys.argv:
|
||||
# Test mode: verify APIs + seed data
|
||||
print("🔍 Verifying agentmail platform support...")
|
||||
safe_print(f"{icon('🔍', '[VERIFY]')} Verifying agentmail platform support...")
|
||||
verify_agentmail_support()
|
||||
print()
|
||||
print("🌱 Seeding agentmail channel...")
|
||||
safe_print()
|
||||
safe_print(f"{icon('🌱', '[SEED]')} Seeding agentmail channel...")
|
||||
seed_agentmail_channel()
|
||||
test_channel_api()
|
||||
else:
|
||||
# Run the dashboard
|
||||
print("🌱 Seeding channels...")
|
||||
safe_print(f"{icon('🌱', '[SEED]')} Seeding channels...")
|
||||
seed_agentmail_channel()
|
||||
print()
|
||||
safe_print()
|
||||
|
||||
app = create_app()
|
||||
print(f"✅ Dashboard at http://localhost:8084")
|
||||
print(f" Channels: http://localhost:8084/api/channels")
|
||||
print(f" Platforms: http://localhost:8084/api/channels/platforms")
|
||||
safe_print(f"{icon('✅', '[OK]')} Dashboard at http://localhost:8084")
|
||||
safe_print(" Channels: http://localhost:8084/api/channels")
|
||||
safe_print(" Platforms: http://localhost:8084/api/channels/platforms")
|
||||
host = os.getenv("HOST", "127.0.0.1")
|
||||
uvicorn.run(app, host=host, port=8084, log_level="info")
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Cross-platform console output for examples.
|
||||
|
||||
Legacy Windows consoles default stdout to a non-UTF code page (cp1252/cp437)
|
||||
that cannot encode emoji such as U+1F680. Calling ``print`` with those
|
||||
characters raises ``UnicodeEncodeError`` and crashes the example before the
|
||||
server starts. These helpers mirror the ``_supports_unicode``/``_icon`` pattern
|
||||
in ``praisonaiui.cli`` so examples degrade to ASCII instead of crashing while
|
||||
preserving emoji on UTF-aware terminals.
|
||||
|
||||
Usage:
|
||||
from _shared.console import safe_print, icon
|
||||
|
||||
safe_print(f"{icon('🚀', '[START]')} Starting gateway...")
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
def supports_unicode() -> bool:
|
||||
try:
|
||||
encoding = sys.stdout.encoding or ""
|
||||
except Exception:
|
||||
return False
|
||||
return "utf" in encoding.lower()
|
||||
|
||||
|
||||
def icon(symbol: str, fallback: str) -> str:
|
||||
return symbol if supports_unicode() else fallback
|
||||
|
||||
|
||||
def safe_print(*args: Any, **kwargs: Any) -> None:
|
||||
sep = kwargs.pop("sep", " ")
|
||||
text = sep.join(str(a) for a in args)
|
||||
try:
|
||||
print(text, **kwargs)
|
||||
except UnicodeEncodeError:
|
||||
print(text.encode("ascii", errors="replace").decode("ascii"), **kwargs)
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "aiui"
|
||||
version = "0.3.129"
|
||||
version = "0.3.142"
|
||||
description = "YAML-driven website generator - CLI and compiler"
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
|
||||
+89
-58
@@ -3,13 +3,14 @@ import { applyTheme } from './themes'
|
||||
import './index.css'
|
||||
import type { UIConfig, DocsNav, RouteManifest, NavItem } from './types'
|
||||
import { Header } from './Header'
|
||||
import { Sidebar } from './Sidebar'
|
||||
import { Sidebar, MobileNavSheet } from './Sidebar'
|
||||
import { Content } from './Content'
|
||||
import { ZoneWidgets } from './Widgets'
|
||||
import { Toc } from './Toc'
|
||||
import { Footer } from './Footer'
|
||||
import { ChatLayout, AgentUILayout, CopilotWidget, PlaygroundLayout } from './layouts'
|
||||
import { LocaleProvider } from './i18n'
|
||||
import { resolveTemplate, shouldShowToc } from './resolver'
|
||||
|
||||
function SkipLink({ enabled }: { enabled?: boolean }) {
|
||||
if (!enabled) return null
|
||||
@@ -31,12 +32,17 @@ export default function App() {
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [selectedItem, setSelectedItem] = useState<NavItem | null>(null)
|
||||
const [activeItemPath, setActiveItemPath] = useState<string>('')
|
||||
const [currentPath, setCurrentPath] = useState(() => window.location.pathname.replace(/\/$/, '') || '/')
|
||||
const [mobileNavOpen, setMobileNavOpen] = useState(false)
|
||||
|
||||
// Helper function to find nav item by path
|
||||
// Helper function to find nav item by path (searches top-level items and nested children)
|
||||
const findNavItemByPath = (navData: DocsNav, path: string): NavItem | null => {
|
||||
const normalizedPath = path.replace(/\/$/, '') || '/'
|
||||
|
||||
const findItem = (items: NavItem[]): NavItem | null => {
|
||||
for (const item of items) {
|
||||
if (item.path === path || item.path === path.replace(/^\//, '')) {
|
||||
const itemPath = (item.path ?? '').replace(/\/$/, '')
|
||||
if (itemPath === normalizedPath || itemPath === normalizedPath.replace(/^\//, '')) {
|
||||
return item
|
||||
}
|
||||
if (item.children) {
|
||||
@@ -47,23 +53,14 @@ export default function App() {
|
||||
return null
|
||||
}
|
||||
|
||||
// Search all nav groups
|
||||
if (navData.items) {
|
||||
for (const group of navData.items) {
|
||||
if (group.children) {
|
||||
const found = findItem(group.children)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
return navData.items ? findItem(navData.items) : null
|
||||
}
|
||||
|
||||
// Update SEO meta tags dynamically
|
||||
const updateSEO = (title: string, path: string, description?: string) => {
|
||||
const updateSEO = (title: string, path: string, description?: string, configOverride?: UIConfig) => {
|
||||
const cfg = configOverride ?? config
|
||||
// Update title using SEO titleTemplate if available
|
||||
const titleTemplate = config.seo?.titleTemplate || '%s | %s'
|
||||
const siteName = config.site?.title || 'Documentation'
|
||||
const titleTemplate = cfg.seo?.titleTemplate || '%s | %s'
|
||||
const siteName = cfg.site?.title || 'Documentation'
|
||||
if (titleTemplate.includes('%s')) {
|
||||
// Replace first %s with page title, second %s (if exists) with site name
|
||||
let formattedTitle = titleTemplate.replace('%s', title)
|
||||
@@ -91,7 +88,7 @@ export default function App() {
|
||||
metaDesc.name = 'description'
|
||||
document.head.appendChild(metaDesc)
|
||||
}
|
||||
metaDesc.content = description || `${title} - ${config.site?.description || ''}`
|
||||
metaDesc.content = description || `${title} - ${cfg.site?.description || ''}`
|
||||
|
||||
// Update Open Graph tags
|
||||
let ogTitle = document.querySelector('meta[property="og:title"]') as HTMLMetaElement
|
||||
@@ -111,8 +108,8 @@ export default function App() {
|
||||
ogUrl.content = window.location.origin + path
|
||||
|
||||
// Update Twitter meta tags if configured
|
||||
if (config.seo?.twitter) {
|
||||
Object.entries(config.seo.twitter).forEach(([key, value]) => {
|
||||
if (cfg.seo?.twitter) {
|
||||
Object.entries(cfg.seo.twitter).forEach(([key, value]) => {
|
||||
let twitterMeta = document.querySelector(`meta[name="twitter:${key}"]`) as HTMLMetaElement
|
||||
if (!twitterMeta) {
|
||||
twitterMeta = document.createElement('meta')
|
||||
@@ -124,17 +121,23 @@ export default function App() {
|
||||
}
|
||||
|
||||
// Set default image if available
|
||||
if (config.seo?.defaultImage) {
|
||||
if (cfg.seo?.defaultImage) {
|
||||
let ogImage = document.querySelector('meta[property="og:image"]') as HTMLMetaElement
|
||||
if (!ogImage) {
|
||||
ogImage = document.createElement('meta')
|
||||
ogImage.setAttribute('property', 'og:image')
|
||||
document.head.appendChild(ogImage)
|
||||
}
|
||||
ogImage.content = config.seo.defaultImage
|
||||
ogImage.content = cfg.seo.defaultImage
|
||||
}
|
||||
}
|
||||
|
||||
const selectNavItem = (item: NavItem) => {
|
||||
setSelectedItem(item)
|
||||
setActiveItemPath(item.path || item.title)
|
||||
updateSEO(item.title, item.path || '/')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
async function loadManifests() {
|
||||
try {
|
||||
@@ -163,12 +166,21 @@ export default function App() {
|
||||
)
|
||||
|
||||
// Handle initial URL path for SPA routing
|
||||
const currentPath = window.location.pathname
|
||||
if (currentPath && currentPath !== '/') {
|
||||
const currentPath = window.location.pathname.replace(/\/$/, '') || '/'
|
||||
setCurrentPath(currentPath)
|
||||
if (currentPath !== '/') {
|
||||
const found = findNavItemByPath(navData, currentPath)
|
||||
if (found) {
|
||||
setSelectedItem(found)
|
||||
setActiveItemPath(found.path || found.title)
|
||||
updateSEO(found.title, found.path || currentPath, undefined, configData)
|
||||
}
|
||||
} else {
|
||||
const docsHome = findNavItemByPath(navData, '/docs/index')
|
||||
if (docsHome) {
|
||||
setSelectedItem(docsHome)
|
||||
setActiveItemPath(docsHome.path || docsHome.title)
|
||||
updateSEO(docsHome.title, docsHome.path || '/docs/index', undefined, configData)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -184,19 +196,21 @@ export default function App() {
|
||||
// Handle browser back/forward navigation in a separate effect
|
||||
useEffect(() => {
|
||||
const handlePopState = () => {
|
||||
const path = window.location.pathname
|
||||
const path = window.location.pathname.replace(/\/$/, '') || '/'
|
||||
setCurrentPath(path)
|
||||
const found = findNavItemByPath(nav, path)
|
||||
|
||||
if (found) {
|
||||
setSelectedItem(found)
|
||||
setActiveItemPath(found.path || found.title)
|
||||
// Use updateSEO function for consistency
|
||||
updateSEO(found.title, path)
|
||||
selectNavItem(found)
|
||||
} else if (path === '/') {
|
||||
setSelectedItem(null)
|
||||
setActiveItemPath('')
|
||||
// Reset title to site title
|
||||
document.title = config.site?.title || 'Documentation'
|
||||
const docsHome = findNavItemByPath(nav, '/docs/index')
|
||||
if (docsHome) {
|
||||
selectNavItem(docsHome)
|
||||
} else {
|
||||
setSelectedItem(null)
|
||||
setActiveItemPath('')
|
||||
document.title = config.site?.title || 'Documentation'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,13 +221,42 @@ export default function App() {
|
||||
const handleItemClick = (item: NavItem) => {
|
||||
setSelectedItem(item)
|
||||
setActiveItemPath(item.path || item.title)
|
||||
// Use History API for SEO-friendly URLs
|
||||
const path = item.path || `/${item.title.toLowerCase().replace(/\s+/g, '-')}`
|
||||
setCurrentPath(path.replace(/\/$/, '') || '/')
|
||||
window.history.pushState({ path }, item.title, path)
|
||||
// Update SEO meta tags
|
||||
updateSEO(item.title, path)
|
||||
}
|
||||
|
||||
const templateMatch = resolveTemplate(
|
||||
currentPath.replace(/^\//, ''),
|
||||
routes,
|
||||
config.templates ?? {},
|
||||
)
|
||||
const activeTemplateKey = templateMatch?.template ?? 'docs'
|
||||
const activeTemplate = config.templates?.[activeTemplateKey] ?? config.templates?.docs
|
||||
const layout = templateMatch?.layout ?? activeTemplate?.layout ?? 'ThreeColumnLayout'
|
||||
const zones = activeTemplate?.zones
|
||||
const showToc = shouldShowToc(templateMatch)
|
||||
const showGlobalFooter = !(layout === 'FlexibleLayout' && (zones?.footer?.length ?? 0) > 0)
|
||||
|
||||
const docsChrome = (
|
||||
<>
|
||||
<MobileNavSheet
|
||||
open={mobileNavOpen}
|
||||
onOpenChange={setMobileNavOpen}
|
||||
nav={nav}
|
||||
activeItem={activeItemPath}
|
||||
onItemClick={handleItemClick}
|
||||
title={config.site?.title || 'Navigation'}
|
||||
/>
|
||||
<Header
|
||||
config={config}
|
||||
templateKey={activeTemplateKey}
|
||||
onMenuClick={() => setMobileNavOpen(true)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="dark flex items-center justify-center min-h-screen bg-background">
|
||||
@@ -231,15 +274,10 @@ export default function App() {
|
||||
)
|
||||
}
|
||||
|
||||
// Determine layout from templates config
|
||||
const layout = config.templates?.docs?.layout || 'ThreeColumnLayout'
|
||||
const zones = config.templates?.docs?.zones
|
||||
|
||||
// Render based on layout type
|
||||
const renderLayout = () => {
|
||||
switch (layout) {
|
||||
case 'TwoColumnLayout':
|
||||
// Sidebar + Content (no TOC)
|
||||
return (
|
||||
<div className="flex">
|
||||
<Sidebar nav={nav} activeItem={activeItemPath} onItemClick={handleItemClick} />
|
||||
@@ -247,7 +285,6 @@ export default function App() {
|
||||
</div>
|
||||
)
|
||||
case 'CenteredLayout':
|
||||
// Centered content, no sidebar
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<div className="w-full max-w-4xl px-6">
|
||||
@@ -257,7 +294,6 @@ export default function App() {
|
||||
</div>
|
||||
)
|
||||
case 'FullWidthLayout':
|
||||
// Full width content
|
||||
return (
|
||||
<div className="px-6">
|
||||
{zones?.hero && zones.hero.length > 0 && <ZoneWidgets widgets={zones.hero} />}
|
||||
@@ -265,12 +301,10 @@ export default function App() {
|
||||
</div>
|
||||
)
|
||||
case 'FlexibleLayout': {
|
||||
// WordPress-style zones layout with navigation invariant
|
||||
// When docs nav exists but no leftSidebar zone configured, render default nav
|
||||
const hasExplicitLeftSidebar = (zones?.leftSidebar?.length ?? 0) > 0
|
||||
const hasNavigation = (nav?.items?.length ?? 0) > 0
|
||||
const shouldRenderNavigation = hasExplicitLeftSidebar || hasNavigation
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{zones?.hero && zones.hero.length > 0 && <ZoneWidgets widgets={zones.hero} />}
|
||||
@@ -287,7 +321,7 @@ export default function App() {
|
||||
<div className="flex-1">
|
||||
<Content config={config} routes={routes} selectedItem={selectedItem} />
|
||||
</div>
|
||||
<Toc selectedItem={selectedItem} zones={zones} />
|
||||
<Toc selectedItem={selectedItem} zones={zones} showToc={showToc} />
|
||||
</div>
|
||||
{zones?.bottomNav && zones.bottomNav.length > 0 && (
|
||||
<div className="border-t p-4 bg-muted/30">
|
||||
@@ -304,12 +338,11 @@ export default function App() {
|
||||
}
|
||||
case 'ThreeColumnLayout':
|
||||
default:
|
||||
// Classic: Sidebar + Content + TOC with zones
|
||||
return (
|
||||
<div className="flex">
|
||||
<Sidebar nav={nav} activeItem={activeItemPath} onItemClick={handleItemClick} />
|
||||
<Content config={config} routes={routes} selectedItem={selectedItem} />
|
||||
<Toc selectedItem={selectedItem} zones={zones} />
|
||||
{showToc && <Toc selectedItem={selectedItem} zones={zones} showToc={showToc} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -347,14 +380,13 @@ export default function App() {
|
||||
// Dashboard is handled entirely by plugin dashboard.js — React yields
|
||||
return null
|
||||
case 'custom':
|
||||
// For custom, show docs with copilot widget if chat enabled
|
||||
if (config.chat?.enabled) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<SkipLink enabled={config.a11y?.skipToContent} />
|
||||
<Header config={config} />
|
||||
{docsChrome}
|
||||
{renderLayout()}
|
||||
<Footer config={config} />
|
||||
{showGlobalFooter && <Footer config={config} templateKey={activeTemplateKey} />}
|
||||
<CopilotWidget config={config.chat} layout={config.layout} />
|
||||
</div>
|
||||
)
|
||||
@@ -362,23 +394,22 @@ export default function App() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<SkipLink enabled={config.a11y?.skipToContent} />
|
||||
<Header config={config} />
|
||||
{docsChrome}
|
||||
{renderLayout()}
|
||||
<Footer config={config} />
|
||||
{showGlobalFooter && <Footer config={config} templateKey={activeTemplateKey} />}
|
||||
</div>
|
||||
)
|
||||
case 'docs':
|
||||
default:
|
||||
// Docs mode - optionally with copilot widget
|
||||
if (config.chat?.enabled) {
|
||||
const layoutMode = config.layout?.mode
|
||||
if (layoutMode && ['bottom-right', 'bottom-left', 'top-right', 'top-left'].includes(layoutMode)) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<SkipLink enabled={config.a11y?.skipToContent} />
|
||||
<Header config={config} />
|
||||
{docsChrome}
|
||||
{renderLayout()}
|
||||
<Footer config={config} />
|
||||
{showGlobalFooter && <Footer config={config} templateKey={activeTemplateKey} />}
|
||||
<CopilotWidget config={config.chat} layout={config.layout} />
|
||||
</div>
|
||||
)
|
||||
@@ -387,9 +418,9 @@ export default function App() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<SkipLink enabled={config.a11y?.skipToContent} />
|
||||
<Header config={config} />
|
||||
{docsChrome}
|
||||
{renderLayout()}
|
||||
<Footer config={config} />
|
||||
{showGlobalFooter && <Footer config={config} templateKey={activeTemplateKey} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
// Content component — markdown rendering and landing page
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import Markdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { SHADCN_THEMES } from './themes'
|
||||
import { enhanceMkdocsDom } from './markdown/mkdocsEnhance'
|
||||
import { preprocessMkdocsMarkdown } from './markdown/mkdocsPreprocess'
|
||||
import { slugify } from './markdown/slug'
|
||||
import { MobileToc } from './Toc'
|
||||
import type { UIConfig, NavItem, RouteManifest } from './types'
|
||||
|
||||
interface ContentProps {
|
||||
@@ -15,6 +19,7 @@ interface ContentProps {
|
||||
export function Content({ config, routes, selectedItem }: ContentProps) {
|
||||
const [markdown, setMarkdown] = useState<string>('')
|
||||
const [loadingContent, setLoadingContent] = useState(false)
|
||||
const articleRef = useRef<HTMLElement>(null)
|
||||
const theme = config.site?.theme
|
||||
|
||||
// Load markdown content when selected item changes
|
||||
@@ -27,12 +32,11 @@ export function Content({ config, routes, selectedItem }: ContentProps) {
|
||||
const loadContent = async () => {
|
||||
setLoadingContent(true)
|
||||
try {
|
||||
// Convert path like /docs/getting-started/installation to docs/getting-started/installation.md
|
||||
const docPath = (selectedItem.path ?? '').replace(/^\//, '') + '.md'
|
||||
const response = await fetch(`/${docPath}`)
|
||||
if (response.ok) {
|
||||
const content = await response.text()
|
||||
setMarkdown(content)
|
||||
setMarkdown(preprocessMkdocsMarkdown(content))
|
||||
} else {
|
||||
setMarkdown(`*Content for **${selectedItem.title}** not found.*`)
|
||||
}
|
||||
@@ -46,12 +50,19 @@ export function Content({ config, routes, selectedItem }: ContentProps) {
|
||||
loadContent()
|
||||
}, [selectedItem])
|
||||
|
||||
// Custom components for react-markdown with Tailwind styling
|
||||
useEffect(() => {
|
||||
if (!markdown || !articleRef.current) return
|
||||
enhanceMkdocsDom(articleRef.current)
|
||||
window.dispatchEvent(new CustomEvent('aiui:content-loaded', { detail: { root: articleRef.current } }))
|
||||
}, [markdown])
|
||||
|
||||
const headingId = (children?: React.ReactNode) => slugify(String(children ?? ''))
|
||||
|
||||
const markdownComponents = {
|
||||
h1: ({ children }: { children?: React.ReactNode }) => <h1 className="text-3xl font-bold mt-8 mb-4">{children}</h1>,
|
||||
h2: ({ children }: { children?: React.ReactNode }) => <h2 className="text-2xl font-semibold mt-8 mb-4 text-primary">{children}</h2>,
|
||||
h3: ({ children }: { children?: React.ReactNode }) => <h3 className="text-xl font-semibold mt-6 mb-3">{children}</h3>,
|
||||
h4: ({ children }: { children?: React.ReactNode }) => <h4 className="text-lg font-semibold mt-4 mb-2">{children}</h4>,
|
||||
h1: ({ children }: { children?: React.ReactNode }) => <h1 id={headingId(children)} className="text-3xl font-bold mt-8 mb-4 scroll-mt-20">{children}</h1>,
|
||||
h2: ({ children }: { children?: React.ReactNode }) => <h2 id={headingId(children)} className="text-2xl font-semibold mt-8 mb-4 text-primary scroll-mt-20">{children}</h2>,
|
||||
h3: ({ children }: { children?: React.ReactNode }) => <h3 id={headingId(children)} className="text-xl font-semibold mt-6 mb-3 scroll-mt-20">{children}</h3>,
|
||||
h4: ({ children }: { children?: React.ReactNode }) => <h4 id={headingId(children)} className="text-lg font-semibold mt-4 mb-2 scroll-mt-20">{children}</h4>,
|
||||
p: ({ children }: { children?: React.ReactNode }) => <p className="my-3 text-muted-foreground leading-relaxed">{children}</p>,
|
||||
a: ({ href, children }: { href?: string; children?: React.ReactNode }) => <a href={href} className="text-primary hover:underline">{children}</a>,
|
||||
ul: ({ children }: { children?: React.ReactNode }) => <ul className="list-disc pl-6 my-4 space-y-1">{children}</ul>,
|
||||
@@ -59,16 +70,14 @@ export function Content({ config, routes, selectedItem }: ContentProps) {
|
||||
li: ({ children }: { children?: React.ReactNode }) => <li className="text-muted-foreground">{children}</li>,
|
||||
blockquote: ({ children }: { children?: React.ReactNode }) => <blockquote className="border-l-4 border-primary pl-4 my-4 italic text-muted-foreground">{children}</blockquote>,
|
||||
code: ({ className, children }: { className?: string; children?: React.ReactNode }) => {
|
||||
// Check if this is inside a pre block (fenced code block)
|
||||
const isInline = !className && String(children).indexOf('\n') === -1
|
||||
if (isInline) {
|
||||
return <code className="bg-primary/10 text-primary px-1.5 py-0.5 rounded text-sm font-mono">{children}</code>
|
||||
}
|
||||
// Block code - rendered by pre wrapper
|
||||
return <code className="block font-mono">{children}</code>
|
||||
return <code className={`block font-mono text-foreground ${className ?? ''}`}>{children}</code>
|
||||
},
|
||||
pre: ({ children }: { children?: React.ReactNode }) => (
|
||||
<pre className="bg-muted p-4 rounded-lg text-sm overflow-auto my-4 font-mono whitespace-pre">{children}</pre>
|
||||
<pre className="bg-muted text-foreground border border-border p-4 rounded-lg text-sm overflow-auto my-4 font-mono whitespace-pre">{children}</pre>
|
||||
),
|
||||
table: ({ children }: { children?: React.ReactNode }) => <div className="overflow-auto my-4"><table className="w-full border-collapse text-sm">{children}</table></div>,
|
||||
thead: ({ children }: { children?: React.ReactNode }) => <thead className="bg-muted/50">{children}</thead>,
|
||||
@@ -83,10 +92,14 @@ export function Content({ config, routes, selectedItem }: ContentProps) {
|
||||
if (selectedItem) {
|
||||
return (
|
||||
<main id="main-content" className="flex-1 p-8 max-w-3xl">
|
||||
<MobileToc selectedItem={selectedItem} />
|
||||
{loadingContent ? (
|
||||
<div className="text-muted-foreground">Loading content...</div>
|
||||
) : markdown ? (
|
||||
<article className="prose max-w-none">
|
||||
<article
|
||||
ref={articleRef}
|
||||
className="prose prose-neutral dark:prose-invert max-w-none prose-pre:bg-muted prose-pre:text-foreground prose-code:text-foreground"
|
||||
>
|
||||
<Markdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||
{markdown}
|
||||
</Markdown>
|
||||
@@ -118,7 +131,7 @@ export function Content({ config, routes, selectedItem }: ContentProps) {
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
<strong>Current theme from YAML:</strong>
|
||||
</p>
|
||||
<pre className="text-primary text-sm">
|
||||
<pre className="bg-muted text-foreground border border-border p-3 rounded-lg text-sm overflow-auto">
|
||||
{`site:
|
||||
theme:
|
||||
preset: "${theme?.preset || 'zinc'}"
|
||||
@@ -150,7 +163,7 @@ export function Content({ config, routes, selectedItem }: ContentProps) {
|
||||
<p className="text-muted-foreground mb-4">
|
||||
<span className="font-medium text-primary">{routes.routes?.length || 0}</span> routes configured.
|
||||
</p>
|
||||
<pre className="bg-muted p-4 rounded-lg text-sm overflow-auto">
|
||||
<pre className="bg-muted text-foreground border border-border p-4 rounded-lg text-sm overflow-auto">
|
||||
{JSON.stringify(routes.routes?.slice(0, 3), null, 2)}
|
||||
</pre>
|
||||
</main>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// Footer component
|
||||
import type { UIConfig } from './types'
|
||||
|
||||
export function Footer({ config }: { config: UIConfig }) {
|
||||
// Resolve footer component via template slot ref (e.g., footer_main)
|
||||
const footerSlot = config.templates?.docs?.slots?.footer
|
||||
export function Footer({ config, templateKey = 'docs' }: { config: UIConfig; templateKey?: string }) {
|
||||
const footerSlot = config.templates?.[templateKey]?.slots?.footer
|
||||
const footerRef = footerSlot?.ref
|
||||
const footerComponent = footerRef
|
||||
? config.components?.[footerRef]
|
||||
|
||||
+32
-11
@@ -1,10 +1,19 @@
|
||||
// Header component
|
||||
import { Menu } from 'lucide-react'
|
||||
import type { UIConfig } from './types'
|
||||
import { LocaleSwitcher } from './i18n'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
export function Header({ config }: { config: UIConfig }) {
|
||||
// Resolve header component via template slot ref (e.g., header_main)
|
||||
const headerSlot = config.templates?.docs?.slots?.header
|
||||
export function Header({
|
||||
config,
|
||||
templateKey = 'docs',
|
||||
onMenuClick,
|
||||
}: {
|
||||
config: UIConfig
|
||||
templateKey?: string
|
||||
onMenuClick?: () => void
|
||||
}) {
|
||||
const headerSlot = config.templates?.[templateKey]?.slots?.header
|
||||
const headerRef = headerSlot?.ref
|
||||
const headerComponent = headerRef
|
||||
? config.components?.[headerRef]
|
||||
@@ -16,7 +25,6 @@ export function Header({ config }: { config: UIConfig }) {
|
||||
cta?: { label: string; href: string }
|
||||
} | undefined
|
||||
|
||||
// Get logo configuration - support both header.logoImage and config.logo
|
||||
const logoConfig = config.logo
|
||||
const isDarkMode = document.documentElement.classList.contains('dark')
|
||||
const logoSrc = header?.logoImage || (logoConfig ? (isDarkMode ? (logoConfig.dark || logoConfig.light) : (logoConfig.light || logoConfig.dark)) : null)
|
||||
@@ -24,16 +32,29 @@ export function Header({ config }: { config: UIConfig }) {
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 border-b border-border/50 bg-background/80 backdrop-blur-xl supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="max-w-screen-2xl mx-auto flex h-16 items-center px-6 gap-8">
|
||||
<a href={logoHref} className="flex items-center gap-3 group cursor-pointer" onClick={(e) => {
|
||||
<div className="max-w-screen-2xl mx-auto flex h-16 items-center px-6 gap-4">
|
||||
{onMenuClick && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="md:hidden shrink-0"
|
||||
onClick={onMenuClick}
|
||||
aria-label="Open navigation menu"
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<a href={logoHref} className="flex items-center gap-3 group cursor-pointer min-w-0" onClick={(e) => {
|
||||
e.preventDefault()
|
||||
window.history.pushState({}, '', logoHref)
|
||||
window.location.reload()
|
||||
}}>
|
||||
{logoSrc ? (
|
||||
<img
|
||||
src={logoSrc}
|
||||
alt="Logo"
|
||||
<img
|
||||
src={logoSrc}
|
||||
alt="Logo"
|
||||
className="w-8 h-8 rounded-lg shadow-lg shadow-primary/20 group-hover:shadow-primary/40 transition-shadow object-contain"
|
||||
/>
|
||||
) : (
|
||||
@@ -41,7 +62,7 @@ export function Header({ config }: { config: UIConfig }) {
|
||||
<span className="text-primary-foreground text-xs font-bold">AI</span>
|
||||
</div>
|
||||
)}
|
||||
<span className="font-semibold text-lg tracking-tight">{header?.logoText || config.site?.title || 'PraisonAIUI'}</span>
|
||||
<span className="font-semibold text-lg tracking-tight truncate">{header?.logoText || config.site?.title || 'PraisonAIUI'}</span>
|
||||
</a>
|
||||
|
||||
<nav className="hidden md:flex items-center gap-1 flex-1">
|
||||
@@ -56,7 +77,7 @@ export function Header({ config }: { config: UIConfig }) {
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
<LocaleSwitcher />
|
||||
{header?.cta && (
|
||||
<a
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
// Sidebar component — navigation tree
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import type { NavItem } from './types'
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from '@/components/ui/sheet'
|
||||
import type { DocsNav, NavItem } from './types'
|
||||
|
||||
interface SidebarProps {
|
||||
nav: { items?: NavItem[] }
|
||||
interface NavTreeProps {
|
||||
nav: DocsNav
|
||||
activeItem: string
|
||||
onItemClick: (item: NavItem) => void
|
||||
}
|
||||
|
||||
export function Sidebar({ nav, activeItem, onItemClick }: SidebarProps) {
|
||||
export function NavTree({ nav, activeItem, onItemClick }: NavTreeProps) {
|
||||
const renderItem = (item: NavItem, depth = 0) => {
|
||||
const isActive = (item.path || item.title) === activeItem
|
||||
const hasChildren = item.children && item.children.length > 0
|
||||
@@ -30,10 +36,11 @@ export function Sidebar({ nav, activeItem, onItemClick }: SidebarProps) {
|
||||
return (
|
||||
<button
|
||||
key={item.title + (item.path || '')}
|
||||
type="button"
|
||||
onClick={() => onItemClick(item)}
|
||||
className={`w-full text-left px-3 py-1.5 text-sm rounded-md transition-all duration-150 ${isActive
|
||||
? 'bg-primary/10 text-primary font-medium border-l-2 border-primary'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
||||
? 'bg-primary/10 text-primary font-medium border-l-2 border-primary'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
||||
}`}
|
||||
style={{ paddingLeft: `${12 + depth * 12}px` }}
|
||||
>
|
||||
@@ -42,14 +49,52 @@ export function Sidebar({ nav, activeItem, onItemClick }: SidebarProps) {
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="space-y-0.5">
|
||||
{nav.items?.map((group) => renderItem(group))}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
interface SidebarProps extends NavTreeProps {
|
||||
nav: DocsNav
|
||||
}
|
||||
|
||||
export function Sidebar({ nav, activeItem, onItemClick }: SidebarProps) {
|
||||
return (
|
||||
<aside className="w-64 min-w-[16rem] border-r border-border/50 hidden md:block">
|
||||
<ScrollArea className="h-[calc(100vh-4rem)] py-4 px-2">
|
||||
<nav className="space-y-0.5">
|
||||
{nav.items?.map((group) => renderItem(group))}
|
||||
</nav>
|
||||
<NavTree nav={nav} activeItem={activeItem} onItemClick={onItemClick} />
|
||||
<Separator className="my-4" />
|
||||
</ScrollArea>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
interface MobileNavSheetProps extends NavTreeProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
title?: string
|
||||
}
|
||||
|
||||
export function MobileNavSheet({ open, onOpenChange, nav, activeItem, onItemClick, title = 'Navigation' }: MobileNavSheetProps) {
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent side="left" className="w-72 p-0">
|
||||
<SheetHeader className="border-b px-4 py-3">
|
||||
<SheetTitle>{title}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<ScrollArea className="h-[calc(100vh-4rem)] px-2 py-4">
|
||||
<NavTree
|
||||
nav={nav}
|
||||
activeItem={activeItem}
|
||||
onItemClick={(item) => {
|
||||
onItemClick(item)
|
||||
onOpenChange(false)
|
||||
}}
|
||||
/>
|
||||
</ScrollArea>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
+101
-37
@@ -1,51 +1,115 @@
|
||||
// Table of Contents & right sidebar component
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import type { NavItem, ZonesConfig } from './types'
|
||||
import { ZoneWidgets } from './Widgets'
|
||||
|
||||
export function Toc({ selectedItem, zones }: { selectedItem: NavItem | null; zones?: ZonesConfig }) {
|
||||
interface TocHeading {
|
||||
id: string
|
||||
text: string
|
||||
level: number
|
||||
}
|
||||
|
||||
function collectHeadings(): TocHeading[] {
|
||||
const article = document.querySelector('#main-content article') ?? document.querySelector('article.prose')
|
||||
if (!article) return []
|
||||
|
||||
return Array.from(article.querySelectorAll('h2[id], h3[id], h4[id]')).map((el) => ({
|
||||
id: el.id,
|
||||
text: el.textContent?.trim() ?? '',
|
||||
level: Number(el.tagName.slice(1)),
|
||||
}))
|
||||
}
|
||||
|
||||
function usePageHeadings(selectedItem: NavItem | null) {
|
||||
const [headings, setHeadings] = useState<TocHeading[]>([])
|
||||
|
||||
const refreshHeadings = useCallback(() => {
|
||||
setHeadings(collectHeadings())
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
refreshHeadings()
|
||||
const timer = window.setTimeout(refreshHeadings, 100)
|
||||
return () => window.clearTimeout(timer)
|
||||
}, [selectedItem, refreshHeadings])
|
||||
|
||||
useEffect(() => {
|
||||
const onContentLoaded = () => refreshHeadings()
|
||||
window.addEventListener('aiui:content-loaded', onContentLoaded)
|
||||
return () => window.removeEventListener('aiui:content-loaded', onContentLoaded)
|
||||
}, [refreshHeadings])
|
||||
|
||||
return headings
|
||||
}
|
||||
|
||||
function TocLinks({ headings }: { headings: TocHeading[] }) {
|
||||
if (headings.length === 0) {
|
||||
return <p className="text-sm text-muted-foreground">No sections on this page.</p>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{headings.map((heading) => (
|
||||
<a
|
||||
key={heading.id}
|
||||
href={`#${heading.id}`}
|
||||
className={`flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors ${heading.level === 3 ? 'pl-3 text-sm' : heading.level === 4 ? 'pl-6 text-sm' : 'font-medium'}`}
|
||||
>
|
||||
<span className="w-1 h-1 rounded-full bg-muted-foreground/30 shrink-0" />
|
||||
{heading.text}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function MobileToc({ selectedItem }: { selectedItem: NavItem | null }) {
|
||||
const headings = usePageHeadings(selectedItem)
|
||||
if (!selectedItem || headings.length === 0) return null
|
||||
|
||||
return (
|
||||
<details className="lg:hidden border border-border rounded-lg mb-6 px-4 py-3 bg-muted/20">
|
||||
<summary className="text-sm font-medium cursor-pointer">On this page</summary>
|
||||
<nav className="mt-3">
|
||||
<TocLinks headings={headings} />
|
||||
</nav>
|
||||
</details>
|
||||
)
|
||||
}
|
||||
|
||||
export function Toc({
|
||||
selectedItem,
|
||||
zones,
|
||||
showToc = true,
|
||||
}: {
|
||||
selectedItem: NavItem | null
|
||||
zones?: ZonesConfig
|
||||
showToc?: boolean
|
||||
}) {
|
||||
const headings = usePageHeadings(selectedItem)
|
||||
const rightSidebarWidgets = zones?.rightSidebar || []
|
||||
|
||||
if (!showToc && rightSidebarWidgets.filter((w) => w.type !== 'Toc').length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="w-64 hidden lg:block border-l border-border/50">
|
||||
<div className="sticky top-20 px-4 py-6 space-y-4">
|
||||
{/* Table of Contents */}
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-muted-foreground/70 uppercase tracking-widest mb-4">
|
||||
On this page
|
||||
</h4>
|
||||
<nav className="space-y-2 text-sm">
|
||||
{selectedItem ? (
|
||||
<div className="space-y-2">
|
||||
<a href="#" className="flex items-center gap-2 text-primary font-medium">
|
||||
<span className="w-1 h-1 rounded-full bg-primary" />
|
||||
{selectedItem.title}
|
||||
</a>
|
||||
<a href="#overview" className="flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors pl-3">
|
||||
Overview
|
||||
</a>
|
||||
<a href="#usage" className="flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors pl-3">
|
||||
Usage
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<a href="#theme" className="flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors">
|
||||
<span className="w-1 h-1 rounded-full bg-muted-foreground/30" />
|
||||
Theme Configuration
|
||||
</a>
|
||||
<a href="#presets" className="flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors">
|
||||
<span className="w-1 h-1 rounded-full bg-muted-foreground/30" />
|
||||
Available Presets
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
{showToc && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-muted-foreground/70 uppercase tracking-widest mb-4">
|
||||
On this page
|
||||
</h4>
|
||||
<nav className="text-sm">
|
||||
<TocLinks headings={headings} />
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Zone Widgets (excluding Toc) */}
|
||||
{rightSidebarWidgets.filter(w => w.type !== 'Toc').length > 0 && (
|
||||
{rightSidebarWidgets.filter((w) => w.type !== 'Toc').length > 0 && (
|
||||
<div className="pt-4 border-t border-border/50">
|
||||
<ZoneWidgets widgets={rightSidebarWidgets.filter(w => w.type !== 'Toc')} />
|
||||
<ZoneWidgets widgets={rightSidebarWidgets.filter((w) => w.type !== 'Toc')} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -56,3 +56,27 @@
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* Readable code blocks in light and dark prose */
|
||||
.prose pre {
|
||||
@apply bg-muted text-foreground border border-border;
|
||||
}
|
||||
|
||||
.prose pre code {
|
||||
@apply text-foreground bg-transparent;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.prose :not(pre) > code {
|
||||
@apply bg-primary/10 text-primary;
|
||||
}
|
||||
|
||||
.prose .aiui-feature-grid > li {
|
||||
@apply list-none;
|
||||
}
|
||||
|
||||
.prose .aiui-tabs pre {
|
||||
@apply my-2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import { isMkdocsTabHeading, mkdocsTabTitle } from './mkdocsPreprocess'
|
||||
|
||||
/** Post-render DOM fixes for MkDocs-style content (tabs, feature cards). */
|
||||
export function enhanceMkdocsDom(root: HTMLElement): void {
|
||||
const articles = new Set<HTMLElement>()
|
||||
if (root.matches('article.prose, main .prose, .prose, main.flex-1, #main-content')) {
|
||||
articles.add(root)
|
||||
}
|
||||
root.querySelectorAll<HTMLElement>('article.prose, main .prose, .prose').forEach((el) => articles.add(el))
|
||||
|
||||
for (const article of articles) {
|
||||
removeRawDivParagraphs(article)
|
||||
enhanceFeatureGrid(article)
|
||||
enhanceTabs(article)
|
||||
}
|
||||
}
|
||||
|
||||
function removeRawDivParagraphs(article: HTMLElement): void {
|
||||
for (const p of article.querySelectorAll('p')) {
|
||||
const text = p.textContent?.trim() ?? ''
|
||||
if (/^<\/?div[\s>]/.test(text) || text === '</div>') {
|
||||
p.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function enhanceFeatureGrid(article: HTMLElement): void {
|
||||
for (const ul of article.querySelectorAll('ul')) {
|
||||
if (ul.classList.contains('aiui-feature-grid')) continue
|
||||
|
||||
const firstLi = ul.querySelector('li')
|
||||
const looksLikeCards = Boolean(
|
||||
firstLi?.textContent?.includes('YAML-Driven')
|
||||
|| firstLi?.textContent?.includes('Component Slots')
|
||||
|| firstLi?.querySelector('strong'),
|
||||
)
|
||||
if (!looksLikeCards) continue
|
||||
|
||||
ul.classList.add(
|
||||
'aiui-feature-grid',
|
||||
'grid',
|
||||
'grid-cols-1',
|
||||
'md:grid-cols-2',
|
||||
'gap-4',
|
||||
'my-6',
|
||||
'list-none',
|
||||
'pl-0',
|
||||
)
|
||||
|
||||
for (const li of ul.querySelectorAll('li')) {
|
||||
li.classList.add('border', 'border-border', 'rounded-lg', 'p-4', 'bg-card')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface TabSection {
|
||||
heading: HTMLHeadingElement
|
||||
title: string
|
||||
nodes: Node[]
|
||||
}
|
||||
|
||||
function enhanceTabs(article: HTMLElement): void {
|
||||
if (article.querySelector('[data-aiui-tabs]')) return
|
||||
|
||||
const tabHeadings = Array.from(article.querySelectorAll<HTMLHeadingElement>('h3')).filter((h3) =>
|
||||
isMkdocsTabHeading(h3.textContent?.trim() ?? ''),
|
||||
)
|
||||
if (tabHeadings.length < 2) return
|
||||
|
||||
const sections: TabSection[] = tabHeadings.map((heading) => {
|
||||
const title = mkdocsTabTitle(heading.textContent?.trim() ?? '')
|
||||
const nodes: Node[] = []
|
||||
let sibling = heading.nextSibling
|
||||
|
||||
while (sibling) {
|
||||
if (sibling.nodeType === Node.ELEMENT_NODE) {
|
||||
const el = sibling as HTMLElement
|
||||
if (el.tagName === 'H3' && isMkdocsTabHeading(el.textContent?.trim() ?? '')) break
|
||||
if (['H1', 'H2'].includes(el.tagName)) break
|
||||
}
|
||||
const next = sibling.nextSibling
|
||||
nodes.push(sibling)
|
||||
sibling = next
|
||||
}
|
||||
|
||||
return { heading, title, nodes }
|
||||
})
|
||||
|
||||
const wrapper = document.createElement('div')
|
||||
wrapper.className = 'aiui-tabs my-6 border border-border rounded-lg overflow-hidden'
|
||||
wrapper.dataset.aiuiTabs = 'true'
|
||||
|
||||
const buttons = document.createElement('div')
|
||||
buttons.className = 'aiui-tab-buttons flex flex-wrap gap-0 border-b border-border bg-muted/30'
|
||||
wrapper.appendChild(buttons)
|
||||
|
||||
const panels = document.createElement('div')
|
||||
panels.className = 'aiui-tab-panels'
|
||||
wrapper.appendChild(panels)
|
||||
|
||||
sections.forEach((section, index) => {
|
||||
const btn = document.createElement('button')
|
||||
btn.type = 'button'
|
||||
btn.className = [
|
||||
'aiui-tab-button px-4 py-2 text-sm font-medium transition-colors',
|
||||
index === 0
|
||||
? 'bg-background text-foreground border-b-2 border-primary'
|
||||
: 'text-muted-foreground hover:text-foreground',
|
||||
].join(' ')
|
||||
btn.textContent = section.title
|
||||
btn.dataset.tabIndex = String(index)
|
||||
buttons.appendChild(btn)
|
||||
|
||||
const panel = document.createElement('div')
|
||||
panel.className = index === 0 ? 'aiui-tab-panel p-4 block' : 'aiui-tab-panel p-4 hidden'
|
||||
panel.dataset.tabPanel = String(index)
|
||||
for (const node of section.nodes) {
|
||||
panel.appendChild(node)
|
||||
}
|
||||
panels.appendChild(panel)
|
||||
})
|
||||
|
||||
const insertPoint = sections[0].heading
|
||||
article.insertBefore(wrapper, insertPoint)
|
||||
|
||||
for (const section of sections) {
|
||||
section.heading.remove()
|
||||
}
|
||||
|
||||
buttons.addEventListener('click', (event) => {
|
||||
const target = (event.target as HTMLElement).closest<HTMLButtonElement>('.aiui-tab-button')
|
||||
if (!target) return
|
||||
const index = target.dataset.tabIndex ?? '0'
|
||||
|
||||
for (const btn of buttons.querySelectorAll<HTMLButtonElement>('.aiui-tab-button')) {
|
||||
const active = btn.dataset.tabIndex === index
|
||||
btn.classList.toggle('bg-background', active)
|
||||
btn.classList.toggle('text-foreground', active)
|
||||
btn.classList.toggle('border-b-2', active)
|
||||
btn.classList.toggle('border-primary', active)
|
||||
btn.classList.toggle('text-muted-foreground', !active)
|
||||
}
|
||||
|
||||
for (const panel of panels.querySelectorAll<HTMLElement>('.aiui-tab-panel')) {
|
||||
panel.classList.toggle('hidden', panel.dataset.tabPanel !== index)
|
||||
panel.classList.toggle('block', panel.dataset.tabPanel === index)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/** MkDocs Material syntax → standard markdown for react-markdown. */
|
||||
|
||||
export const MKDOCS_ICON_MAP: Record<string, string> = {
|
||||
':material-file-document:': '📄',
|
||||
':material-puzzle:': '🧩',
|
||||
':material-palette:': '🎨',
|
||||
':material-rocket-launch:': '🚀',
|
||||
':material-code-tags:': '💻',
|
||||
':material-cog:': '⚙️',
|
||||
':material-lightning-bolt:': '⚡',
|
||||
':material-shield:': '🛡️',
|
||||
':material-database:': '🗄️',
|
||||
':material-web:': '🌐',
|
||||
':material-book:': '📖',
|
||||
':material-star:': '⭐',
|
||||
':material-check:': '✅',
|
||||
':material-close:': '❌',
|
||||
':material-alert:': '⚠️',
|
||||
':material-information:': 'ℹ️',
|
||||
}
|
||||
|
||||
const TAB_HEADING_PREFIX = '\u200Btab:'
|
||||
|
||||
export function replaceMkdocsIcons(text: string): string {
|
||||
let result = text
|
||||
for (const [shortcode, emoji] of Object.entries(MKDOCS_ICON_MAP)) {
|
||||
result = result.replaceAll(shortcode, emoji)
|
||||
}
|
||||
return result
|
||||
.replace(/:material-[\w-]+:/g, '•')
|
||||
.replace(/:octicons-[\w-]+(?:-\d+)?:/g, '•')
|
||||
}
|
||||
|
||||
export function stripMkdocsGridWrappers(text: string): string {
|
||||
return text
|
||||
.replace(/<div[^>]*markdown[^>]*>\s*/gi, '')
|
||||
.replace(/<\/div>\s*/gi, '')
|
||||
}
|
||||
|
||||
/** Convert `=== "Title"` tab groups to headings the DOM enhancer recognises. */
|
||||
export function convertMkdocsTabs(md: string): string {
|
||||
const lines = md.split('\n')
|
||||
const out: string[] = []
|
||||
let i = 0
|
||||
|
||||
while (i < lines.length) {
|
||||
const tabMatch = lines[i].match(/^===\s+"([^"]+)"\s*$/)
|
||||
if (!tabMatch) {
|
||||
out.push(lines[i])
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
const title = tabMatch[1]
|
||||
i++
|
||||
while (i < lines.length && lines[i].trim() === '') i++
|
||||
|
||||
const block: string[] = []
|
||||
while (i < lines.length) {
|
||||
if (/^===\s+"[^"]+"\s*$/.test(lines[i])) break
|
||||
if (lines[i].trim() === '' && i + 1 < lines.length && /^===\s+"[^"]+"\s*$/.test(lines[i + 1])) break
|
||||
|
||||
if (lines[i].startsWith(' ') || lines[i].trimStart().startsWith('```')) {
|
||||
block.push(lines[i].replace(/^ /, ''))
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if (block.length > 0 && lines[i].trim() === '') {
|
||||
block.push('')
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
out.push(`### ${TAB_HEADING_PREFIX}${title}`)
|
||||
out.push('')
|
||||
out.push(...block)
|
||||
out.push('')
|
||||
}
|
||||
|
||||
return out.join('\n')
|
||||
}
|
||||
|
||||
export function preprocessMkdocsMarkdown(md: string): string {
|
||||
let result = stripMkdocsGridWrappers(md)
|
||||
result = replaceMkdocsIcons(result)
|
||||
result = convertMkdocsTabs(result)
|
||||
return result
|
||||
}
|
||||
|
||||
export function isMkdocsTabHeading(text: string): boolean {
|
||||
return text.startsWith(TAB_HEADING_PREFIX) || text.startsWith('tab:')
|
||||
}
|
||||
|
||||
export function mkdocsTabTitle(text: string): string {
|
||||
return text.replace(/^\u200Btab:/, '').replace(/^tab:/, '')
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export function slugify(text: string): string {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { RouteManifest, SlotConfig, TemplateConfig } from './types'
|
||||
|
||||
export interface TemplateMatch {
|
||||
template: string
|
||||
layout: string
|
||||
slots: Record<string, SlotConfig | null | undefined>
|
||||
slotOverrides?: Record<string, SlotConfig | null>
|
||||
}
|
||||
|
||||
export function matchPattern(path: string, pattern: string): boolean {
|
||||
const normalizedPath = path.replace(/^\/+|\/+$/g, '')
|
||||
const normalizedPattern = pattern.replace(/^\/+|\/+$/g, '')
|
||||
|
||||
const regexPattern = normalizedPattern
|
||||
.replace(/\*\*/g, '{{GLOB}}')
|
||||
.replace(/\*/g, '[^/]+')
|
||||
.replace(/\{\{GLOB\}\}/g, '.*')
|
||||
|
||||
return new RegExp(`^${regexPattern}$`).test(normalizedPath)
|
||||
}
|
||||
|
||||
export function resolveTemplate(
|
||||
path: string,
|
||||
manifest: RouteManifest,
|
||||
templates: Record<string, TemplateConfig>,
|
||||
): TemplateMatch | null {
|
||||
const routes = manifest.routes ?? []
|
||||
const sortedRoutes = [...routes].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))
|
||||
|
||||
for (const route of sortedRoutes) {
|
||||
if (!matchPattern(path, route.pattern)) continue
|
||||
|
||||
const template = templates[route.template]
|
||||
if (!template) {
|
||||
console.warn(`Template "${route.template}" not found`)
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
template: route.template,
|
||||
layout: template.layout ?? 'ThreeColumnLayout',
|
||||
slots: template.slots ?? {},
|
||||
slotOverrides: route.slotOverrides,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function mergedSlot(
|
||||
slotName: string,
|
||||
match: TemplateMatch | null,
|
||||
): SlotConfig | null | undefined {
|
||||
if (!match) return undefined
|
||||
if (match.slotOverrides && slotName in match.slotOverrides) {
|
||||
return match.slotOverrides[slotName]
|
||||
}
|
||||
return match.slots[slotName]
|
||||
}
|
||||
|
||||
export function shouldShowToc(match: TemplateMatch | null): boolean {
|
||||
const right = mergedSlot('right', match)
|
||||
return right !== null
|
||||
}
|
||||
@@ -96,7 +96,12 @@ export interface DocsNav {
|
||||
}
|
||||
|
||||
export interface RouteManifest {
|
||||
routes?: { pattern: string; template: string }[]
|
||||
routes?: {
|
||||
pattern: string
|
||||
template: string
|
||||
priority?: number
|
||||
slotOverrides?: Record<string, SlotConfig | null>
|
||||
}[]
|
||||
}
|
||||
|
||||
// Message element types (matching Python schema)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""Version information."""
|
||||
|
||||
__version__ = "0.3.129"
|
||||
__version__ = "0.3.142"
|
||||
@@ -1596,10 +1596,18 @@ def run(
|
||||
# Run gateway - check if user module defines its own main function
|
||||
import asyncio
|
||||
import inspect
|
||||
import os
|
||||
|
||||
if hasattr(user_module, 'main') and callable(getattr(user_module, 'main')):
|
||||
# Call user's main function directly (gateway pattern)
|
||||
main_func = getattr(user_module, 'main')
|
||||
# Propagate CLI --port/--host to gateway examples that read the
|
||||
# PORT/HOST environment variables. Explicit env set by the user
|
||||
# takes precedence over the CLI flag.
|
||||
if "PORT" not in os.environ:
|
||||
os.environ["PORT"] = str(actual_port)
|
||||
if "HOST" not in os.environ:
|
||||
os.environ["HOST"] = host
|
||||
if inspect.iscoroutinefunction(main_func):
|
||||
console.print("[yellow]ℹ️[/yellow] Delegating to user-defined async main()")
|
||||
asyncio.run(main_func())
|
||||
|
||||
@@ -476,6 +476,18 @@ class JSONFileDataStore(BaseDataStore):
|
||||
|
||||
return await asyncio.to_thread(_get)
|
||||
|
||||
async def update_session(self, session_id: str, **kwargs: Any) -> None:
|
||||
def _update() -> None:
|
||||
path = self._session_path(session_id)
|
||||
data = self._read_session(path)
|
||||
if data is None:
|
||||
return
|
||||
data.update(kwargs)
|
||||
data["updated_at"] = datetime.now(timezone.utc).isoformat()
|
||||
self._write_session(path, data)
|
||||
|
||||
await asyncio.to_thread(_update)
|
||||
|
||||
def _read_feedback(self) -> list[dict[str, Any]]:
|
||||
"""Read feedback from JSON file."""
|
||||
try:
|
||||
@@ -579,7 +591,7 @@ class SQLAlchemyDataStore(BaseDataStore):
|
||||
base = Path(os.environ.get("AIUI_DATA_DIR", str(Path.home() / ".praisonaiui")))
|
||||
base.mkdir(parents=True, exist_ok=True)
|
||||
db_path = base / "aiui.db"
|
||||
return f"sqlite+aiosqlite:///{db_path}"
|
||||
return f"sqlite+aiosqlite:///{db_path.as_posix()}"
|
||||
|
||||
async def _ensure_initialized(self) -> None:
|
||||
"""Lazy initialization of SQLAlchemy engine and tables."""
|
||||
|
||||
@@ -33,6 +33,8 @@ logger = logging.getLogger(__name__)
|
||||
# Sentinel key used to identify the AIUI metadata message inside SDK sessions
|
||||
_META_KEY = "_aiui_meta"
|
||||
_TITLE_KEY = "_aiui_title"
|
||||
# Bucket holding arbitrary session-level metadata (e.g. fork lineage)
|
||||
_CUSTOM_META_KEY = "_aiui_custom"
|
||||
|
||||
|
||||
class SDKFileDataStore(BaseDataStore):
|
||||
@@ -116,6 +118,10 @@ class SDKFileDataStore(BaseDataStore):
|
||||
result["platform"] = meta["platform"]
|
||||
if meta.get("icon"):
|
||||
result["icon"] = meta["icon"]
|
||||
# Surface any custom session-level metadata (e.g. fork lineage)
|
||||
custom = meta.get(_CUSTOM_META_KEY)
|
||||
if isinstance(custom, dict) and custom:
|
||||
result["metadata"] = custom
|
||||
return result
|
||||
|
||||
async def create_session(self, session_id: Optional[str] = None) -> dict[str, Any]:
|
||||
@@ -183,6 +189,8 @@ class SDKFileDataStore(BaseDataStore):
|
||||
await self._persist_meta(session_id, "platform", kwargs["platform"])
|
||||
if "icon" in kwargs:
|
||||
await self._persist_meta(session_id, "icon", kwargs["icon"])
|
||||
if "metadata" in kwargs and isinstance(kwargs["metadata"], dict):
|
||||
await self._persist_meta(session_id, _CUSTOM_META_KEY, kwargs["metadata"])
|
||||
|
||||
async def close(self) -> None:
|
||||
pass
|
||||
|
||||
@@ -671,6 +671,93 @@ async def _run_and_broadcast(
|
||||
pass
|
||||
|
||||
|
||||
DEFAULT_CONTEXT_LIMIT = 128000
|
||||
_TOKENS_PER_CHAR = 0.25
|
||||
_SYSTEM_BASELINE_TOKENS = 800
|
||||
_TOOLS_BASELINE_TOKENS = 400
|
||||
|
||||
|
||||
def _estimate_context_stats(
|
||||
messages: List[Dict[str, Any]],
|
||||
*,
|
||||
session_id: str,
|
||||
limit: int = DEFAULT_CONTEXT_LIMIT,
|
||||
model: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Compute a char-based context usage estimate for a session.
|
||||
|
||||
No tokenizer dependency: approximates tokens as ``chars * 0.25`` and
|
||||
adds fixed system/tools baselines. Returns a wire-safe dict with a
|
||||
deterministic, sorted breakdown. ``estimate`` is always ``True`` since
|
||||
this is a heuristic, not a provider-reported count.
|
||||
"""
|
||||
message_chars = 0
|
||||
tool_chars = 0
|
||||
memory_chars = 0
|
||||
for m in messages:
|
||||
content = m.get("content") or ""
|
||||
message_chars += len(str(content))
|
||||
tool_calls = m.get("toolCalls") or m.get("tool_calls") or []
|
||||
for tc in tool_calls:
|
||||
tool_chars += len(str(tc.get("args", "")))
|
||||
tool_chars += len(str(tc.get("result", "")))
|
||||
for el in m.get("elements") or []:
|
||||
memory_chars += len(str(el.get("url", "")))
|
||||
|
||||
system_tokens = _SYSTEM_BASELINE_TOKENS
|
||||
tools_tokens = _TOOLS_BASELINE_TOKENS + int(tool_chars * _TOKENS_PER_CHAR)
|
||||
messages_tokens = int(message_chars * _TOKENS_PER_CHAR)
|
||||
memory_tokens = int(memory_chars * _TOKENS_PER_CHAR)
|
||||
|
||||
breakdown = {
|
||||
"memory": memory_tokens,
|
||||
"messages": messages_tokens,
|
||||
"system": system_tokens,
|
||||
"tools": tools_tokens,
|
||||
}
|
||||
used = system_tokens + tools_tokens + messages_tokens + memory_tokens
|
||||
safe_limit = limit if limit and limit > 0 else DEFAULT_CONTEXT_LIMIT
|
||||
usage_pct = round((used / safe_limit) * 100, 1) if safe_limit else 0.0
|
||||
|
||||
result: Dict[str, Any] = {
|
||||
"breakdown": breakdown,
|
||||
"estimate": True,
|
||||
"limit": limit,
|
||||
"session_id": session_id,
|
||||
"usage_pct": usage_pct,
|
||||
"used": used,
|
||||
}
|
||||
if model:
|
||||
result["model"] = model
|
||||
return result
|
||||
|
||||
|
||||
async def _chat_context_stats(request: Request) -> JSONResponse:
|
||||
"""GET /api/chat/context-stats?session_id= — context budget estimate.
|
||||
|
||||
Config-driven fallback for the composer token ring. Computes a
|
||||
char-based estimate from persisted messages; unknown sessions return
|
||||
a zero-usage estimate rather than 404 so the UI can always render.
|
||||
"""
|
||||
session_id = request.query_params.get("session_id", "")
|
||||
try:
|
||||
limit = int(request.query_params.get("limit", str(DEFAULT_CONTEXT_LIMIT)))
|
||||
except (TypeError, ValueError):
|
||||
limit = DEFAULT_CONTEXT_LIMIT
|
||||
|
||||
messages: List[Dict[str, Any]] = []
|
||||
if session_id:
|
||||
try:
|
||||
from praisonaiui.server import _datastore
|
||||
|
||||
messages = await _datastore.get_messages(session_id) or []
|
||||
except Exception:
|
||||
messages = []
|
||||
|
||||
stats = _estimate_context_stats(messages, session_id=session_id, limit=limit)
|
||||
return JSONResponse(stats)
|
||||
|
||||
|
||||
async def _chat_history(request: Request) -> JSONResponse:
|
||||
"""GET /api/chat/history/{session_id} — get message history."""
|
||||
session_id = request.path_params["session_id"]
|
||||
@@ -854,6 +941,7 @@ class ChatFeature(BaseFeatureProtocol):
|
||||
def routes(self) -> List[Route]:
|
||||
return [
|
||||
Route("/api/chat/send", _chat_send, methods=["POST"]),
|
||||
Route("/api/chat/context-stats", _chat_context_stats, methods=["GET"]),
|
||||
Route("/api/chat/history/{session_id}", _chat_history, methods=["GET"]),
|
||||
Route("/api/chat/abort", _chat_abort, methods=["POST"]),
|
||||
WebSocketRoute("/api/chat/ws", _chat_ws),
|
||||
|
||||
@@ -176,6 +176,49 @@ CONFIG_SCHEMA = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"finops": {
|
||||
"type": "object",
|
||||
"title": "FinOps & Budgets",
|
||||
"description": (
|
||||
"Alerts appear on Overview and Chat. Does not block agent runs."
|
||||
),
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"title": "Enable FinOps",
|
||||
"default": True,
|
||||
},
|
||||
"daily_token_budget": {
|
||||
"type": "integer",
|
||||
"title": "Daily Token Budget",
|
||||
"minimum": 0,
|
||||
},
|
||||
"daily_cost_budget_usd": {
|
||||
"type": "number",
|
||||
"title": "Daily Cost Budget (USD)",
|
||||
"minimum": 0,
|
||||
},
|
||||
"warn_pct": {
|
||||
"type": "integer",
|
||||
"title": "Warn Threshold (%)",
|
||||
"minimum": 1,
|
||||
"maximum": 99,
|
||||
"default": 80,
|
||||
},
|
||||
"critical_pct": {
|
||||
"type": "integer",
|
||||
"title": "Critical Threshold (%)",
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
"default": 95,
|
||||
},
|
||||
"show_session_chip": {
|
||||
"type": "boolean",
|
||||
"title": "Show Session Cost Chip",
|
||||
"default": True,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -216,6 +259,24 @@ def validate_config(config: Dict[str, Any], schema: Dict[str, Any] = None) -> Li
|
||||
if "maximum" in prop_schema and value > prop_schema["maximum"]:
|
||||
errors.append(f"{key}: must be <= {prop_schema['maximum']}")
|
||||
|
||||
errors.extend(_validate_finops(config.get("finops")))
|
||||
return errors
|
||||
|
||||
|
||||
def _validate_finops(finops: Any) -> List[str]:
|
||||
"""Cross-field validation for the FinOps section (E4/E21)."""
|
||||
if not isinstance(finops, dict):
|
||||
return []
|
||||
errors: List[str] = []
|
||||
warn = finops.get("warn_pct", 80)
|
||||
critical = finops.get("critical_pct", 95)
|
||||
if isinstance(warn, (int, float)) and isinstance(critical, (int, float)):
|
||||
if critical <= warn:
|
||||
errors.append("finops.critical_pct: must be greater than warn_pct")
|
||||
for field in ("daily_token_budget", "daily_cost_budget_usd"):
|
||||
val = finops.get(field)
|
||||
if isinstance(val, (int, float)) and val < 0:
|
||||
errors.append(f"finops.{field}: must be >= 0")
|
||||
return errors
|
||||
|
||||
|
||||
|
||||
@@ -73,6 +73,31 @@ class JSONLocaleManager(I18nProtocol):
|
||||
"chat.placeholder": "Type your message...",
|
||||
"chat.send": "Send",
|
||||
"chat.thinking": "Thinking...",
|
||||
"chat.context.healthy": "Context healthy",
|
||||
"chat.context.warning": "Approaching context limit",
|
||||
"chat.context.critical": "Context nearly full",
|
||||
"chat.context.estimate": "Estimate",
|
||||
"chat.compaction.banner": "Context nearly full \u2014 older messages may be summarized on next send",
|
||||
"chat.compaction.compacted": "Context compacted",
|
||||
"chat.memory.chip": "{count} memories",
|
||||
"chat.memory.empty_turn": "No memories for this turn",
|
||||
"skills.title": "Skills Studio",
|
||||
"skills.subtitle": "Review self_improve proposals",
|
||||
"skills.tab.pending": "Pending",
|
||||
"skills.tab.installed": "Installed",
|
||||
"skills.tab.history": "History",
|
||||
"skills.diff.current": "Current",
|
||||
"skills.diff.proposed": "Proposed",
|
||||
"skills.diff.no_existing": "No existing skill",
|
||||
"skills.action.approve": "Approve",
|
||||
"skills.action.reject": "Reject",
|
||||
"skills.pending.empty": "No pending skill writes",
|
||||
"finops.strip.title": "Cost FinOps",
|
||||
"finops.strip.today": "Today: {tokens} tokens",
|
||||
"finops.strip.view_usage": "View usage",
|
||||
"finops.banner.warn": "Daily token budget at {pct}% \u2014 consider lighter models",
|
||||
"finops.banner.critical": "Daily token budget at {pct}% \u2014 review usage",
|
||||
"finops.chip.label": "{tokens} \u00b7 {model}",
|
||||
"nav.dashboard": "Dashboard",
|
||||
"nav.agents": "Agents",
|
||||
"nav.settings": "Settings",
|
||||
@@ -86,6 +111,31 @@ class JSONLocaleManager(I18nProtocol):
|
||||
"chat.placeholder": "Escribe tu mensaje...",
|
||||
"chat.send": "Enviar",
|
||||
"chat.thinking": "Pensando...",
|
||||
"chat.context.healthy": "Contexto saludable",
|
||||
"chat.context.warning": "Acercándose al límite de contexto",
|
||||
"chat.context.critical": "Contexto casi lleno",
|
||||
"chat.context.estimate": "Estimación",
|
||||
"chat.compaction.banner": "Contexto casi lleno \u2014 los mensajes antiguos pueden resumirse al enviar",
|
||||
"chat.compaction.compacted": "Contexto compactado",
|
||||
"chat.memory.chip": "{count} recuerdos",
|
||||
"chat.memory.empty_turn": "Sin recuerdos para este turno",
|
||||
"skills.title": "Estudio de habilidades",
|
||||
"skills.subtitle": "Revisar propuestas de self_improve",
|
||||
"skills.tab.pending": "Pendientes",
|
||||
"skills.tab.installed": "Instaladas",
|
||||
"skills.tab.history": "Historial",
|
||||
"skills.diff.current": "Actual",
|
||||
"skills.diff.proposed": "Propuesto",
|
||||
"skills.diff.no_existing": "No existe la habilidad",
|
||||
"skills.action.approve": "Aprobar",
|
||||
"skills.action.reject": "Rechazar",
|
||||
"skills.pending.empty": "No hay escrituras de habilidades pendientes",
|
||||
"finops.strip.title": "FinOps de costos",
|
||||
"finops.strip.today": "Hoy: {tokens} tokens",
|
||||
"finops.strip.view_usage": "Ver uso",
|
||||
"finops.banner.warn": "Presupuesto diario de tokens al {pct}% \u2014 considere modelos más ligeros",
|
||||
"finops.banner.critical": "Presupuesto diario de tokens al {pct}% \u2014 revise el uso",
|
||||
"finops.chip.label": "{tokens} \u00b7 {model}",
|
||||
"nav.dashboard": "Panel",
|
||||
"nav.agents": "Agentes",
|
||||
"nav.settings": "Configuración",
|
||||
@@ -99,6 +149,31 @@ class JSONLocaleManager(I18nProtocol):
|
||||
"chat.placeholder": "Tapez votre message...",
|
||||
"chat.send": "Envoyer",
|
||||
"chat.thinking": "Réflexion...",
|
||||
"chat.context.healthy": "Contexte sain",
|
||||
"chat.context.warning": "Approche de la limite de contexte",
|
||||
"chat.context.critical": "Contexte presque plein",
|
||||
"chat.context.estimate": "Estimation",
|
||||
"chat.compaction.banner": "Contexte presque plein \u2014 les anciens messages peuvent être résumés au prochain envoi",
|
||||
"chat.compaction.compacted": "Contexte compacté",
|
||||
"chat.memory.chip": "{count} souvenirs",
|
||||
"chat.memory.empty_turn": "Aucun souvenir pour ce tour",
|
||||
"skills.title": "Studio de comp\u00e9tences",
|
||||
"skills.subtitle": "Examiner les propositions de self_improve",
|
||||
"skills.tab.pending": "En attente",
|
||||
"skills.tab.installed": "Install\u00e9es",
|
||||
"skills.tab.history": "Historique",
|
||||
"skills.diff.current": "Actuel",
|
||||
"skills.diff.proposed": "Propos\u00e9",
|
||||
"skills.diff.no_existing": "Aucune comp\u00e9tence existante",
|
||||
"skills.action.approve": "Approuver",
|
||||
"skills.action.reject": "Rejeter",
|
||||
"skills.pending.empty": "Aucune \u00e9criture de comp\u00e9tence en attente",
|
||||
"finops.strip.title": "FinOps des coûts",
|
||||
"finops.strip.today": "Aujourd'hui : {tokens} tokens",
|
||||
"finops.strip.view_usage": "Voir l'utilisation",
|
||||
"finops.banner.warn": "Budget quotidien de tokens à {pct}% \u2014 envisagez des modèles plus légers",
|
||||
"finops.banner.critical": "Budget quotidien de tokens à {pct}% \u2014 vérifiez l'utilisation",
|
||||
"finops.chip.label": "{tokens} \u00b7 {model}",
|
||||
"nav.dashboard": "Tableau de bord",
|
||||
"nav.agents": "Agents",
|
||||
"nav.settings": "Paramètres",
|
||||
|
||||
@@ -26,6 +26,24 @@ from ._base import BaseFeatureProtocol
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_EMBEDDING_ERROR_MARKERS = (
|
||||
"does not have access",
|
||||
"model_not_found",
|
||||
"does not exist or you do not have access",
|
||||
"notfounderror",
|
||||
)
|
||||
|
||||
|
||||
def _is_embedding_error(err_msg: str) -> bool:
|
||||
"""True when an error string indicates an unavailable embedding model."""
|
||||
lowered = err_msg.lower()
|
||||
return any(marker in lowered for marker in _EMBEDDING_ERROR_MARKERS)
|
||||
|
||||
|
||||
def _is_embedding_unavailable(err: BaseException) -> bool:
|
||||
"""Detect an embedding model access / not-found failure from an exception."""
|
||||
return _is_embedding_error(str(err))
|
||||
|
||||
|
||||
# ── Knowledge Protocol ───────────────────────────────────────────────
|
||||
|
||||
@@ -91,6 +109,10 @@ class KnowledgeProtocol(ABC):
|
||||
"""Get corpus statistics."""
|
||||
return {"total": 0}
|
||||
|
||||
def last_search_meta(self) -> Dict[str, Any]:
|
||||
"""Metadata for the most recent search: {"mode": ..., "error": ...}."""
|
||||
return {"mode": "vector", "error": None}
|
||||
|
||||
def health(self) -> Dict[str, Any]:
|
||||
"""Health check for this backend."""
|
||||
return {"status": "ok", "provider": self.__class__.__name__}
|
||||
@@ -171,8 +193,12 @@ class SimpleKnowledgeManager(KnowledgeProtocol):
|
||||
"status": "ok",
|
||||
"provider": "SimpleKnowledgeManager",
|
||||
"total_entries": len(self._entries),
|
||||
"embedding": {"available": False, "reason": "no_vector_backend"},
|
||||
}
|
||||
|
||||
def last_search_meta(self) -> Dict[str, Any]:
|
||||
return {"mode": "local", "error": None}
|
||||
|
||||
|
||||
# ── SDK Knowledge Manager (wraps praisonaiagents.knowledge) ──────────
|
||||
|
||||
@@ -191,6 +217,9 @@ class SDKKnowledgeManager(KnowledgeProtocol):
|
||||
self._sdk_knowledge = None
|
||||
self._sdk_probed = False # True once we've verified SDK actually works
|
||||
self._local_index: Dict[str, Dict[str, Any]] = {}
|
||||
self._embedding_unavailable = False
|
||||
self._embedding_error: Optional[str] = None
|
||||
self._last_search_meta: Dict[str, Any] = {"mode": "vector", "error": None}
|
||||
|
||||
def _get_sdk_knowledge(self) -> Any:
|
||||
"""Lazy-init SDK Knowledge with health probe.
|
||||
@@ -242,6 +271,10 @@ class SDKKnowledgeManager(KnowledgeProtocol):
|
||||
"using local in-memory index"
|
||||
)
|
||||
self._sdk_knowledge = None
|
||||
elif _is_embedding_error(err_msg):
|
||||
self._embedding_unavailable = True
|
||||
self._embedding_error = err_msg
|
||||
logger.warning("Embedding model unavailable: %s", e)
|
||||
else:
|
||||
# Other errors (e.g. empty DB) are fine — SDK works
|
||||
pass
|
||||
@@ -249,6 +282,10 @@ class SDKKnowledgeManager(KnowledgeProtocol):
|
||||
self._sdk_probed = True
|
||||
return self._sdk_knowledge
|
||||
|
||||
def last_search_meta(self) -> Dict[str, Any]:
|
||||
"""Metadata for the most recent search: {"mode": ..., "error": ...}."""
|
||||
return dict(self._last_search_meta)
|
||||
|
||||
def store(
|
||||
self,
|
||||
text: str,
|
||||
@@ -294,6 +331,7 @@ class SDKKnowledgeManager(KnowledgeProtocol):
|
||||
try:
|
||||
results = sdk.search(query, limit=limit, user_id="praisonaiui")
|
||||
if isinstance(results, list) and results:
|
||||
self._last_search_meta = {"mode": "vector", "error": None}
|
||||
return [
|
||||
{
|
||||
"id": str(r.get("id", i)),
|
||||
@@ -304,7 +342,11 @@ class SDKKnowledgeManager(KnowledgeProtocol):
|
||||
for i, r in enumerate(results[:limit])
|
||||
]
|
||||
except BaseException as e:
|
||||
err_msg = str(e)
|
||||
logger.warning("SDK knowledge search failed: %s; falling back to local", e)
|
||||
if _is_embedding_error(err_msg):
|
||||
self._embedding_unavailable = True
|
||||
self._embedding_error = err_msg
|
||||
|
||||
# Fallback to local text search
|
||||
query_lower = query.lower()
|
||||
@@ -314,6 +356,21 @@ class SDKKnowledgeManager(KnowledgeProtocol):
|
||||
results.append(e)
|
||||
if len(results) >= limit:
|
||||
break
|
||||
|
||||
if self._embedding_unavailable:
|
||||
self._last_search_meta = {
|
||||
"mode": "fallback",
|
||||
"error": (
|
||||
"Vector search unavailable: "
|
||||
f"{self._embedding_error or 'embedding model not accessible'}. "
|
||||
"Set AIUI_EMBEDDING_MODEL=text-embedding-ada-002 or enable embedding "
|
||||
"models in your provider project."
|
||||
),
|
||||
}
|
||||
elif sdk is not None:
|
||||
self._last_search_meta = {"mode": "vector", "error": None}
|
||||
else:
|
||||
self._last_search_meta = {"mode": "local", "error": None}
|
||||
return results
|
||||
|
||||
def add_file(
|
||||
@@ -453,11 +510,25 @@ class SDKKnowledgeManager(KnowledgeProtocol):
|
||||
|
||||
def health(self) -> Dict[str, Any]:
|
||||
sdk = self._get_sdk_knowledge()
|
||||
embedding_available = sdk is not None and not self._embedding_unavailable
|
||||
warnings: List[str] = []
|
||||
embedding: Dict[str, Any] = {"available": embedding_available}
|
||||
if self._embedding_unavailable:
|
||||
embedding["error"] = self._embedding_error
|
||||
warnings.append(
|
||||
"Embedding model unavailable; knowledge search degraded to local text "
|
||||
"matching. Set AIUI_EMBEDDING_MODEL to an accessible model."
|
||||
)
|
||||
status = "ok"
|
||||
if sdk is None or self._embedding_unavailable:
|
||||
status = "degraded"
|
||||
return {
|
||||
"status": "ok" if sdk is not None else "degraded",
|
||||
"status": status,
|
||||
"provider": "SDKKnowledgeManager",
|
||||
"sdk_available": sdk is not None,
|
||||
"total_indexed": len(self._local_index),
|
||||
"embedding": embedding,
|
||||
"warnings": warnings,
|
||||
}
|
||||
|
||||
|
||||
@@ -626,7 +697,15 @@ class KnowledgeFeature(BaseFeatureProtocol):
|
||||
query=query,
|
||||
limit=body.get("limit", 10),
|
||||
)
|
||||
return JSONResponse({"results": results, "count": len(results)})
|
||||
meta = mgr.last_search_meta()
|
||||
return JSONResponse(
|
||||
{
|
||||
"results": results,
|
||||
"count": len(results),
|
||||
"search_mode": meta.get("mode", "unknown"),
|
||||
"warning": meta.get("error"),
|
||||
}
|
||||
)
|
||||
|
||||
async def _add_file(self, request: Request) -> JSONResponse:
|
||||
"""POST /api/knowledge/add-file — ingest a file."""
|
||||
@@ -675,6 +754,8 @@ class KnowledgeFeature(BaseFeatureProtocol):
|
||||
"files": stats.get("files", 0),
|
||||
"backend": h.get("provider", "unknown"),
|
||||
"status": h.get("status", "ok"),
|
||||
"embedding": h.get("embedding", {"available": True}),
|
||||
"warnings": h.get("warnings", []),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@ async def run_startup_hooks() -> None:
|
||||
return
|
||||
|
||||
_log.info(f"Running {len(_startup_hooks)} startup hooks")
|
||||
start_time = time.time()
|
||||
start_time = time.perf_counter()
|
||||
|
||||
for hook in _startup_hooks:
|
||||
try:
|
||||
@@ -187,7 +187,7 @@ async def run_startup_hooks() -> None:
|
||||
_log.error(f"Startup hook failed: {getattr(hook, '__name__', str(hook))}: {e}")
|
||||
# Continue with other hooks - individual failures shouldn't break startup
|
||||
|
||||
execution_time = time.time() - start_time
|
||||
execution_time = time.perf_counter() - start_time
|
||||
_lifecycle_state.update(
|
||||
{
|
||||
"startup_completed": True,
|
||||
@@ -209,7 +209,7 @@ async def run_shutdown_hooks() -> None:
|
||||
|
||||
_lifecycle_state["shutdown_initiated"] = True
|
||||
_log.info(f"Running {len(_shutdown_hooks)} shutdown hooks")
|
||||
start_time = time.time()
|
||||
start_time = time.perf_counter()
|
||||
|
||||
# Get shutdown timeout from environment
|
||||
timeout = float(os.environ.get("AIUI_SHUTDOWN_TIMEOUT", "30.0"))
|
||||
@@ -227,7 +227,7 @@ async def run_shutdown_hooks() -> None:
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(_execute_hooks(), timeout=timeout)
|
||||
execution_time = time.time() - start_time
|
||||
execution_time = time.perf_counter() - start_time
|
||||
_lifecycle_state["shutdown_time"] = execution_time
|
||||
_log.info(f"Shutdown hooks completed in {execution_time:.3f}s")
|
||||
except asyncio.TimeoutError:
|
||||
|
||||
@@ -6,6 +6,7 @@ Wraps praisonai.capabilities functions as HTTP endpoints.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
from dataclasses import asdict
|
||||
@@ -17,6 +18,35 @@ from starlette.routing import Route
|
||||
|
||||
from ._base import BaseFeatureProtocol
|
||||
|
||||
|
||||
def _default_embedding_model() -> str:
|
||||
"""Resolve the default embedding model from environment."""
|
||||
return os.environ.get(
|
||||
"AIUI_EMBEDDING_MODEL",
|
||||
os.environ.get("OPENAI_EMBEDDING_MODEL", "text-embedding-3-small"),
|
||||
)
|
||||
|
||||
|
||||
def _embedding_fallback_models() -> list[str]:
|
||||
"""Resolve the embedding fallback chain from environment."""
|
||||
raw = os.environ.get(
|
||||
"AIUI_EMBEDDING_FALLBACK_MODELS",
|
||||
"text-embedding-ada-002,text-embedding-3-small",
|
||||
)
|
||||
return [m.strip() for m in raw.split(",") if m.strip()]
|
||||
|
||||
|
||||
def _is_model_not_found(err: BaseException) -> bool:
|
||||
"""Heuristically detect embedding model access / not-found errors."""
|
||||
msg = str(err).lower()
|
||||
return (
|
||||
"does not have access" in msg
|
||||
or "model_not_found" in msg
|
||||
or "does not exist" in msg
|
||||
or "notfounderror" in msg
|
||||
)
|
||||
|
||||
|
||||
# Lazy import capabilities
|
||||
_capabilities = None
|
||||
|
||||
@@ -280,29 +310,57 @@ class OpenAIAPIFeature(BaseFeatureProtocol):
|
||||
|
||||
body = await request.json()
|
||||
input_text = body.get("input", "")
|
||||
model = body.get("model", "text-embedding-3-small")
|
||||
model = body.get("model") or _default_embedding_model()
|
||||
|
||||
try:
|
||||
result = await caps.aembed(
|
||||
input=input_text,
|
||||
model=model,
|
||||
)
|
||||
candidates = [model]
|
||||
for fallback in _embedding_fallback_models():
|
||||
if fallback not in candidates:
|
||||
candidates.append(fallback)
|
||||
|
||||
data = _result_to_dict(result)
|
||||
last_err: BaseException | None = None
|
||||
for candidate in candidates:
|
||||
try:
|
||||
result = await caps.aembed(
|
||||
input=input_text,
|
||||
model=candidate,
|
||||
)
|
||||
data = _result_to_dict(result)
|
||||
return JSONResponse(
|
||||
{
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"object": "embedding",
|
||||
"index": 0,
|
||||
"embedding": data.get("embedding", []),
|
||||
}
|
||||
],
|
||||
"model": candidate,
|
||||
"usage": data.get("usage", {"prompt_tokens": 0, "total_tokens": 0}),
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
last_err = e
|
||||
if not _is_model_not_found(e):
|
||||
break
|
||||
|
||||
if last_err is not None and _is_model_not_found(last_err):
|
||||
return JSONResponse(
|
||||
{
|
||||
"object": "list",
|
||||
"data": [
|
||||
{"object": "embedding", "index": 0, "embedding": data.get("embedding", [])}
|
||||
],
|
||||
"model": model,
|
||||
"usage": data.get("usage", {"prompt_tokens": 0, "total_tokens": 0}),
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
return JSONResponse(
|
||||
{"error": {"message": str(e), "type": "api_error"}}, status_code=500
|
||||
"error": {
|
||||
"message": (
|
||||
f"Embedding model unavailable: {last_err}. "
|
||||
"Set AIUI_EMBEDDING_MODEL to a model your project can access "
|
||||
"(e.g. text-embedding-ada-002)."
|
||||
),
|
||||
"type": "model_not_found",
|
||||
}
|
||||
},
|
||||
status_code=503,
|
||||
)
|
||||
return JSONResponse(
|
||||
{"error": {"message": str(last_err), "type": "api_error"}}, status_code=500
|
||||
)
|
||||
|
||||
async def _image_generate(self, request: Request) -> JSONResponse:
|
||||
"""POST /v1/images/generations — Generate images."""
|
||||
|
||||
@@ -116,7 +116,14 @@ class OpenAIRealtimeManager(RealtimeProtocol):
|
||||
logger.debug(f"Audio chunk for session {session_id}: {len(audio_data)} bytes")
|
||||
|
||||
async def receive_audio(self, session_id: str) -> AsyncIterator[Dict[str, Any]]:
|
||||
"""Receive events from OpenAI realtime session."""
|
||||
"""Receive events from OpenAI realtime session.
|
||||
|
||||
First-event contract:
|
||||
- Unknown session -> ``error`` (Session not found)
|
||||
- ``OPENAI_API_KEY`` set -> ``session.ready`` (WebRTC handshake)
|
||||
- Integrated mode, no key -> ``error`` (status=degraded)
|
||||
- Standalone, no key -> ``conversation.item.created`` (mock)
|
||||
"""
|
||||
# In WebRTC mode, events come via WebRTC connection
|
||||
# This method would handle transcript/tool call events
|
||||
session = self._sessions.get(session_id)
|
||||
|
||||
@@ -132,6 +132,14 @@ class _InMemorySessionStore(SessionProtocol):
|
||||
_session_metadata: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
|
||||
_MESSAGE_COPY_FIELDS = ("role", "content", "text", "tool_calls", "name", "metadata")
|
||||
|
||||
|
||||
def _sanitize_message(message: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Copy a message for a forked session, stripping ephemeral identifiers."""
|
||||
return {k: message[k] for k in _MESSAGE_COPY_FIELDS if k in message}
|
||||
|
||||
|
||||
def _get_metadata(session_id: str) -> Dict[str, Any]:
|
||||
"""Get session metadata (labels, usage, etc)."""
|
||||
if session_id not in _session_metadata:
|
||||
@@ -166,6 +174,7 @@ class SessionsFeature(BaseFeatureProtocol):
|
||||
Route("/api/sessions/{session_id}/context", self._build_context, methods=["POST"]),
|
||||
Route("/api/sessions/{session_id}/compact", self._compact, methods=["POST"]),
|
||||
Route("/api/sessions/{session_id}/reset", self._reset, methods=["POST"]),
|
||||
Route("/api/sessions/{session_id}/fork", self._fork, methods=["POST"]),
|
||||
Route("/api/sessions/{session_id}/preview", self._preview, methods=["GET"]),
|
||||
Route("/api/sessions/{session_id}/labels", self._labels, methods=["GET"]),
|
||||
Route("/api/sessions/{session_id}/labels", self._set_labels, methods=["POST"]),
|
||||
@@ -307,6 +316,16 @@ class SessionsFeature(BaseFeatureProtocol):
|
||||
or s.get("agent")
|
||||
or s.get("metadata", {}).get("agent_id")
|
||||
),
|
||||
"platform": (
|
||||
s.get("platform")
|
||||
or s.get("metadata", {}).get("platform")
|
||||
or meta.get("_platform")
|
||||
),
|
||||
"channel_id": (
|
||||
s.get("channel_id")
|
||||
or s.get("metadata", {}).get("channel_id")
|
||||
or (sid[len("channel-"):] if sid.startswith("channel-") else None)
|
||||
),
|
||||
"source": s.get("source") or s.get("metadata", {}).get("source") or "datastore",
|
||||
}
|
||||
)
|
||||
@@ -519,6 +538,89 @@ class SessionsFeature(BaseFeatureProtocol):
|
||||
}
|
||||
)
|
||||
|
||||
async def _fork(self, request: Request) -> JSONResponse:
|
||||
"""POST /api/sessions/{id}/fork — Branch a session into a new child.
|
||||
|
||||
Body (all optional):
|
||||
up_to_index: int — keep messages[0..up_to_index] inclusive
|
||||
title_suffix: str — appended to the parent title (default " (fork)")
|
||||
agent_id: str — override recorded on the child metadata
|
||||
labels: list[str] — labels applied to the child
|
||||
"""
|
||||
source_id = request.path_params["session_id"]
|
||||
|
||||
if source_id.startswith("agent:"):
|
||||
return JSONResponse(
|
||||
{"error": "Cannot fork a virtual agent session"}, status_code=400
|
||||
)
|
||||
|
||||
content_type = request.headers.get("content-type") or ""
|
||||
body = await request.json() if content_type.startswith("application/json") else {}
|
||||
|
||||
try:
|
||||
from praisonaiui.server import get_datastore
|
||||
|
||||
ds = get_datastore()
|
||||
except Exception as e:
|
||||
logger.warning(f"fork: datastore unavailable: {e}")
|
||||
return JSONResponse({"error": "Datastore unavailable"}, status_code=503)
|
||||
|
||||
source = await ds.get_session(source_id)
|
||||
if source is None:
|
||||
return JSONResponse({"error": "Source session not found"}, status_code=404)
|
||||
|
||||
messages = await ds.get_messages(source_id)
|
||||
total = len(messages)
|
||||
|
||||
up_to_index = body.get("up_to_index")
|
||||
if up_to_index is not None:
|
||||
if not isinstance(up_to_index, int) or up_to_index < 0 or up_to_index >= max(total, 1):
|
||||
return JSONResponse(
|
||||
{"error": "up_to_index out of range", "total_messages": total},
|
||||
status_code=400,
|
||||
)
|
||||
messages = messages[: up_to_index + 1]
|
||||
|
||||
child = await ds.create_session()
|
||||
child_id = child.get("id") or child.get("session_id")
|
||||
|
||||
for msg in messages:
|
||||
await ds.add_message(child_id, _sanitize_message(msg))
|
||||
|
||||
suffix = body.get("title_suffix", " (fork)")
|
||||
parent_title = source.get("title") or ""
|
||||
child_meta = {
|
||||
"parent_session_id": source_id,
|
||||
"forked_at": time.time(),
|
||||
"fork_point_index": up_to_index,
|
||||
"source": "playground-fork",
|
||||
}
|
||||
if body.get("agent_id"):
|
||||
child_meta["agent_id"] = body["agent_id"]
|
||||
|
||||
try:
|
||||
await ds.update_session(
|
||||
child_id,
|
||||
title=f"{parent_title}{suffix}",
|
||||
metadata=child_meta,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"fork: update_session metadata failed: {e}")
|
||||
|
||||
labels = body.get("labels")
|
||||
if isinstance(labels, list) and labels:
|
||||
_get_metadata(child_id)["_labels"] = [str(x) for x in labels]
|
||||
|
||||
return JSONResponse(
|
||||
{
|
||||
"session_id": child_id,
|
||||
"parent_id": source_id,
|
||||
"forked_at_index": up_to_index,
|
||||
"message_count": len(messages),
|
||||
"created_at": time.time(),
|
||||
}
|
||||
)
|
||||
|
||||
async def _labels(self, request: Request) -> JSONResponse:
|
||||
sid = request.path_params["session_id"]
|
||||
meta = _get_metadata(sid)
|
||||
|
||||
@@ -2860,7 +2860,14 @@ def create_app(
|
||||
require_auth: bool = False,
|
||||
config_path: Optional[Path] = None,
|
||||
) -> Starlette:
|
||||
"""Create the Starlette application."""
|
||||
"""Create the Starlette application.
|
||||
|
||||
URL-token auth (``TokenQueryMiddleware``) is opt-in: it activates only
|
||||
when ``AIUI_REQUIRE_TOKEN`` is truthy, YAML ``auth.requireToken`` is set,
|
||||
or ``require_auth=True``. A bare ``GATEWAY_AUTH_TOKEN`` /
|
||||
``AIUI_URL_TOKEN`` env var (e.g. leaked into a developer shell after a
|
||||
gateway run) no longer forces 401 on standalone ``/api/*`` (#238, #251).
|
||||
"""
|
||||
from praisonaiui.auth import (
|
||||
AuthMiddleware,
|
||||
login_handler,
|
||||
@@ -2874,10 +2881,13 @@ def create_app(
|
||||
config = load_config_from_yaml(config_path)
|
||||
|
||||
# Extract auth settings from config
|
||||
_config_requires_token = False
|
||||
if config:
|
||||
auth_config = config.get("auth", {})
|
||||
if auth_config.get("requireAuth") or auth_config.get("require_auth"):
|
||||
require_auth = True
|
||||
if auth_config.get("requireToken") or auth_config.get("require_token"):
|
||||
_config_requires_token = True
|
||||
|
||||
middleware = [
|
||||
Middleware(
|
||||
@@ -2891,10 +2901,20 @@ def create_app(
|
||||
# Add optional AUTH_ENFORCE middleware (env var driven)
|
||||
middleware.append(Middleware(AuthEnforcementMiddleware))
|
||||
|
||||
# URL-token auth — opt-in via env. Enables query-param / cookie auth so
|
||||
# dashboards can be shared by URL (Jupyter model).
|
||||
# URL-token auth — opt-in. Enables query-param / cookie auth so dashboards
|
||||
# can be shared by URL (Jupyter model). The mere presence of a token env
|
||||
# var (e.g. GATEWAY_AUTH_TOKEN leaking into a developer shell after a
|
||||
# gateway run) must NOT force auth on standalone create_app() — that broke
|
||||
# ad-hoc TestClient scripts (#238, #251). Activation now requires an
|
||||
# explicit opt-in: AIUI_REQUIRE_TOKEN=1, YAML auth.requireToken, or the
|
||||
# require_auth flag.
|
||||
_tok = os.environ.get("AIUI_URL_TOKEN") or os.environ.get("GATEWAY_AUTH_TOKEN")
|
||||
if _tok:
|
||||
_token_auth_opt_in = (
|
||||
os.environ.get("AIUI_REQUIRE_TOKEN", "").lower() in ("true", "1", "yes")
|
||||
or _config_requires_token
|
||||
or require_auth
|
||||
)
|
||||
if _tok and _token_auth_opt_in:
|
||||
from praisonaiui.auth import TokenQueryMiddleware
|
||||
middleware.append(
|
||||
Middleware(TokenQueryMiddleware, expected_token=_tok)
|
||||
@@ -3284,6 +3304,14 @@ def create_app(
|
||||
"description": "Messaging platform connections",
|
||||
"order": 5,
|
||||
},
|
||||
{
|
||||
"id": "inbox",
|
||||
"title": "Channel Inbox",
|
||||
"icon": "📥",
|
||||
"group": "Agent",
|
||||
"description": "Unified multi-channel conversation triage",
|
||||
"order": 6,
|
||||
},
|
||||
{
|
||||
"id": "sessions",
|
||||
"title": "Sessions",
|
||||
@@ -3324,6 +3352,14 @@ def create_app(
|
||||
"description": "Async agent jobs",
|
||||
"order": 40,
|
||||
},
|
||||
{
|
||||
"id": "work",
|
||||
"title": "Work Hub",
|
||||
"icon": "🗂️",
|
||||
"group": "Work",
|
||||
"description": "Unified kanban, card detail, chat and job log",
|
||||
"order": 9,
|
||||
},
|
||||
{
|
||||
"id": "jobs-board",
|
||||
"title": "Jobs Board",
|
||||
@@ -3372,6 +3408,14 @@ def create_app(
|
||||
"description": "Agent skills & plugins",
|
||||
"order": 20,
|
||||
},
|
||||
{
|
||||
"id": "marketplace",
|
||||
"title": "Marketplace",
|
||||
"icon": "🧩",
|
||||
"group": "Agent",
|
||||
"description": "Browse, install & manage plugins",
|
||||
"order": 22,
|
||||
},
|
||||
{
|
||||
"id": "memory",
|
||||
"title": "Memory",
|
||||
@@ -3452,6 +3496,14 @@ def create_app(
|
||||
"description": "Agent evaluation & accuracy",
|
||||
"order": 40,
|
||||
},
|
||||
{
|
||||
"id": "code-studio",
|
||||
"title": "Code Studio",
|
||||
"icon": "💻",
|
||||
"group": "Control",
|
||||
"description": "Write, run, and share code in a sandbox",
|
||||
"order": 45,
|
||||
},
|
||||
{
|
||||
"id": "telemetry",
|
||||
"title": "Telemetry",
|
||||
@@ -3499,6 +3551,8 @@ def create_app(
|
||||
"cron": "/api/schedules",
|
||||
"channels": "/api/channels",
|
||||
"mcp": "/api/mcp/servers",
|
||||
"marketplace": "/api/marketplace/plugins",
|
||||
"code-studio": "/api/code/languages",
|
||||
}
|
||||
|
||||
# Read page whitelist from config.yaml if set_pages() was not called
|
||||
|
||||
@@ -1 +1 @@
|
||||
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-content:"";--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-red-950:oklch(25.8% .092 26.042);--color-orange-50:oklch(98% .016 73.684);--color-orange-100:oklch(95.4% .038 75.164);--color-orange-200:oklch(90.1% .076 70.697);--color-orange-400:oklch(75% .183 55.934);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-800:oklch(47% .157 37.304);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-yellow-50:oklch(98.7% .026 102.212);--color-yellow-200:oklch(94.5% .129 101.54);--color-yellow-500:oklch(79.5% .184 86.047);--color-yellow-600:oklch(68.1% .162 75.834);--color-yellow-800:oklch(47.6% .114 61.907);--color-green-50:oklch(98.2% .018 155.826);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-emerald-400:oklch(76.5% .177 163.223);--color-cyan-400:oklch(78.9% .154 211.53);--color-blue-50:oklch(97% .014 254.604);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-blue-900:oklch(37.9% .146 265.522);--color-violet-500:oklch(60.6% .25 292.717);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-pink-400:oklch(71.8% .202 349.761);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--breakpoint-2xl:96rem;--contLine truncated
|
||||
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-content:"";--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-red-950:oklch(25.8% .092 26.042);--color-orange-50:oklch(98% .016 73.684);--color-orange-100:oklch(95.4% .038 75.164);--color-orange-200:oklch(90.1% .076 70.697);--color-orange-400:oklch(75% .183 55.934);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-800:oklch(47% .157 37.304);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-yellow-50:oklch(98.7% .026 102.212);--color-yellow-200:oklch(94.5% .129 101.54);--color-yellow-500:oklch(79.5% .184 86.047);--color-yellow-600:oklch(68.1% .162 75.834);--color-yellow-800:oklch(47.6% .114 61.907);--color-green-50:oklch(98.2% .018 155.826);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-emerald-400:oklch(76.5% .177 163.223);--color-cyan-400:oklch(78.9% .154 211.53);--color-blue-50:oklch(97% .014 254.604);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-blue-900:oklch(37.9% .146 265.522);--color-violet-500:oklch(60.6% .25 292.717);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-pink-400:oklch(71.8% .202 349.761);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--breakpoint-2xl:96rem;--contLine truncated
|
||||
@@ -1,65 +1,18 @@
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/index2.js","assets/common-types.js","assets/index3.js"])))=>i.map(i=>d[i]);
|
||||
function j1(e,t){for(var r=0;r<t.length;r++){const a=t[r];if(typeof a!="string"&&!Array.isArray(a)){for(const o in a)if(o!=="default"&&!(o in e)){const s=Object.getOwnPropertyDescriptor(a,o);s&&Object.defineProperty(e,o,s.get?s:{enumerable:!0,get:()=>a[o]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))a(o);new MutationObserver(o=>{for(const s of o)if(s.type==="childList")for(const u of s.addedNodes)u.tagName==="LINK"&&u.rel==="modulepreload"&&a(u)}).observe(document,{childList:!0,subtree:!0});function r(o){const s={};return o.integrity&&(s.integrity=o.integrity),o.referrerPolicy&&(s.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?s.credentials="include":o.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function a(o){if(o.ep)return;o.ep=!0;const s=r(o);fetch(o.href,s)}})();function Qu(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var of={exports:{}},al={};var LT;function U1(){if(LT)return al;LT=1;var e=Symbol.for("react.transitional.element"),t=Symbol.for("react.fragment");function r(a,o,s){var u=null;if(s!==void 0&&(u=""+s),o.key!==void 0&&(u=""+o.key),"key"in o){s={};for(var d in o)d!=="key"&&(s[d]=o[d])}else s=o;return o=s.ref,{$$typeof:e,type:a,key:u,ref:o!==void 0?o:null,props:s}}return al.Fragment=t,al.jsx=r,al.jsxs=r,al}var DT;function F1(){return DT||(DT=1,of.exports=U1()),of.exports}var g=F1(),lf={exports:{}},Ne={};var MT;function B1(){if(MT)return Ne;MT=1;var e=Symbol.for("react.transitional.element"),t=Symbol.for("react.portal"),r=Symbol.for("react.fragment"),a=Symbol.for("react.strict_mode"),o=Symbol.for("react.profiler"),s=Symbol.for("react.consumer"),u=Symbol.for("react.context"),d=Symbol.for("react.forward_ref"),p=Symbol.for("react.suspense"),f=Symbol.for("react.memo"),h=Symbol.for("react.lazy"),b=Symbol.for("react.activity"),v=Symbol.iterator;function E(U){return U===null||typeof U!="object"?null:(U=v&&U[v]||U["@@iterator"],typeof U=="function"?U:null)}var w={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},T=Object.assign,k={};function A(U,J,I){this.props=U,this.context=J,this.refs=k,this.updater=I||w}A.prototype.isReactComponent={},A.prototype.setState=function(U,J){if(typeof U!="object"&&typeof U!="function"&&U!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,U,J,"setState")},A.prototype.forceUpdate=function(U){this.updater.enqueueForceUpdate(this,U,"forceUpdate")};function _(){}_.prototype=A.prototype;function R(U,J,I){this.props=U,this.context=J,this.refs=k,this.updater=I||w}var L=R.prototype=new _;L.constructor=R,T(L,A.prototype),L.isPureReactComponent=!0;var j=Array.isArray;function D(){}var z={H:null,A:null,T:null,S:null},F=Object.prototype.hasOwnProperty;function V(U,J,I){var ue=I.ref;return{$$typeof:e,type:U,key:J,ref:ue!==void 0?ue:null,props:I}}function M(U,J){return V(U.type,J,U.props)}function H(U){return typeof U=="object"&&U!==null&&U.$$typeof===e}function K(U){var J={"=":"=0",":":"=2"};return"$"+U.replace(/[=:]/g,function(I){return J[I]})}var Z=/\/+/g;function te(U,J){return typeof U=="object"&&U!==null&&U.key!=null?K(""+U.key):J.toString(36)}function Q(U){switch(U.status){case"fulfilled":return U.value;case"rejected":throw U.reason;default:switch(typeof U.status=="string"?U.then(D,D):(U.status="pending",U.then(function(J){U.status==="pending"&&(U.status="fulfilled",U.value=J)},function(J){U.status==="pending"&&(U.status="rejected",U.reason=J)})),U.status){case"fulfilled":return U.value;case"rejected":throw U.reason}}throw U}function P(U,J,I,ue,fe){var de=typeof U;(de==="undefined"||de==="boolean")&&(U=null);var le=!1;if(U===null)le=!0;else switch(de){case"bigint":case"string":case"number":le=!0;break;case"object":switch(U.$$typeof){case e:case t:le=!0;break;case h:return le=U._init,P(le(U._payload),J,I,ue,fe)}}if(le)return fe=fe(U),le=ue===""?"."+te(U,0):ue,j(fe)?(I="",le!=null&&(I=le.replace(Z,"$&/")+"/"),P(fe,J,I,"",function(Re){return Re})):fe!=null&&(H(fe)&&(fe=M(fe,I+(fe.key==null||U&&U.key===fe.key?"":(""+fe.key).replace(Z,"$&/")+"/")+le)),J.push(fe)),1;le=0;var we=ue===""?".":ue+":";if(j(U))for(var me=0;me<U.length;me++)ue=U[me],de=we+te(ue,me),le+=P(ue,J,I,de,fe);else if(me=E(U),typeof me=="function")for(U=me.call(U),me=0;!(ue=U.next()).done;)ue=ue.value,de=we+te(ue,me++),le+=P(ue,J,I,de,fe);else if(de==="object"){if(typeof U.then=="function")return P(Q(U),J,I,ue,fe);throw J=String(U),Error("Objects are not valid as a React child (found: "+(J==="[object Object]"?"object with keys {"+Object.keys(U).join(", ")+"}":J)+"). If you meant to render a collection of children, use an arLine truncated
|
||||
function NL(e,t){for(var r=0;r<t.length;r++){const a=t[r];if(typeof a!="string"&&!Array.isArray(a)){for(const o in a)if(o!=="default"&&!(o in e)){const s=Object.getOwnPropertyDescriptor(a,o);s&&Object.defineProperty(e,o,s.get?s:{enumerable:!0,get:()=>a[o]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))a(o);new MutationObserver(o=>{for(const s of o)if(s.type==="childList")for(const u of s.addedNodes)u.tagName==="LINK"&&u.rel==="modulepreload"&&a(u)}).observe(document,{childList:!0,subtree:!0});function r(o){const s={};return o.integrity&&(s.integrity=o.integrity),o.referrerPolicy&&(s.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?s.credentials="include":o.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function a(o){if(o.ep)return;o.ep=!0;const s=r(o);fetch(o.href,s)}})();function ac(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var ff={exports:{}},ul={};var XT;function RL(){if(XT)return ul;XT=1;var e=Symbol.for("react.transitional.element"),t=Symbol.for("react.fragment");function r(a,o,s){var u=null;if(s!==void 0&&(u=""+s),o.key!==void 0&&(u=""+o.key),"key"in o){s={};for(var c in o)c!=="key"&&(s[c]=o[c])}else s=o;return o=s.ref,{$$typeof:e,type:a,key:u,ref:o!==void 0?o:null,props:s}}return ul.Fragment=t,ul.jsx=r,ul.jsxs=r,ul}var ZT;function CL(){return ZT||(ZT=1,ff.exports=RL()),ff.exports}var g=CL(),gf={exports:{}},Re={};var QT;function IL(){if(QT)return Re;QT=1;var e=Symbol.for("react.transitional.element"),t=Symbol.for("react.portal"),r=Symbol.for("react.fragment"),a=Symbol.for("react.strict_mode"),o=Symbol.for("react.profiler"),s=Symbol.for("react.consumer"),u=Symbol.for("react.context"),c=Symbol.for("react.forward_ref"),p=Symbol.for("react.suspense"),f=Symbol.for("react.memo"),h=Symbol.for("react.lazy"),m=Symbol.for("react.activity"),v=Symbol.iterator;function E(U){return U===null||typeof U!="object"?null:(U=v&&U[v]||U["@@iterator"],typeof U=="function"?U:null)}var x={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},w=Object.assign,k={};function A(U,B,C){this.props=U,this.context=B,this.refs=k,this.updater=C||x}A.prototype.isReactComponent={},A.prototype.setState=function(U,B){if(typeof U!="object"&&typeof U!="function"&&U!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,U,B,"setState")},A.prototype.forceUpdate=function(U){this.updater.enqueueForceUpdate(this,U,"forceUpdate")};function _(){}_.prototype=A.prototype;function N(U,B,C){this.props=U,this.context=B,this.refs=k,this.updater=C||x}var D=N.prototype=new _;D.constructor=N,w(D,A.prototype),D.isPureReactComponent=!0;var F=Array.isArray;function L(){}var z={H:null,A:null,T:null,S:null},$=Object.prototype.hasOwnProperty;function K(U,B,C){var J=C.ref;return{$$typeof:e,type:U,key:B,ref:J!==void 0?J:null,props:C}}function j(U,B){return K(U.type,B,U.props)}function q(U){return typeof U=="object"&&U!==null&&U.$$typeof===e}function X(U){var B={"=":"=0",":":"=2"};return"$"+U.replace(/[=:]/g,function(C){return B[C]})}var ne=/\/+/g;function re(U,B){return typeof U=="object"&&U!==null&&U.key!=null?X(""+U.key):B.toString(36)}function ee(U){switch(U.status){case"fulfilled":return U.value;case"rejected":throw U.reason;default:switch(typeof U.status=="string"?U.then(L,L):(U.status="pending",U.then(function(B){U.status==="pending"&&(U.status="fulfilled",U.value=B)},function(B){U.status==="pending"&&(U.status="rejected",U.reason=B)})),U.status){case"fulfilled":return U.value;case"rejected":throw U.reason}}throw U}function M(U,B,C,J,ae){var ue=typeof U;(ue==="undefined"||ue==="boolean")&&(U=null);var ie=!1;if(U===null)ie=!0;else switch(ue){case"bigint":case"string":case"number":ie=!0;break;case"object":switch(U.$$typeof){case e:case t:ie=!0;break;case h:return ie=U._init,M(ie(U._payload),B,C,J,ae)}}if(ie)return ae=ae(U),ie=J===""?"."+re(U,0):J,F(ae)?(C="",ie!=null&&(C=ie.replace(ne,"$&/")+"/"),M(ae,B,C,"",function(ke){return ke})):ae!=null&&(q(ae)&&(ae=j(ae,C+(ae.key==null||U&&U.key===ae.key?"":(""+ae.key).replace(ne,"$&/")+"/")+ie)),B.push(ae)),1;ie=0;var he=J===""?".":J+":";if(F(U))for(var fe=0;fe<U.length;fe++)J=U[fe],ue=he+re(J,fe),ie+=M(J,B,C,ue,ae);else if(fe=E(U),typeof fe=="function")for(U=fe.call(U),fe=0;!(J=U.next()).done;)J=J.value,ue=he+re(J,fe++),ie+=M(J,B,C,ue,ae);else if(ue==="object"){if(typeof U.then=="function")return M(ee(U),B,C,J,ae);throw B=String(U),Error("Objects are not valid as a React child (found: "+(B==="[object Object]"?"object with keys {"+Object.keys(U).join(", ")+"}":B)+"). If you meant to render a collection of children, use an array instead."Line truncated
|
||||
at`)?" (<anonymous>)":-1<l.stack.indexOf("@")?"@unknown:0:0":""}return`
|
||||
`+qe+n+dt}var qn=!1;function Yn(n,i){if(!n||qn)return"";qn=!0;var l=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var c={DetermineComponentFrameRoot:function(){try{if(i){var oe=function(){throw Error()};if(Object.defineProperty(oe.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(oe,[])}catch(ee){var X=ee}Reflect.construct(n,[],oe)}else{try{oe.call()}catch(ee){X=ee}n.call(oe.prototype)}}else{try{throw Error()}catch(ee){X=ee}(oe=n())&&typeof oe.catch=="function"&&oe.catch(function(){})}}catch(ee){if(ee&&X&&typeof ee.stack=="string")return[ee.stack,X.stack]}return[null,null]}};c.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var m=Object.getOwnPropertyDescriptor(c.DetermineComponentFrameRoot,"name");m&&m.configurable&&Object.defineProperty(c.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var y=c.DetermineComponentFrameRoot(),x=y[0],N=y[1];if(x&&N){var B=x.split(`
|
||||
`),Y=N.split(`
|
||||
`);for(m=c=0;c<B.length&&!B[c].includes("DetermineComponentFrameRoot");)c++;for(;m<Y.length&&!Y[m].includes("DetermineComponentFrameRoot");)m++;if(c===B.length||m===Y.length)for(c=B.length-1,m=Y.length-1;1<=c&&0<=m&&B[c]!==Y[m];)m--;for(;1<=c&&0<=m;c--,m--)if(B[c]!==Y[m]){if(c!==1||m!==1)do if(c--,m--,0>m||B[c]!==Y[m]){var re=`
|
||||
`+B[c].replace(" at new "," at ");return n.displayName&&re.includes("<anonymous>")&&(re=re.replace("<anonymous>",n.displayName)),re}while(1<=c&&0<=m);break}}}finally{qn=!1,Error.prepareStackTrace=l}return(l=n?n.displayName||n.name:"")?st(l):""}function Cr(n,i){switch(n.tag){case 26:case 27:case 5:return st(n.type);case 16:return st("Lazy");case 13:return n.child!==i&&i!==null?st("Suspense Fallback"):st("Suspense");case 19:return st("SuspenseList");case 0:case 15:return Yn(n.type,!1);case 11:return Yn(n.type.render,!1);case 1:return Yn(n.type,!0);case 31:return st("Activity");default:return""}}function ql(n){try{var i="",l=null;do i+=Cr(n,l),l=n,n=n.return;while(n);return i}catch(c){return`
|
||||
Error generating stack: `+c.message+`
|
||||
`+c.stack}}var Wa=Object.prototype.hasOwnProperty,Ka=e.unstable_scheduleCallback,po=e.unstable_cancelCallback,Hc=e.unstable_shouldYield,Vc=e.unstable_requestPaint,zt=e.unstable_now,qc=e.unstable_getCurrentPriorityLevel,ae=e.unstable_ImmediatePriority,pe=e.unstable_UserBlockingPriority,ke=e.unstable_NormalPriority,Me=e.unstable_LowPriority,We=e.unstable_IdlePriority,nn=e.log,or=e.unstable_setDisableYieldValue,Pt=null,vt=null;function Vt(n){if(typeof nn=="function"&&or(n),vt&&typeof vt.setStrictMode=="function")try{vt.setStrictMode(Pt,n)}catch{}}var tt=Math.clz32?Math.clz32:xO,Or=Math.log,In=Math.LN2;function xO(n){return n>>>=0,n===0?32:31-(Or(n)/In|0)|0}var Yl=256,Wl=262144,Kl=4194304;function ma(n){var i=n&42;if(i!==0)return i;switch(n&-n){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return n&261888;case 262144:case 524288:case 1048576:case 2097152:return n&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return n&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return n}}function Xl(n,i,l){var c=n.pendingLanes;if(c===0)return 0;var m=0,y=n.suspendedLanes,x=n.pingedLanes;n=n.warmLanes;var N=c&134217727;return N!==0?(c=N&~y,c!==0?m=ma(c):(x&=N,x!==0?m=ma(x):l||(l=N&~n,l!==0&&(m=ma(l))))):(N=c&~y,N!==0?m=ma(N):x!==0?m=ma(x):l||(l=c&~n,l!==0&&(m=ma(l)))),m===0?0:i!==0&&i!==m&&(i&y)===0&&(y=m&-m,l=i&-i,y>=l||y===32&&(l&4194048)!==0)?i:m}function fo(n,i){return(n.pendingLanes&~(n.suspendedLanes&~n.pingedLanes)&i)===0}function wO(n,i){switch(n){case 1:case 2:case 4:case 8:case 64:return i+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return i+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Uv(){var n=Kl;return Kl<<=1,(Kl&62914560)===0&&(Kl=4194304),n}function Yc(n){for(var i=[],l=0;31>l;l++)i.push(n);return i}function go(n,i){n.pendingLanes|=i,i!==268435456&&(n.suspendedLanes=0,n.pingedLanes=0,n.warmLanes=0)}function TO(n,i,l,c,m,y){var x=n.pendingLanes;n.pendingLanes=l,n.suspendedLanes=0,n.pingedLanes=0,n.warmLanes=0,n.expiredLanes&=l,n.entangledLanes&=l,n.errorRecoveryDisabledLanes&=l,n.shellSuspendCounter=0;var N=n.entanglements,B=n.expirationTimes,Y=n.hiddenUpdates;for(l=x&~l;0<l;){var re=31-tt(l),oe=1<<re;N[re]=0,B[re]=-1;var X=Y[re];if(X!==null)for(Y[re]=null,re=0;re<X.length;re++){var ee=X[re];ee!==null&&(ee.lane&=-536870913)}l&=~oe}c!==0&&Fv(n,c,0),y!==0&&m===0&&n.tag!==0&&(n.suspendedLanes|=y&~(x&~i))}function Fv(n,i,l){n.pendingLanes|=i,n.suspendedLanes&=~i;var c=31-tt(i);n.entangledLanes|=i,n.entanglements[c]=n.entanglements[c]|1073741824|l&261930}function Bv(n,i){var l=n.entangledLanes|=i;for(n=n.entanglements;l;){var c=31-tt(l),m=1<<c;m&i|n[c]&i&&(n[c]|=i),l&=~m}}function zv(n,i){var l=i&-i;return l=(l&42)!==0?1:Wc(l),(l&(n.suspendedLanes|i))!==0?0:l}function Wc(n){switch(n){case 2:n=1;break;case 8:n=4;break;case 32:n=16;break;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:n=128;break;case 268435456:n=134217728;break;default:n=0}return n}function Kc(n){return n&=-n,2<n?8<n?(n&134217727)!==0?32:268435456:8:2}function Pv(){var n=W.p;return n!==0?n:(n=window.event,n===void 0?32:kT(n.type))}function $v(n,i){var l=W.p;try{return W.p=n,i()}finally{W.p=l}}var Ir=Math.random().toString(36).slice(2),kt="__reactFiber$"+Ir,qt="__reactProps$"+Ir,Xa="__reactContainer$"+Ir,Xc="__reactEvents$"+Ir,AO="__reactListeners$"+Ir,kO="__reactHandles$"+Ir,Gv="__reactResources$"+Ir,mo="__reactMarker$"+Ir;function Zc(n){delete n[kt],delete n[qt],delete n[Xc],delete n[AO],delete n[kO]}function Za(n){var i=n[kt];if(i)return i;for(var l=n.parentNode;l;){if(i=l[Xa]||l[kt]){if(l=i.alternate,i.child!==null||l!==null&&l.child!==null)for(n=cT(n);n!==null;){if(l=n[kt])return l;n=cT(n)}return i}n=l,l=n.parentNode}return null}function Qa(n){if(n=n[kt]||n[Xa]){var i=n.tag;if(i===5||i===6||i===13||i===31||i===26||i===27||i===3)return n}return null}function ho(n){var i=n.tag;if(i===5||i===26||i===27||i===6)return n.stateNode;throw Error(a(33))}function Ja(n){var i=n[Gv];return i||(i=n[Gv]={hoistableStyles:new Map,hoistableScripts:new Map}),i}function wt(n){n[mo]=!0}var Hv=new Set,Vv={};function ha(n,i){ei(n,i),ei(n+"Capture",i)}function ei(n,i){for(Vv[n]=i,n=0;n<i.length;n++)Hv.add(i[n])}var _O=RegExp("^[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u20
Line truncated
|
||||
`).replace(a1,"")}function eT(n,i){return i=Jw(i),Jw(n)===i}function Je(n,i,l,c,m,y){switch(l){case"children":typeof c=="string"?i==="body"||i==="textarea"&&c===""||ni(n,c):(typeof c=="number"||typeof c=="bigint")&&i!=="body"&&ni(n,""+c);break;case"className":Ql(n,"class",c);break;case"tabIndex":Ql(n,"tabindex",c);break;case"dir":case"role":case"viewBox":case"width":case"height":Ql(n,l,c);break;case"style":eS(n,c,y);break;case"data":if(i!=="object"){Ql(n,"data",c);break}case"src":case"href":if(c===""&&(i!=="a"||l!=="href")){n.removeAttribute(l);break}if(c==null||typeof c=="function"||typeof c=="symbol"||typeof c=="boolean"){n.removeAttribute(l);break}c=es(""+c),n.setAttribute(l,c);break;case"action":case"formAction":if(typeof c=="function"){n.setAttribute(l,"javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')");break}else typeof y=="function"&&(l==="formAction"?(i!=="input"&&Je(n,i,"name",m.name,m,null),Je(n,i,"formEncType",m.formEncType,m,null),Je(n,i,"formMethod",m.formMethod,m,null),Je(n,i,"formTarget",m.formTarget,m,null)):(Je(n,i,"encType",m.encType,m,null),Je(n,i,"method",m.method,m,null),Je(n,i,"target",m.target,m,null)));if(c==null||typeof c=="symbol"||typeof c=="boolean"){n.removeAttribute(l);break}c=es(""+c),n.setAttribute(l,c);break;case"onClick":c!=null&&(n.onclick=sr);break;case"onScroll":c!=null&&Ue("scroll",n);break;case"onScrollEnd":c!=null&&Ue("scrollend",n);break;case"dangerouslySetInnerHTML":if(c!=null){if(typeof c!="object"||!("__html"in c))throw Error(a(61));if(l=c.__html,l!=null){if(m.children!=null)throw Error(a(60));n.innerHTML=l}}break;case"multiple":n.multiple=c&&typeof c!="function"&&typeof c!="symbol";break;case"muted":n.muted=c&&typeof c!="function"&&typeof c!="symbol";break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"defaultValue":case"defaultChecked":case"innerHTML":case"ref":break;case"autoFocus":break;case"xlinkHref":if(c==null||typeof c=="function"||typeof c=="boolean"||typeof c=="symbol"){n.removeAttribute("xlink:href");break}l=es(""+c),n.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",l);break;case"contentEditable":case"spellCheck":case"draggable":case"value":case"autoReverse":case"externalResourcesRequired":case"focusable":case"preserveAlpha":c!=null&&typeof c!="function"&&typeof c!="symbol"?n.setAttribute(l,""+c):n.removeAttribute(l);break;case"inert":case"allowFullScreen":case"async":case"autoPlay":case"controls":case"default":case"defer":case"disabled":case"disablePictureInPicture":case"disableRemotePlayback":case"formNoValidate":case"hidden":case"loop":case"noModule":case"noValidate":case"open":case"playsInline":case"readOnly":case"required":case"reversed":case"scoped":case"seamless":case"itemScope":c&&typeof c!="function"&&typeof c!="symbol"?n.setAttribute(l,""):n.removeAttribute(l);break;case"capture":case"download":c===!0?n.setAttribute(l,""):c!==!1&&c!=null&&typeof c!="function"&&typeof c!="symbol"?n.setAttribute(l,c):n.removeAttribute(l);break;case"cols":case"rows":case"size":case"span":c!=null&&typeof c!="function"&&typeof c!="symbol"&&!isNaN(c)&&1<=c?n.setAttribute(l,c):n.removeAttribute(l);break;case"rowSpan":case"start":c==null||typeof c=="function"||typeof c=="symbol"||isNaN(c)?n.removeAttribute(l):n.setAttribute(l,c);break;case"popover":Ue("beforetoggle",n),Ue("toggle",n),Zl(n,"popover",c);break;case"xlinkActuate":lr(n,"http://www.w3.org/1999/xlink","xlink:actuate",c);break;case"xlinkArcrole":lr(n,"http://www.w3.org/1999/xlink","xlink:arcrole",c);break;case"xlinkRole":lr(n,"http://www.w3.org/1999/xlink","xlink:role",c);break;case"xlinkShow":lr(n,"http://www.w3.org/1999/xlink","xlink:show",c);break;case"xlinkTitle":lr(n,"http://www.w3.org/1999/xlink","xlink:title",c);break;case"xlinkType":lr(n,"http://www.w3.org/1999/xlink","xlink:type",c);break;case"xmlBase":lr(n,"http://www.w3.org/XML/1998/namespace","xml:base",c);break;case"xmlLang":lr(n,"http://www.w3.org/XML/1998/namespace","xml:lang",c);break;case"xmlSpace":lr(n,"http://www.w3.org/XML/1998/namespace","xml:space",c);break;case"is":Zl(n,"is",c);break;case"innerText":case"textContent":break;default:(!(2<l.length)||l[0]!=="o"&&l[0]!=="O"||l[1]!=="n"&&l[1]!=="N")&&(l=IO.get(l)||l,Zl(n,l,c))}}function Pp(n,i,l,c,m,y){switch(l){case"style":eS(n,c,y);break;case"dangerouslySetInnerHTML":if(c!=null){if(typeof c!="object"||!("__html"in c))throw Error(a(61));if(l=c.__html,l!=null){if(m.children!=null)throw Error(a(60));n.innerHTML=l}}break;case"children":typeof c=="string"?ni(n,c):(typeof c=="number"||typeof c=="bigint")&&ni(n,""+c);break;case"onScroll":c!=null&&Ue("scroll",n);break;case"onScrollEnd":c!=null&&Ue("scrollend",n);break;case"onClick":c!=null&&(n.onclick=sr);break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"inne
Line truncated
|
||||
`,f="/",h="*",b="",v="comment",E="declaration";function w(k,A){if(typeof k!="string")throw new TypeError("First argument must be a string");if(!k)return[];A=A||{};var _=1,R=1;function L(te){var Q=te.match(t);Q&&(_+=Q.length);var P=te.lastIndexOf(p);R=~P?te.length-P:R+te.length}function j(){var te={line:_,column:R};return function(Q){return Q.position=new D(te),V(),Q}}function D(te){this.start=te,this.end={line:_,column:R},this.source=A.source}D.prototype.content=k;function z(te){var Q=new Error(A.source+":"+_+":"+R+": "+te);if(Q.reason=te,Q.filename=A.source,Q.line=_,Q.column=R,Q.source=k,!A.silent)throw Q}function F(te){var Q=te.exec(k);if(Q){var P=Q[0];return L(P),k=k.slice(P.length),Q}}function V(){F(r)}function M(te){var Q;for(te=te||[];Q=H();)Q!==!1&&te.push(Q);return te}function H(){var te=j();if(!(f!=k.charAt(0)||h!=k.charAt(1))){for(var Q=2;b!=k.charAt(Q)&&(h!=k.charAt(Q)||f!=k.charAt(Q+1));)++Q;if(Q+=2,b===k.charAt(Q-1))return z("End of comment missing");var P=k.slice(2,Q-2);return R+=2,L(P),k=k.slice(Q),R+=2,te({type:v,comment:P})}}function K(){var te=j(),Q=F(a);if(Q){if(H(),!F(o))return z("property missing ':'");var P=F(s),W=te({type:E,property:T(Q[0].replace(e,b)),value:P?T(P[0].replace(e,b)):b});return F(u),W}}function Z(){var te=[];M(te);for(var Q;Q=K();)Q!==!1&&(te.push(Q),M(te));return te}return V(),Z()}function T(k){return k?k.replace(d,b):b}return mf=w,mf}var iA;function VD(){if(iA)return Di;iA=1;var e=Di&&Di.__importDefault||function(a){return a&&a.__esModule?a:{default:a}};Object.defineProperty(Di,"__esModule",{value:!0}),Di.default=r;const t=e(HD());function r(a,o){let s=null;if(!a||typeof a!="string")return s;const u=(0,t.default)(a),d=typeof o=="function";return u.forEach(p=>{if(p.type!=="declaration")return;const{property:f,value:h}=p;d?o(f,h,p):h&&(s=s||{},s[f]=h)}),s}return Di}var ll={},oA;function qD(){if(oA)return ll;oA=1,Object.defineProperty(ll,"__esModule",{value:!0}),ll.camelCase=void 0;var e=/^--[a-zA-Z0-9_-]+$/,t=/-([a-z])/g,r=/^[^-]+$/,a=/^-(webkit|moz|ms|o|khtml)-/,o=/^-(ms)-/,s=function(f){return!f||r.test(f)||e.test(f)},u=function(f,h){return h.toUpperCase()},d=function(f,h){return"".concat(h,"-")},p=function(f,h){return h===void 0&&(h={}),s(f)?f:(f=f.toLowerCase(),h.reactCompat?f=f.replace(o,d):f=f.replace(a,d),f.replace(t,u))};return ll.camelCase=p,ll}var sl,lA;function YD(){if(lA)return sl;lA=1;var e=sl&&sl.__importDefault||function(o){return o&&o.__esModule?o:{default:o}},t=e(VD()),r=qD();function a(o,s){var u={};return!o||typeof o!="string"||(0,t.default)(o,function(d,p){d&&p&&(u[(0,r.camelCase)(d,s)]=p)}),u}return a.default=a,sl=a,sl}var WD=YD();const KD=Qu(WD),M_=j_("end"),jg=j_("start");function j_(e){return t;function t(r){const a=r&&r.position&&r.position[e]||{};if(typeof a.line=="number"&&a.line>0&&typeof a.column=="number"&&a.column>0)return{line:a.line,column:a.column,offset:typeof a.offset=="number"&&a.offset>-1?a.offset:void 0}}}function XD(e){const t=jg(e),r=M_(e);if(t&&r)return{start:t,end:r}}function gl(e){return!e||typeof e!="object"?"":"position"in e||"type"in e?sA(e.position):"start"in e||"end"in e?sA(e):"line"in e||"column"in e?Kf(e):""}function Kf(e){return uA(e&&e.line)+":"+uA(e&&e.column)}function sA(e){return Kf(e&&e.start)+"-"+Kf(e&&e.end)}function uA(e){return e&&typeof e=="number"?e:1}class Ut extends Error{constructor(t,r,a){super(),typeof r=="string"&&(a=r,r=void 0);let o="",s={},u=!1;if(r&&("line"in r&&"column"in r?s={place:r}:"start"in r&&"end"in r?s={place:r}:"type"in r?s={ancestors:[r],place:r.position}:s={...r}),typeof t=="string"?o=t:!s.cause&&t&&(u=!0,o=t.message,s.cause=t),!s.ruleId&&!s.source&&typeof a=="string"){const p=a.indexOf(":");p===-1?s.ruleId=a:(s.source=a.slice(0,p),s.ruleId=a.slice(p+1))}if(!s.place&&s.ancestors&&s.ancestors){const p=s.ancestors[s.ancestors.length-1];p&&(s.place=p.position)}const d=s.place&&"start"in s.place?s.place.start:s.place;this.ancestors=s.ancestors||void 0,this.cause=s.cause||void 0,this.column=d?d.column:void 0,this.fatal=void 0,this.file="",this.message=o,this.line=d?d.line:void 0,this.name=gl(s.place)||"1:1",this.place=s.place||void 0,this.reason=this.message,this.ruleId=s.ruleId||void 0,this.source=s.source||void 0,this.stack=u&&s.cause&&typeof s.cause.stack=="string"?s.cause.stack:"",this.actual=void 0,this.expected=void 0,this.note=void 0,this.url=void 0}}Ut.prototype.file="";Ut.prototype.name="";Ut.prototype.reason="";Ut.prototype.message="";Ut.prototype.stack="";Ut.prototype.column=void 0;Ut.prototype.line=void 0;Ut.prototype.ancestors=void 0;Ut.prototype.cause=void 0;Ut.prototype.fatal=void 0;Ut.prototype.place=void 0;Ut.prototype.ruleId=void 0;Ut.prototype.source=void 0;const Ug={}.hasOwnProperty,ZD=new Map,QD=/[A-Z]/g,JD=new Set(["table","tbody","thead","tfoot","tr"]),eM=new Set(["td","th"]),U_="https://github.com/syntax-tree/hast-util-to-jsx-runtime";function tM(e,t){if(!t||t.Fragment===void 0)throw new TypeError("Expected `Fragment` in options");const r=t.filePath||void 0;let a;if(
Line truncated
|
||||
`;break}case-3:{u=`\r
|
||||
`;break}case-2:{u=t?" ":" ";break}case-1:{if(!t&&o)continue;u=" ";break}default:u=String.fromCharCode(s)}o=s===-2,a.push(u)}return a.join("")}function uj(e){const a={constructs:$_([ij,...(e||{}).extensions||[]]),content:o(TM),defined:[],document:o(kM),flow:o(H2),lazy:{},string:o(Y2),text:o(W2)};return a;function o(s){return u;function u(d){return oj(a,s,d)}}}function cj(e){for(;!Y_(e););return e}const EA=/[\0\t\n\r]/g;function dj(){let e=1,t="",r=!0,a;return o;function o(s,u,d){const p=[];let f,h,b,v,E;for(s=t+(typeof s=="string"?s.toString():new TextDecoder(u||void 0).decode(s)),b=0,t="",r&&(s.charCodeAt(0)===65279&&b++,r=void 0);b<s.length;){if(EA.lastIndex=b,f=EA.exec(s),v=f&&f.index!==void 0?f.index:s.length,E=s.charCodeAt(v),!f){t=s.slice(b);break}if(E===10&&b===v&&a)p.push(-3),a=void 0;else switch(a&&(p.push(-5),a=void 0),b<v&&(p.push(s.slice(b,v)),e+=v-b),E){case 0:{p.push(65533),e++;break}case 9:{for(h=Math.ceil(e/4)*4,p.push(-2);e++<h;)p.push(-1);break}case 10:{p.push(-4),e=1;break}default:a=!0,e=1}b=v+1}return d&&(a&&p.push(-5),t&&p.push(t),p.push(null)),p}}const pj=/\\([!-/:-@[-`{-~])|&(#(?:\d{1,7}|x[\da-f]{1,6})|[\da-z]{1,31});/gi;function fj(e){return e.replace(pj,gj)}function gj(e,t,r){if(t)return t;if(r.charCodeAt(0)===35){const o=r.charCodeAt(1),s=o===120||o===88;return G_(r.slice(s?2:1),s?16:10)}return xl(r)||e}const J_={}.hasOwnProperty;function mj(e,t,r){return t&&typeof t=="object"&&(r=t,t=void 0),hj(r)(cj(uj(r).document().write(dj()(e,t,!0))))}function hj(e){const t={transforms:[],canContainEols:["emphasis","fragment","heading","paragraph","strong"],enter:{autolink:s(Wa),autolinkProtocol:V,autolinkEmail:V,atxHeading:s(qn),blockQuote:s(Re),characterEscape:V,characterReference:V,codeFenced:s(Oe),codeFencedFenceInfo:u,codeFencedFenceMeta:u,codeIndented:s(Oe,u),codeText:s(qe,u),codeTextData:V,data:V,codeFlowValue:V,definition:s(dt),definitionDestinationString:u,definitionLabelString:u,definitionTitleString:u,emphasis:s(st),hardBreakEscape:s(Yn),hardBreakTrailing:s(Yn),htmlFlow:s(Cr,u),htmlFlowData:V,htmlText:s(Cr,u),htmlTextData:V,image:s(ql),label:u,link:s(Wa),listItem:s(po),listItemValue:v,listOrdered:s(Ka,b),listUnordered:s(Ka),paragraph:s(Hc),reference:I,referenceString:u,resourceDestinationString:u,resourceTitleString:u,setextHeading:s(qn),strong:s(Vc),thematicBreak:s(qc)},exit:{atxHeading:p(),atxHeadingSequence:j,autolink:p(),autolinkEmail:me,autolinkProtocol:we,blockQuote:p(),characterEscapeValue:M,characterReferenceMarkerHexadecimal:fe,characterReferenceMarkerNumeric:fe,characterReferenceValue:de,characterReference:le,codeFenced:p(k),codeFencedFence:T,codeFencedFenceInfo:E,codeFencedFenceMeta:w,codeFlowValue:M,codeIndented:p(A),codeText:p(Q),codeTextData:M,data:M,definition:p(),definitionDestinationString:L,definitionLabelString:_,definitionTitleString:R,emphasis:p(),hardBreakEscape:p(K),hardBreakTrailing:p(K),htmlFlow:p(Z),htmlFlowData:M,htmlText:p(te),htmlTextData:M,image:p(W),label:ce,labelText:ne,lineEnding:H,link:p(P),listItem:p(),listOrdered:p(),listUnordered:p(),paragraph:p(),referenceString:ue,resourceDestinationString:C,resourceTitleString:U,resource:J,setextHeading:p(F),setextHeadingLineSequence:z,setextHeadingText:D,strong:p(),thematicBreak:p()}};eN(t,(e||{}).mdastExtensions||[]);const r={};return a;function a(ae){let pe={type:"root",children:[]};const ke={stack:[pe],tokenStack:[],config:t,enter:d,exit:f,buffer:u,resume:h,data:r},Me=[];let We=-1;for(;++We<ae.length;)if(ae[We][1].type==="listOrdered"||ae[We][1].type==="listUnordered")if(ae[We][0]==="enter")Me.push(We);else{const nn=Me.pop();We=o(ae,nn,We)}for(We=-1;++We<ae.length;){const nn=t[ae[We][0]];J_.call(nn,ae[We][1].type)&&nn[ae[We][1].type].call(Object.assign({sliceSerialize:ae[We][2].sliceSerialize},ke),ae[We][1])}if(ke.tokenStack.length>0){const nn=ke.tokenStack[ke.tokenStack.length-1];(nn[1]||vA).call(ke,void 0,nn[0])}for(pe.position={start:aa(ae.length>0?ae[0][1].start:{line:1,column:1,offset:0}),end:aa(ae.length>0?ae[ae.length-2][1].end:{line:1,column:1,offset:0})},We=-1;++We<t.transforms.length;)pe=t.transforms[We](pe)||pe;return pe}function o(ae,pe,ke){let Me=pe-1,We=-1,nn=!1,or,Pt,vt,Vt;for(;++Me<=ke;){const tt=ae[Me];switch(tt[1].type){case"listUnordered":case"listOrdered":case"blockQuote":{tt[0]==="enter"?We++:We--,Vt=void 0;break}case"lineEndingBlank":{tt[0]==="enter"&&(or&&!Vt&&!We&&!vt&&(vt=Me),Vt=void 0);break}case"linePrefix":case"listItemValue":case"listItemMarker":case"listItemPrefix":case"listItemPrefixWhitespace":break;default:Vt=void 0}if(!We&&tt[0]==="enter"&&tt[1].type==="listItemPrefix"||We===-1&&tt[0]==="exit"&&(tt[1].type==="listUnordered"||tt[1].type==="listOrdered")){if(or){let Or=Me;for(Pt=void 0;Or--;){const In=ae[Or];if(In[1].type==="lineEnding"||In[1].type==="lineEndingBlank"){if(In[0]==="exit")continue;Pt&&(ae[Pt][1].type="lineEndingBlank",nn=!0),In[1].type="lineEnding",Pt=Or}else if(!(In[1].type==="linePrefix"||In[1].type==="blockQuotePrefix"||In[1].type==="blockQuotePrefixWhitespLine truncated
|
||||
`}]}function Sj(e,t){const r=t.value?t.value+`
|
||||
`:"",a={},o=t.lang?t.lang.split(/\s+/):[];o.length>0&&(a.className=["language-"+o[0]]);let s={type:"element",tagName:"code",properties:a,children:[{type:"text",value:r}]};return t.meta&&(s.data={meta:t.meta}),e.patch(t,s),s=e.applyData(t,s),s={type:"element",tagName:"pre",properties:{},children:[s]},e.patch(t,s),s}function xj(e,t){const r={type:"element",tagName:"del",properties:{},children:e.all(t)};return e.patch(t,r),e.applyData(t,r)}function wj(e,t){const r={type:"element",tagName:"em",properties:{},children:e.all(t)};return e.patch(t,r),e.applyData(t,r)}function Tj(e,t){const r=typeof e.options.clobberPrefix=="string"?e.options.clobberPrefix:"user-content-",a=String(t.identifier).toUpperCase(),o=Ji(a.toLowerCase()),s=e.footnoteOrder.indexOf(a);let u,d=e.footnoteCounts.get(a);d===void 0?(d=0,e.footnoteOrder.push(a),u=e.footnoteOrder.length):u=s+1,d+=1,e.footnoteCounts.set(a,d);const p={type:"element",tagName:"a",properties:{href:"#"+r+"fn-"+o,id:r+"fnref-"+o+(d>1?"-"+d:""),dataFootnoteRef:!0,ariaDescribedBy:["footnote-label"]},children:[{type:"text",value:String(u)}]};e.patch(t,p);const f={type:"element",tagName:"sup",properties:{},children:[p]};return e.patch(t,f),e.applyData(t,f)}function Aj(e,t){const r={type:"element",tagName:"h"+t.depth,properties:{},children:e.all(t)};return e.patch(t,r),e.applyData(t,r)}function kj(e,t){if(e.options.allowDangerousHtml){const r={type:"raw",value:t.value};return e.patch(t,r),e.applyData(t,r)}}function tN(e,t){const r=t.referenceType;let a="]";if(r==="collapsed"?a+="[]":r==="full"&&(a+="["+(t.label||t.identifier)+"]"),t.type==="imageReference")return[{type:"text",value:"!["+t.alt+a}];const o=e.all(t),s=o[0];s&&s.type==="text"?s.value="["+s.value:o.unshift({type:"text",value:"["});const u=o[o.length-1];return u&&u.type==="text"?u.value+=a:o.push({type:"text",value:a}),o}function _j(e,t){const r=String(t.identifier).toUpperCase(),a=e.definitionById.get(r);if(!a)return tN(e,t);const o={src:Ji(a.url||""),alt:t.alt};a.title!==null&&a.title!==void 0&&(o.title=a.title);const s={type:"element",tagName:"img",properties:o,children:[]};return e.patch(t,s),e.applyData(t,s)}function Nj(e,t){const r={src:Ji(t.url)};t.alt!==null&&t.alt!==void 0&&(r.alt=t.alt),t.title!==null&&t.title!==void 0&&(r.title=t.title);const a={type:"element",tagName:"img",properties:r,children:[]};return e.patch(t,a),e.applyData(t,a)}function Rj(e,t){const r={type:"text",value:t.value.replace(/\r?\n|\r/g," ")};e.patch(t,r);const a={type:"element",tagName:"code",properties:{},children:[r]};return e.patch(t,a),e.applyData(t,a)}function Cj(e,t){const r=String(t.identifier).toUpperCase(),a=e.definitionById.get(r);if(!a)return tN(e,t);const o={href:Ji(a.url||"")};a.title!==null&&a.title!==void 0&&(o.title=a.title);const s={type:"element",tagName:"a",properties:o,children:e.all(t)};return e.patch(t,s),e.applyData(t,s)}function Oj(e,t){const r={href:Ji(t.url)};t.title!==null&&t.title!==void 0&&(r.title=t.title);const a={type:"element",tagName:"a",properties:r,children:e.all(t)};return e.patch(t,a),e.applyData(t,a)}function Ij(e,t,r){const a=e.all(t),o=r?Lj(r):nN(t),s={},u=[];if(typeof t.checked=="boolean"){const h=a[0];let b;h&&h.type==="element"&&h.tagName==="p"?b=h:(b={type:"element",tagName:"p",properties:{},children:[]},a.unshift(b)),b.children.length>0&&b.children.unshift({type:"text",value:" "}),b.children.unshift({type:"element",tagName:"input",properties:{type:"checkbox",checked:t.checked,disabled:!0},children:[]}),s.className=["task-list-item"]}let d=-1;for(;++d<a.length;){const h=a[d];(o||d!==0||h.type!=="element"||h.tagName!=="p")&&u.push({type:"text",value:`
|
||||
`}),h.type==="element"&&h.tagName==="p"&&!o?u.push(...h.children):u.push(h)}const p=a[a.length-1];p&&(o||p.type!=="element"||p.tagName!=="p")&&u.push({type:"text",value:`
|
||||
`});const f={type:"element",tagName:"li",properties:s,children:u};return e.patch(t,f),e.applyData(t,f)}function Lj(e){let t=!1;if(e.type==="list"){t=e.spread||!1;const r=e.children;let a=-1;for(;!t&&++a<r.length;)t=nN(r[a])}return t}function nN(e){const t=e.spread;return t??e.children.length>1}function Dj(e,t){const r={},a=e.all(t);let o=-1;for(typeof t.start=="number"&&t.start!==1&&(r.start=t.start);++o<a.length;){const u=a[o];if(u.type==="element"&&u.tagName==="li"&&u.properties&&Array.isArray(u.properties.className)&&u.properties.className.includes("task-list-item")){r.className=["contains-task-list"];break}}const s={type:"element",tagName:t.ordered?"ol":"ul",properties:r,children:e.wrap(a,!0)};return e.patch(t,s),e.applyData(t,s)}function Mj(e,t){const r={type:"element",tagName:"p",properties:{},children:e.all(t)};return e.patch(t,r),e.applyData(t,r)}function jj(e,t){const r={type:"root",children:e.wrap(e.all(t))};return e.patch(t,r),e.applyData(t,r)}function Uj(e,t){const r={type:"element",tagName:"strong",properties:{},children:e.all(t)};return e.patch(t,r),e.applyData(t,r)}function Fj(e,t){const r=e.all(t),a=r.shift(),o=[];if(a){const u={type:"element",tagName:"thead",properties:{},children:e.wrap([a],!0)};e.patch(t.children[0],u),o.push(u)}if(r.length>0){const u={type:"element",tagName:"tbody",properties:{},children:e.wrap(r,!0)},d=jg(t.children[1]),p=M_(t.children[t.children.length-1]);d&&p&&(u.position={start:d,end:p}),o.push(u)}const s={type:"element",tagName:"table",properties:{},children:e.wrap(o,!0)};return e.patch(t,s),e.applyData(t,s)}function Bj(e,t,r){const a=r?r.children:void 0,s=(a?a.indexOf(t):1)===0?"th":"td",u=r&&r.type==="table"?r.align:void 0,d=u?u.length:t.children.length;let p=-1;const f=[];for(;++p<d;){const b=t.children[p],v={},E=u?u[p]:void 0;E&&(v.align=E);let w={type:"element",tagName:s,properties:v,children:[]};b&&(w.children=e.all(b),e.patch(b,w),w=e.applyData(b,w)),f.push(w)}const h={type:"element",tagName:"tr",properties:{},children:e.wrap(f,!0)};return e.patch(t,h),e.applyData(t,h)}function zj(e,t){const r={type:"element",tagName:"td",properties:{},children:e.all(t)};return e.patch(t,r),e.applyData(t,r)}const SA=9,xA=32;function Pj(e){const t=String(e),r=/\r?\n|\r/g;let a=r.exec(t),o=0;const s=[];for(;a;)s.push(wA(t.slice(o,a.index),o>0,!0),a[0]),o=a.index+a[0].length,a=r.exec(t);return s.push(wA(t.slice(o),o>0,!1)),s.join("")}function wA(e,t,r){let a=0,o=e.length;if(t){let s=e.codePointAt(a);for(;s===SA||s===xA;)a++,s=e.codePointAt(a)}if(r){let s=e.codePointAt(o-1);for(;s===SA||s===xA;)o--,s=e.codePointAt(o-1)}return o>a?e.slice(a,o):""}function $j(e,t){const r={type:"text",value:Pj(String(t.value))};return e.patch(t,r),e.applyData(t,r)}function Gj(e,t){const r={type:"element",tagName:"hr",properties:{},children:[]};return e.patch(t,r),e.applyData(t,r)}const Hj={blockquote:Ej,break:vj,code:Sj,delete:xj,emphasis:wj,footnoteReference:Tj,heading:Aj,html:kj,imageReference:_j,image:Nj,inlineCode:Rj,linkReference:Cj,link:Oj,listItem:Ij,list:Dj,paragraph:Mj,root:jj,strong:Uj,table:Fj,tableCell:zj,tableRow:Bj,text:$j,thematicBreak:Gj,toml:fu,yaml:fu,definition:fu,footnoteDefinition:fu};function fu(){}const rN=-1,ic=0,hl=1,zu=2,$g=3,Gg=4,Hg=5,Vg=6,aN=7,iN=8,TA=typeof self=="object"?self:globalThis,Vj=(e,t)=>{const r=(o,s)=>(e.set(s,o),o),a=o=>{if(e.has(o))return e.get(o);const[s,u]=t[o];switch(s){case ic:case rN:return r(u,o);case hl:{const d=r([],o);for(const p of u)d.push(a(p));return d}case zu:{const d=r({},o);for(const[p,f]of u)d[a(p)]=a(f);return d}case $g:return r(new Date(u),o);case Gg:{const{source:d,flags:p}=u;return r(new RegExp(d,p),o)}case Hg:{const d=r(new Map,o);for(const[p,f]of u)d.set(a(p),a(f));return d}case Vg:{const d=r(new Set,o);for(const p of u)d.add(a(p));return d}case aN:{const{name:d,message:p}=u;return r(new TA[d](p),o)}case iN:return r(BigInt(u),o);case"BigInt":return r(Object(BigInt(u)),o);case"ArrayBuffer":return r(new Uint8Array(u).buffer,u);case"DataView":{const{buffer:d}=new Uint8Array(u);return r(new DataView(d),u)}}return r(new TA[s](u),o)};return a},AA=e=>Vj(new Map,e)(0),Mi="",{toString:qj}={},{keys:Yj}=Object,cl=e=>{const t=typeof e;if(t!=="object"||!e)return[ic,t];const r=qj.call(e).slice(8,-1);switch(r){case"Array":return[hl,Mi];case"Object":return[zu,Mi];case"Date":return[$g,Mi];case"RegExp":return[Gg,Mi];case"Map":return[Hg,Mi];case"Set":return[Vg,Mi];case"DataView":return[hl,r]}return r.includes("Array")?[hl,r]:r.includes("Error")?[aN,r]:[zu,r]},gu=([e,t])=>e===ic&&(t==="function"||t==="symbol"),Wj=(e,t,r,a)=>{const o=(u,d)=>{const p=a.push(u)-1;return r.set(d,p),p},s=u=>{if(r.has(u))return r.get(u);let[d,p]=cl(u);switch(d){case ic:{let h=u;switch(p){case"bigint":d=iN,h=u.toString();break;case"function":case"symbol":if(e)throw new TypeError("unable to serialize "+p);h=null;break;case"undefined":return o([rN],u)}return o([d,h],u)}case hl:{if(p){let v=u;return p==="DataView"?v=new Uint8Array(u.buffer):p==="ArrayBuffer"&&(v=new Uint8Array(
Line truncated
|
||||
`},{type:"element",tagName:"ol",properties:{},children:e.wrap(d,!0)},{type:"text",value:`
|
||||
`}]}}const oc=(function(e){if(e==null)return tU;if(typeof e=="function")return lc(e);if(typeof e=="object")return Array.isArray(e)?Qj(e):Jj(e);if(typeof e=="string")return eU(e);throw new Error("Expected function, string, or object as test")});function Qj(e){const t=[];let r=-1;for(;++r<e.length;)t[r]=oc(e[r]);return lc(a);function a(...o){let s=-1;for(;++s<t.length;)if(t[s].apply(this,o))return!0;return!1}}function Jj(e){const t=e;return lc(r);function r(a){const o=a;let s;for(s in e)if(o[s]!==t[s])return!1;return!0}}function eU(e){return lc(t);function t(r){return r&&r.type===e}}function lc(e){return t;function t(r,a,o){return!!(nU(r)&&e.call(this,r,typeof a=="number"?a:void 0,o||void 0))}}function tU(){return!0}function nU(e){return e!==null&&typeof e=="object"&&"type"in e}const oN=[],rU=!0,Qf=!1,aU="skip";function lN(e,t,r,a){let o;typeof t=="function"&&typeof r!="function"?(a=r,r=t):o=t;const s=oc(o),u=a?-1:1;d(e,void 0,[])();function d(p,f,h){const b=p&&typeof p=="object"?p:{};if(typeof b.type=="string"){const E=typeof b.tagName=="string"?b.tagName:typeof b.name=="string"?b.name:void 0;Object.defineProperty(v,"name",{value:"node ("+(p.type+(E?"<"+E+">":""))+")"})}return v;function v(){let E=oN,w,T,k;if((!t||s(p,f,h[h.length-1]||void 0))&&(E=iU(r(p,h)),E[0]===Qf))return E;if("children"in p&&p.children){const A=p;if(A.children&&E[0]!==aU)for(T=(a?A.children.length:-1)+u,k=h.concat(A);T>-1&&T<A.children.length;){const _=A.children[T];if(w=d(_,T,k)(),w[0]===Qf)return w;T=typeof w[1]=="number"?w[1]:T+u}}return E}}}function iU(e){return Array.isArray(e)?e:typeof e=="number"?[rU,e]:e==null?oN:[e]}function qg(e,t,r,a){let o,s,u;typeof t=="function"&&typeof r!="function"?(s=void 0,u=t,o=r):(s=t,u=r,o=a),lN(e,s,d,o);function d(p,f){const h=f[f.length-1],b=h?h.children.indexOf(p):void 0;return u(p,b,h)}}const Jf={}.hasOwnProperty,oU={};function lU(e,t){const r=t||oU,a=new Map,o=new Map,s=new Map,u={...Hj,...r.handlers},d={all:f,applyData:uU,definitionById:a,footnoteById:o,footnoteCounts:s,footnoteOrder:[],handlers:u,one:p,options:r,patch:sU,wrap:dU};return qg(e,function(h){if(h.type==="definition"||h.type==="footnoteDefinition"){const b=h.type==="definition"?a:o,v=String(h.identifier).toUpperCase();b.has(v)||b.set(v,h)}}),d;function p(h,b){const v=h.type,E=d.handlers[v];if(Jf.call(d.handlers,v)&&E)return E(d,h,b);if(d.options.passThrough&&d.options.passThrough.includes(v)){if("children"in h){const{children:T,...k}=h,A=Pu(k);return A.children=d.all(h),A}return Pu(h)}return(d.options.unknownHandler||cU)(d,h,b)}function f(h){const b=[];if("children"in h){const v=h.children;let E=-1;for(;++E<v.length;){const w=d.one(v[E],h);if(w){if(E&&v[E-1].type==="break"&&(!Array.isArray(w)&&w.type==="text"&&(w.value=_A(w.value)),!Array.isArray(w)&&w.type==="element")){const T=w.children[0];T&&T.type==="text"&&(T.value=_A(T.value))}Array.isArray(w)?b.push(...w):b.push(w)}}}return b}}function sU(e,t){e.position&&(t.position=XD(e))}function uU(e,t){let r=t;if(e&&e.data){const a=e.data.hName,o=e.data.hChildren,s=e.data.hProperties;if(typeof a=="string")if(r.type==="element")r.tagName=a;else{const u="children"in r?r.children:[r];r={type:"element",tagName:a,properties:{},children:u}}r.type==="element"&&s&&Object.assign(r.properties,Pu(s)),"children"in r&&r.children&&o!==null&&o!==void 0&&(r.children=o)}return r}function cU(e,t){const r=t.data||{},a="value"in t&&!(Jf.call(r,"hProperties")||Jf.call(r,"hChildren"))?{type:"text",value:t.value}:{type:"element",tagName:"div",properties:{},children:e.all(t)};return e.patch(t,a),e.applyData(t,a)}function dU(e,t){const r=[];let a=-1;for(t&&r.push({type:"text",value:`
|
||||
`});++a<e.length;)a&&r.push({type:"text",value:`
|
||||
`}),r.push(e[a]);return t&&e.length>0&&r.push({type:"text",value:`
|
||||
`}),r}function _A(e){let t=0,r=e.charCodeAt(t);for(;r===9||r===32;)t++,r=e.charCodeAt(t);return e.slice(t)}function NA(e,t){const r=lU(e,t),a=r.one(e,void 0),o=Zj(r),s=Array.isArray(a)?{type:"root",children:a}:a||{type:"root",children:[]};return o&&s.children.push({type:"text",value:`
|
||||
`},o),s}function pU(e,t){return e&&"run"in e?async function(r,a){const o=NA(r,{file:a,...t});await e.run(o,a)}:function(r,a){return NA(r,{file:a,...e||t})}}function RA(e){if(e)throw e}var Ef,CA;function fU(){if(CA)return Ef;CA=1;var e=Object.prototype.hasOwnProperty,t=Object.prototype.toString,r=Object.defineProperty,a=Object.getOwnPropertyDescriptor,o=function(f){return typeof Array.isArray=="function"?Array.isArray(f):t.call(f)==="[object Array]"},s=function(f){if(!f||t.call(f)!=="[object Object]")return!1;var h=e.call(f,"constructor"),b=f.constructor&&f.constructor.prototype&&e.call(f.constructor.prototype,"isPrototypeOf");if(f.constructor&&!h&&!b)return!1;var v;for(v in f);return typeof v>"u"||e.call(f,v)},u=function(f,h){r&&h.name==="__proto__"?r(f,h.name,{enumerable:!0,configurable:!0,value:h.newValue,writable:!0}):f[h.name]=h.newValue},d=function(f,h){if(h==="__proto__")if(e.call(f,h)){if(a)return a(f,h).value}else return;return f[h]};return Ef=function p(){var f,h,b,v,E,w,T=arguments[0],k=1,A=arguments.length,_=!1;for(typeof T=="boolean"&&(_=T,T=arguments[1]||{},k=2),(T==null||typeof T!="object"&&typeof T!="function")&&(T={});k<A;++k)if(f=arguments[k],f!=null)for(h in f)b=d(T,h),v=d(f,h),T!==v&&(_&&v&&(s(v)||(E=o(v)))?(E?(E=!1,w=b&&o(b)?b:[]):w=b&&s(b)?b:{},u(T,{name:h,newValue:p(_,w,v)})):typeof v<"u"&&u(T,{name:h,newValue:v}));return T},Ef}var gU=fU();const vf=Qu(gU);function eg(e){if(typeof e!="object"||e===null)return!1;const t=Object.getPrototypeOf(e);return(t===null||t===Object.prototype||Object.getPrototypeOf(t)===null)&&!(Symbol.toStringTag in e)&&!(Symbol.iterator in e)}function mU(){const e=[],t={run:r,use:a};return t;function r(...o){let s=-1;const u=o.pop();if(typeof u!="function")throw new TypeError("Expected function as last argument, not "+u);d(null,...o);function d(p,...f){const h=e[++s];let b=-1;if(p){u(p);return}for(;++b<o.length;)(f[b]===null||f[b]===void 0)&&(f[b]=o[b]);o=f,h?hU(h,d)(...f):u(null,...f)}}function a(o){if(typeof o!="function")throw new TypeError("Expected `middelware` to be a function, not "+o);return e.push(o),t}}function hU(e,t){let r;return a;function a(...u){const d=e.length>u.length;let p;d&&u.push(o);try{p=e.apply(this,u)}catch(f){const h=f;if(d&&r)throw h;return o(h)}d||(p&&p.then&&typeof p.then=="function"?p.then(s,o):p instanceof Error?o(p):s(p))}function o(u,...d){r||(r=!0,t(u,...d))}function s(u){o(null,u)}}const Qn={basename:bU,dirname:yU,extname:EU,join:vU,sep:"/"};function bU(e,t){if(t!==void 0&&typeof t!="string")throw new TypeError('"ext" argument must be a string');Ol(e);let r=0,a=-1,o=e.length,s;if(t===void 0||t.length===0||t.length>e.length){for(;o--;)if(e.codePointAt(o)===47){if(s){r=o+1;break}}else a<0&&(s=!0,a=o+1);return a<0?"":e.slice(r,a)}if(t===e)return"";let u=-1,d=t.length-1;for(;o--;)if(e.codePointAt(o)===47){if(s){r=o+1;break}}else u<0&&(s=!0,u=o+1),d>-1&&(e.codePointAt(o)===t.codePointAt(d--)?d<0&&(a=o):(d=-1,a=u));return r===a?a=u:a<0&&(a=e.length),e.slice(r,a)}function yU(e){if(Ol(e),e.length===0)return".";let t=-1,r=e.length,a;for(;--r;)if(e.codePointAt(r)===47){if(a){t=r;break}}else a||(a=!0);return t<0?e.codePointAt(0)===47?"/":".":t===1&&e.codePointAt(0)===47?"//":e.slice(0,t)}function EU(e){Ol(e);let t=e.length,r=-1,a=0,o=-1,s=0,u;for(;t--;){const d=e.codePointAt(t);if(d===47){if(u){a=t+1;break}continue}r<0&&(u=!0,r=t+1),d===46?o<0?o=t:s!==1&&(s=1):o>-1&&(s=-1)}return o<0||r<0||s===0||s===1&&o===r-1&&o===a+1?"":e.slice(o,r)}function vU(...e){let t=-1,r;for(;++t<e.length;)Ol(e[t]),e[t]&&(r=r===void 0?e[t]:r+"/"+e[t]);return r===void 0?".":SU(r)}function SU(e){Ol(e);const t=e.codePointAt(0)===47;let r=xU(e,!t);return r.length===0&&!t&&(r="."),r.length>0&&e.codePointAt(e.length-1)===47&&(r+="/"),t?"/"+r:r}function xU(e,t){let r="",a=0,o=-1,s=0,u=-1,d,p;for(;++u<=e.length;){if(u<e.length)d=e.codePointAt(u);else{if(d===47)break;d=47}if(d===47){if(!(o===u-1||s===1))if(o!==u-1&&s===2){if(r.length<2||a!==2||r.codePointAt(r.length-1)!==46||r.codePointAt(r.length-2)!==46){if(r.length>2){if(p=r.lastIndexOf("/"),p!==r.length-1){p<0?(r="",a=0):(r=r.slice(0,p),a=r.length-1-r.lastIndexOf("/")),o=u,s=0;continue}}else if(r.length>0){r="",a=0,o=u,s=0;continue}}t&&(r=r.length>0?r+"/..":"..",a=2)}else r.length>0?r+="/"+e.slice(o+1,u):r=e.slice(o+1,u),a=u-o-1;o=u,s=0}else d===46&&s>-1?s++:s=-1}return r}function Ol(e){if(typeof e!="string")throw new TypeError("Path must be a string. Received "+JSON.stringify(e))}const wU={cwd:TU};function TU(){return"/"}function tg(e){return!!(e!==null&&typeof e=="object"&&"href"in e&&e.href&&"protocol"in e&&e.protocol&&e.auth===void 0)}function AU(e){if(typeof e=="string")e=new URL(e);else if(!tg(e)){const t=new TypeError('The "path" argument must be of type string or an instance of URL. Received `'+e+"`");throw t.code="ERR_INVALID_ARG_TYPE",t}if(e.protocol!=="file:"){const t=new TypeError("The URL must be of scheme file");throw t.code="ERR_INVALID_URL_SCHEME",t}return kU(e)}function kU(e){if(e.hostname!==""){consLine truncated
|
||||
`:" ")+s.indentLines(s.containerFlow(a,d.current()),t?pN:mF))),f(),p}}function mF(e,t,r){return t===0?e:pN(e,t,r)}function pN(e,t,r){return(r?"":" ")+e}const hF=["autolink","destinationLiteral","destinationRaw","reference","titleQuote","titleApostrophe"];fN.peek=SF;function bF(){return{canContainEols:["delete"],enter:{strikethrough:EF},exit:{strikethrough:vF}}}function yF(){return{unsafe:[{character:"~",inConstruct:"phrasing",notInConstruct:hF}],handlers:{delete:fN}}}function EF(e){this.enter({type:"delete",children:[]},e)}function vF(e){this.exit(e)}function fN(e,t,r,a){const o=r.createTracker(a),s=r.enter("strikethrough");let u=o.move("~~");return u+=r.containerPhrasing(e,{...o.current(),before:u,after:"~"}),u+=o.move("~~"),s(),u}function SF(){return"~"}function xF(e){return e.length}function wF(e,t){const r=t||{},a=(r.align||[]).concat(),o=r.stringLength||xF,s=[],u=[],d=[],p=[];let f=0,h=-1;for(;++h<e.length;){const T=[],k=[];let A=-1;for(e[h].length>f&&(f=e[h].length);++A<e[h].length;){const _=TF(e[h][A]);if(r.alignDelimiters!==!1){const R=o(_);k[A]=R,(p[A]===void 0||R>p[A])&&(p[A]=R)}T.push(_)}u[h]=T,d[h]=k}let b=-1;if(typeof a=="object"&&"length"in a)for(;++b<f;)s[b]=UA(a[b]);else{const T=UA(a);for(;++b<f;)s[b]=T}b=-1;const v=[],E=[];for(;++b<f;){const T=s[b];let k="",A="";T===99?(k=":",A=":"):T===108?k=":":T===114&&(A=":");let _=r.alignDelimiters===!1?1:Math.max(1,p[b]-k.length-A.length);const R=k+"-".repeat(_)+A;r.alignDelimiters!==!1&&(_=k.length+_+A.length,_>p[b]&&(p[b]=_),E[b]=_),v[b]=R}u.splice(1,0,v),d.splice(1,0,E),h=-1;const w=[];for(;++h<u.length;){const T=u[h],k=d[h];b=-1;const A=[];for(;++b<f;){const _=T[b]||"";let R="",L="";if(r.alignDelimiters!==!1){const j=p[b]-(k[b]||0),D=s[b];D===114?R=" ".repeat(j):D===99?j%2?(R=" ".repeat(j/2+.5),L=" ".repeat(j/2-.5)):(R=" ".repeat(j/2),L=R):L=" ".repeat(j)}r.delimiterStart!==!1&&!b&&A.push("|"),r.padding!==!1&&!(r.alignDelimiters===!1&&_==="")&&(r.delimiterStart!==!1||b)&&A.push(" "),r.alignDelimiters!==!1&&A.push(R),A.push(_),r.alignDelimiters!==!1&&A.push(L),r.padding!==!1&&A.push(" "),(r.delimiterEnd!==!1||b!==f-1)&&A.push("|")}w.push(r.delimiterEnd===!1?A.join("").replace(/ +$/,""):A.join(""))}return w.join(`
|
||||
`)}function TF(e){return e==null?"":String(e)}function UA(e){const t=typeof e=="string"?e.codePointAt(0):0;return t===67||t===99?99:t===76||t===108?108:t===82||t===114?114:0}function AF(e,t,r,a){const o=r.enter("blockquote"),s=r.createTracker(a);s.move("> "),s.shift(2);const u=r.indentLines(r.containerFlow(e,s.current()),kF);return o(),u}function kF(e,t,r){return">"+(r?"":" ")+e}function _F(e,t){return FA(e,t.inConstruct,!0)&&!FA(e,t.notInConstruct,!1)}function FA(e,t,r){if(typeof t=="string"&&(t=[t]),!t||t.length===0)return r;let a=-1;for(;++a<t.length;)if(e.includes(t[a]))return!0;return!1}function BA(e,t,r,a){let o=-1;for(;++o<r.unsafe.length;)if(r.unsafe[o].character===`
|
||||
`&&_F(r.stack,r.unsafe[o]))return/[ \t]/.test(a.before)?"":" ";return`\\
|
||||
`}function NF(e,t){const r=String(e);let a=r.indexOf(t),o=a,s=0,u=0;if(typeof t!="string")throw new TypeError("Expected substring");for(;a!==-1;)a===o?++s>u&&(u=s):s=1,o=a+t.length,a=r.indexOf(t,o);return u}function RF(e,t){return!!(t.options.fences===!1&&e.value&&!e.lang&&/[^ \r\n]/.test(e.value)&&!/^[\t ]*(?:[\r\n]|$)|(?:^|[\r\n])[\t ]*$/.test(e.value))}function CF(e){const t=e.options.fence||"`";if(t!=="`"&&t!=="~")throw new Error("Cannot serialize code with `"+t+"` for `options.fence`, expected `` ` `` or `~`");return t}function OF(e,t,r,a){const o=CF(r),s=e.value||"",u=o==="`"?"GraveAccent":"Tilde";if(RF(e,r)){const b=r.enter("codeIndented"),v=r.indentLines(s,IF);return b(),v}const d=r.createTracker(a),p=o.repeat(Math.max(NF(s,o)+1,3)),f=r.enter("codeFenced");let h=d.move(p);if(e.lang){const b=r.enter(`codeFencedLang${u}`);h+=d.move(r.safe(e.lang,{before:h,after:" ",encode:["`"],...d.current()})),b()}if(e.lang&&e.meta){const b=r.enter(`codeFencedMeta${u}`);h+=d.move(" "),h+=d.move(r.safe(e.meta,{before:h,after:`
|
||||
`,encode:["`"],...d.current()})),b()}return h+=d.move(`
|
||||
`),s&&(h+=d.move(s+`
|
||||
`)),h+=d.move(p),f(),h}function IF(e,t,r){return(r?"":" ")+e}function Wg(e){const t=e.options.quote||'"';if(t!=='"'&&t!=="'")throw new Error("Cannot serialize title with `"+t+"` for `options.quote`, expected `\"`, or `'`");return t}function LF(e,t,r,a){const o=Wg(r),s=o==='"'?"Quote":"Apostrophe",u=r.enter("definition");let d=r.enter("label");const p=r.createTracker(a);let f=p.move("[");return f+=p.move(r.safe(r.associationId(e),{before:f,after:"]",...p.current()})),f+=p.move("]: "),d(),!e.url||/[\0- \u007F]/.test(e.url)?(d=r.enter("destinationLiteral"),f+=p.move("<"),f+=p.move(r.safe(e.url,{before:f,after:">",...p.current()})),f+=p.move(">")):(d=r.enter("destinationRaw"),f+=p.move(r.safe(e.url,{before:f,after:e.title?" ":`
|
||||
`,...p.current()}))),d(),e.title&&(d=r.enter(`title${s}`),f+=p.move(" "+o),f+=p.move(r.safe(e.title,{before:f,after:o,...p.current()})),f+=p.move(o),d()),u(),f}function DF(e){const t=e.options.emphasis||"*";if(t!=="*"&&t!=="_")throw new Error("Cannot serialize emphasis with `"+t+"` for `options.emphasis`, expected `*`, or `_`");return t}function wl(e){return"&#x"+e.toString(16).toUpperCase()+";"}function $u(e,t,r){const a=Wi(e),o=Wi(t);return a===void 0?o===void 0?r==="_"?{inside:!0,outside:!0}:{inside:!1,outside:!1}:o===1?{inside:!0,outside:!0}:{inside:!1,outside:!0}:a===1?o===void 0?{inside:!1,outside:!1}:o===1?{inside:!0,outside:!0}:{inside:!1,outside:!1}:o===void 0?{inside:!1,outside:!1}:o===1?{inside:!0,outside:!1}:{inside:!1,outside:!1}}gN.peek=MF;function gN(e,t,r,a){const o=DF(r),s=r.enter("emphasis"),u=r.createTracker(a),d=u.move(o);let p=u.move(r.containerPhrasing(e,{after:o,before:d,...u.current()}));const f=p.charCodeAt(0),h=$u(a.before.charCodeAt(a.before.length-1),f,o);h.inside&&(p=wl(f)+p.slice(1));const b=p.charCodeAt(p.length-1),v=$u(a.after.charCodeAt(0),b,o);v.inside&&(p=p.slice(0,-1)+wl(b));const E=u.move(o);return s(),r.attentionEncodeSurroundingInfo={after:v.outside,before:h.outside},d+p+E}function MF(e,t,r){return r.options.emphasis||"*"}function jF(e,t){let r=!1;return qg(e,function(a){if("value"in a&&/\r?\n|\r/.test(a.value)||a.type==="break")return r=!0,Qf}),!!((!e.depth||e.depth<3)&&zg(e)&&(t.options.setext||r))}function UF(e,t,r,a){const o=Math.max(Math.min(6,e.depth||1),1),s=r.createTracker(a);if(jF(e,r)){const h=r.enter("headingSetext"),b=r.enter("phrasing"),v=r.containerPhrasing(e,{...s.current(),before:`
|
||||
`,after:`
|
||||
`});return b(),h(),v+`
|
||||
`+(o===1?"=":"-").repeat(v.length-(Math.max(v.lastIndexOf("\r"),v.lastIndexOf(`
|
||||
`))+1))}const u="#".repeat(o),d=r.enter("headingAtx"),p=r.enter("phrasing");s.move(u+" ");let f=r.containerPhrasing(e,{before:"# ",after:`
|
||||
`,...s.current()});return/^[\t ]/.test(f)&&(f=wl(f.charCodeAt(0))+f.slice(1)),f=f?u+" "+f:u,r.options.closeAtx&&(f+=" "+u),p(),d(),f}mN.peek=FF;function mN(e){return e.value||""}function FF(){return"<"}hN.peek=BF;function hN(e,t,r,a){const o=Wg(r),s=o==='"'?"Quote":"Apostrophe",u=r.enter("image");let d=r.enter("label");const p=r.createTracker(a);let f=p.move("![");return f+=p.move(r.safe(e.alt,{before:f,after:"]",...p.current()})),f+=p.move("]("),d(),!e.url&&e.title||/[\0- \u007F]/.test(e.url)?(d=r.enter("destinationLiteral"),f+=p.move("<"),f+=p.move(r.safe(e.url,{before:f,after:">",...p.current()})),f+=p.move(">")):(d=r.enter("destinationRaw"),f+=p.move(r.safe(e.url,{before:f,after:e.title?" ":")",...p.current()}))),d(),e.title&&(d=r.enter(`title${s}`),f+=p.move(" "+o),f+=p.move(r.safe(e.title,{before:f,after:o,...p.current()})),f+=p.move(o),d()),f+=p.move(")"),u(),f}function BF(){return"!"}bN.peek=zF;function bN(e,t,r,a){const o=e.referenceType,s=r.enter("imageReference");let u=r.enter("label");const d=r.createTracker(a);let p=d.move("![");const f=r.safe(e.alt,{before:p,after:"]",...d.current()});p+=d.move(f+"]["),u();const h=r.stack;r.stack=[],u=r.enter("reference");const b=r.safe(r.associationId(e),{before:p,after:"]",...d.current()});return u(),r.stack=h,s(),o==="full"||!f||f!==b?p+=d.move(b+"]"):o==="shortcut"?p=p.slice(0,-1):p+=d.move("]"),p}function zF(){return"!"}yN.peek=PF;function yN(e,t,r){let a=e.value||"",o="`",s=-1;for(;new RegExp("(^|[^`])"+o+"([^`]|$)").test(a);)o+="`";for(/[^ \r\n]/.test(a)&&(/^[ \r\n]/.test(a)&&/[ \r\n]$/.test(a)||/^`|`$/.test(a))&&(a=" "+a+" ");++s<r.unsafe.length;){const u=r.unsafe[s],d=r.compilePattern(u);let p;if(u.atBreak)for(;p=d.exec(a);){let f=p.index;a.charCodeAt(f)===10&&a.charCodeAt(f-1)===13&&f--,a=a.slice(0,f)+" "+a.slice(p.index+1)}}return o+a+o}function PF(){return"`"}function EN(e,t){const r=zg(e);return!!(!t.options.resourceLink&&e.url&&!e.title&&e.children&&e.children.length===1&&e.children[0].type==="text"&&(r===e.url||"mailto:"+r===e.url)&&/^[a-z][a-z+.-]+:/i.test(e.url)&&!/[\0- <>\u007F]/.test(e.url))}vN.peek=$F;function vN(e,t,r,a){const o=Wg(r),s=o==='"'?"Quote":"Apostrophe",u=r.createTracker(a);let d,p;if(EN(e,r)){const h=r.stack;r.stack=[],d=r.enter("autolink");let b=u.move("<");return b+=u.move(r.containerPhrasing(e,{before:b,after:">",...u.current()})),b+=u.move(">"),d(),r.stack=h,b}d=r.enter("link"),p=r.enter("label");let f=u.move("[");return f+=u.move(r.containerPhrasing(e,{before:f,after:"](",...u.current()})),f+=u.move("]("),p(),!e.url&&e.title||/[\0- \u007F]/.test(e.url)?(p=r.enter("destinationLiteral"),f+=u.move("<"),f+=u.move(r.safe(e.url,{before:f,after:">",...u.current()})),f+=u.move(">")):(p=r.enter("destinationRaw"),f+=u.move(r.safe(e.url,{before:f,after:e.title?" ":")",...u.current()}))),p(),e.title&&(p=r.enter(`title${s}`),f+=u.move(" "+o),f+=u.move(r.safe(e.title,{before:f,after:o,...u.current()})),f+=u.move(o),p()),f+=u.move(")"),d(),f}function $F(e,t,r){return EN(e,r)?"<":"["}SN.peek=GF;function SN(e,t,r,a){const o=e.referenceType,s=r.enter("linkReference");let u=r.enter("label");const d=r.createTracker(a);let p=d.move("[");const f=r.containerPhrasing(e,{before:p,after:"]",...d.current()});p+=d.move(f+"]["),u();const h=r.stack;r.stack=[],u=r.enter("reference");const b=r.safe(r.associationId(e),{before:p,after:"]",...d.current()});return u(),r.stack=h,s(),o==="full"||!f||f!==b?p+=d.move(b+"]"):o==="shortcut"?p=p.slice(0,-1):p+=d.move("]"),p}function GF(){return"["}function Kg(e){const t=e.options.bullet||"*";if(t!=="*"&&t!=="+"&&t!=="-")throw new Error("Cannot serialize items with `"+t+"` for `options.bullet`, expected `*`, `+`, or `-`");return t}function HF(e){const t=Kg(e),r=e.options.bulletOther;if(!r)return t==="*"?"-":"*";if(r!=="*"&&r!=="+"&&r!=="-")throw new Error("Cannot serialize items with `"+r+"` for `options.bulletOther`, expected `*`, `+`, or `-`");if(r===t)throw new Error("Expected `bullet` (`"+t+"`) and `bulletOther` (`"+r+"`) to be different");return r}function VF(e){const t=e.options.bulletOrdered||".";if(t!=="."&&t!==")")throw new Error("Cannot serialize items with `"+t+"` for `options.bulletOrdered`, expected `.` or `)`");return t}function xN(e){const t=e.options.rule||"*";if(t!=="*"&&t!=="-"&&t!=="_")throw new Error("Cannot serialize rules with `"+t+"` for `options.rule`, expected `*`, `-`, or `_`");return t}function qF(e,t,r,a){const o=r.enter("list"),s=r.bulletCurrent;let u=e.ordered?VF(r):Kg(r);const d=e.ordered?u==="."?")":".":HF(r);let p=t&&r.bulletLastUsed?u===r.bulletLastUsed:!1;if(!e.ordered){const h=e.children?e.children[0]:void 0;if((u==="*"||u==="-")&&h&&(!h.children||!h.children[0])&&r.stack[r.stack.length-1]==="list"&&r.stack[r.stack.length-2]==="listItem"&&r.stack[r.stack.length-3]==="list"&&r.stack[r.stack.length-4]==="listItem"&&r.indexStack[r.indexStack.length-1]===0&&r.indexStack[r.indexStack.length-2]===0&&r.indexStack[r.indexStack.length-3]===0&&(p=!0),xN(r)===u&&h){let b=-1;for(;++b<e.chilLine truncated
|
||||
`,inConstruct:"tableCell"},{atBreak:!0,character:"|",after:"[ :-]"},{character:"|",inConstruct:"tableCell"},{atBreak:!0,character:":",after:"-"},{atBreak:!0,character:"-",after:"[:|-]"}],handlers:{inlineCode:v,table:u,tableCell:p,tableRow:d}};function u(E,w,T,k){return f(h(E,T,k),E.align)}function d(E,w,T,k){const A=b(E,T,k),_=f([A]);return _.slice(0,_.indexOf(`
|
||||
`))}function p(E,w,T,k){const A=T.enter("tableCell"),_=T.enter("phrasing"),R=T.containerPhrasing(E,{...k,before:s,after:s});return _(),A(),R}function f(E,w){return wF(E,{align:w,alignDelimiters:a,padding:r,stringLength:o})}function h(E,w,T){const k=E.children;let A=-1;const _=[],R=w.enter("table");for(;++A<k.length;)_[A]=b(k[A],w,T);return R(),_}function b(E,w,T){const k=E.children;let A=-1;const _=[],R=w.enter("tableRow");for(;++A<k.length;)_[A]=p(k[A],E,w,T);return R(),_}function v(E,w,T){let k=TN.inlineCode(E,w,T);return T.stack.includes("tableCell")&&(k=k.replace(/\|/g,"\\$&")),k}}function cB(){return{exit:{taskListCheckValueChecked:PA,taskListCheckValueUnchecked:PA,paragraph:pB}}}function dB(){return{unsafe:[{atBreak:!0,character:"-",after:"[:|-]"}],handlers:{listItem:fB}}}function PA(e){const t=this.stack[this.stack.length-2];t.type,t.checked=e.type==="taskListCheckValueChecked"}function pB(e){const t=this.stack[this.stack.length-2];if(t&&t.type==="listItem"&&typeof t.checked=="boolean"){const r=this.stack[this.stack.length-1];r.type;const a=r.children[0];if(a&&a.type==="text"){const o=t.children;let s=-1,u;for(;++s<o.length;){const d=o[s];if(d.type==="paragraph"){u=d;break}}u===r&&(a.value=a.value.slice(1),a.value.length===0?r.children.shift():r.position&&a.position&&typeof a.position.start.offset=="number"&&(a.position.start.column++,a.position.start.offset++,r.position.start=Object.assign({},a.position.start)))}}this.exit(e)}function fB(e,t,r,a){const o=e.children[0],s=typeof e.checked=="boolean"&&o&&o.type==="paragraph",u="["+(e.checked?"x":" ")+"] ",d=r.createTracker(a);s&&d.move(u);let p=TN.listItem(e,t,r,{...a,...d.current()});return s&&(p=p.replace(/^(?:[*+-]|\d+\.)([\r\n]| {1,3})/,f)),p;function f(h){return h+u}}function gB(){return[qU(),fF(),bF(),rB(),cB()]}function mB(e){return{extensions:[YU(),gF(e),yF(),uB(e),dB()]}}const hB={tokenize:xB,partial:!0},AN={tokenize:wB,partial:!0},kN={tokenize:TB,partial:!0},_N={tokenize:AB,partial:!0},bB={tokenize:kB,partial:!0},NN={name:"wwwAutolink",tokenize:vB,previous:CN},RN={name:"protocolAutolink",tokenize:SB,previous:ON},Nr={name:"emailAutolink",tokenize:EB,previous:IN},rr={};function yB(){return{text:rr}}let Da=48;for(;Da<123;)rr[Da]=Nr,Da++,Da===58?Da=65:Da===91&&(Da=97);rr[43]=Nr;rr[45]=Nr;rr[46]=Nr;rr[95]=Nr;rr[72]=[Nr,RN];rr[104]=[Nr,RN];rr[87]=[Nr,NN];rr[119]=[Nr,NN];function EB(e,t,r){const a=this;let o,s;return u;function u(b){return!ng(b)||!IN.call(a,a.previous)||Xg(a.events)?r(b):(e.enter("literalAutolink"),e.enter("literalAutolinkEmail"),d(b))}function d(b){return ng(b)?(e.consume(b),d):b===64?(e.consume(b),p):r(b)}function p(b){return b===46?e.check(bB,h,f)(b):b===45||b===95||Dt(b)?(s=!0,e.consume(b),p):h(b)}function f(b){return e.consume(b),o=!0,p}function h(b){return s&&o&&Ht(a.previous)?(e.exit("literalAutolinkEmail"),e.exit("literalAutolink"),t(b)):r(b)}}function vB(e,t,r){const a=this;return o;function o(u){return u!==87&&u!==119||!CN.call(a,a.previous)||Xg(a.events)?r(u):(e.enter("literalAutolink"),e.enter("literalAutolinkWww"),e.check(hB,e.attempt(AN,e.attempt(kN,s),r),r)(u))}function s(u){return e.exit("literalAutolinkWww"),e.exit("literalAutolink"),t(u)}}function SB(e,t,r){const a=this;let o="",s=!1;return u;function u(b){return(b===72||b===104)&&ON.call(a,a.previous)&&!Xg(a.events)?(e.enter("literalAutolink"),e.enter("literalAutolinkHttp"),o+=String.fromCodePoint(b),e.consume(b),d):r(b)}function d(b){if(Ht(b)&&o.length<5)return o+=String.fromCodePoint(b),e.consume(b),d;if(b===58){const v=o.toLowerCase();if(v==="http"||v==="https")return e.consume(b),p}return r(b)}function p(b){return b===47?(e.consume(b),s?f:(s=!0,p)):r(b)}function f(b){return b===null||Bu(b)||rt(b)||Fa(b)||rc(b)?r(b):e.attempt(AN,e.attempt(kN,h),r)(b)}function h(b){return e.exit("literalAutolinkHttp"),e.exit("literalAutolink"),t(b)}}function xB(e,t,r){let a=0;return o;function o(u){return(u===87||u===119)&&a<3?(a++,e.consume(u),o):u===46&&a===3?(e.consume(u),s):r(u)}function s(u){return u===null?r(u):t(u)}}function wB(e,t,r){let a,o,s;return u;function u(f){return f===46||f===95?e.check(_N,p,d)(f):f===null||rt(f)||Fa(f)||f!==45&&rc(f)?p(f):(s=!0,e.consume(f),u)}function d(f){return f===95?a=!0:(o=a,a=void 0),e.consume(f),u}function p(f){return o||a||!s?r(f):t(f)}}function TB(e,t){let r=0,a=0;return o;function o(u){return u===40?(r++,e.consume(u),o):u===41&&a<r?s(u):u===33||u===34||u===38||u===39||u===41||u===42||u===44||u===46||u===58||u===59||u===60||u===63||u===93||u===95||u===126?e.check(_N,t,s)(u):u===null||rt(u)||Fa(u)?t(u):(e.consume(u),o)}function s(u){return u===41&&a++,e.consume(u),o}}function AB(e,t,r){return a;function a(d){return d===33||d===34||d===39||d===41||d===42||d===44||d===46||d===58||d===59||d===63||d===95||d===126?(e.consume(d),a):d===38?(e.consume(d),s):d===93?(e.consume(d),o):d===60||d===null||rt(d)||Fa(d)?t(d):r(d)}function o(d){return d===null||d===40||d===91||rt(d)||Fa(d)?t(d):a(d)}function s(d){return Ht(d)?u(d):r(d)}function u(d){re
Line truncated
|
||||
`)===-1?g.jsx("code",{className:"bg-primary/10 text-primary px-1.5 py-0.5 rounded text-sm font-mono",children:h}):g.jsx("code",{className:"block font-mono",children:h}),pre:({children:f})=>g.jsx("pre",{className:"bg-muted p-4 rounded-lg text-sm overflow-auto my-4 font-mono whitespace-pre",children:f}),table:({children:f})=>g.jsx("div",{className:"overflow-auto my-4",children:g.jsx("table",{className:"w-full border-collapse text-sm",children:f})}),thead:({children:f})=>g.jsx("thead",{className:"bg-muted/50",children:f}),tr:({children:f})=>g.jsx("tr",{className:"border-b",children:f}),th:({children:f})=>g.jsx("th",{className:"px-4 py-2 text-left font-medium",children:f}),td:({children:f})=>g.jsx("td",{className:"px-4 py-2 text-muted-foreground",children:f}),hr:()=>g.jsx("hr",{className:"my-6 border-border"}),strong:({children:f})=>g.jsx("strong",{className:"font-semibold text-foreground",children:f}),em:({children:f})=>g.jsx("em",{children:f})};return r?g.jsx("main",{id:"main-content",className:"flex-1 p-8 max-w-3xl",children:s?g.jsx("div",{className:"text-muted-foreground",children:"Loading content..."}):a?g.jsx("article",{className:"prose max-w-none",children:g.jsx(uN,{remarkPlugins:[LN],components:p,children:a})}):g.jsx("div",{className:"bg-muted/50 border rounded-lg p-6",children:g.jsxs("p",{className:"text-muted-foreground",children:["Content for ",g.jsx("strong",{className:"text-primary",children:r.title})," would be displayed here."]})})}):g.jsxs("main",{className:"flex-1 p-8 max-w-3xl",children:[g.jsx("h1",{className:"text-4xl font-bold tracking-tight mb-4",children:e.site?.title||"Documentation"}),g.jsx("p",{className:"text-muted-foreground text-lg mb-8",children:e.site?.description||"Welcome to the documentation."}),g.jsx(Pi,{className:"my-8"}),g.jsx("h2",{className:"text-2xl font-semibold mb-4 text-primary",children:"Theme Configuration"}),g.jsxs("div",{className:"bg-muted/50 border rounded-lg p-4 mb-6",children:[g.jsx("p",{className:"text-sm text-muted-foreground mb-2",children:g.jsx("strong",{children:"Current theme from YAML:"})}),g.jsx("pre",{className:"text-primary text-sm",children:`site:
|
||||
theme:
|
||||
preset: "${d?.preset||"zinc"}"
|
||||
radius: "${d?.radius||"md"}"
|
||||
darkMode: ${d?.darkMode!==!1}`})]}),g.jsx("h3",{className:"text-lg font-medium mb-3",children:"Available Presets"}),g.jsx("div",{className:"flex flex-wrap gap-2 mb-6",children:Object.keys(Y1).map(f=>g.jsx("span",{className:`px-3 py-1 rounded-full text-sm ${f===(d?.preset||"zinc")?"bg-primary text-primary-foreground":"bg-muted text-muted-foreground"}`,children:f},f))}),g.jsxs("p",{className:"text-muted-foreground mb-4",children:[g.jsx("strong",{children:"Click any item in the sidebar"})," to navigate."]}),g.jsx("h2",{className:"text-2xl font-semibold mt-8 mb-4",children:"Routes"}),g.jsxs("p",{className:"text-muted-foreground mb-4",children:[g.jsx("span",{className:"font-medium text-primary",children:t.routes?.length||0})," routes configured."]}),g.jsx("pre",{className:"bg-muted p-4 rounded-lg text-sm overflow-auto",children:JSON.stringify(t.routes?.slice(0,3),null,2)})]})}const GA=e=>typeof e=="boolean"?`${e}`:e===0?"0":e,HA=u_,Zg=(e,t)=>r=>{var a;if(t?.variants==null)return HA(e,r?.class,r?.className);const{variants:o,defaultVariants:s}=t,u=Object.keys(o).map(f=>{const h=r?.[f],b=s?.[f];if(h===null)return null;const v=GA(h)||GA(b);return o[f][v]}),d=r&&Object.entries(r).reduce((f,h)=>{let[b,v]=h;return v===void 0||(f[b]=v),f},{}),p=t==null||(a=t.compoundVariants)===null||a===void 0?void 0:a.reduce((f,h)=>{let{class:b,className:v,...E}=h;return Object.entries(E).every(w=>{let[T,k]=w;return Array.isArray(k)?k.includes({...s,...d}[T]):{...s,...d}[T]===k})?[...f,b,v]:f},[]);return HA(e,u,p,r?.class,r?.className)},DN=Zg("inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",{variants:{variant:{default:"bg-primary text-primary-foreground hover:bg-primary/90",destructive:"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",outline:"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",secondary:"bg-secondary text-secondary-foreground hover:bg-secondary/80",ghost:"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",link:"text-primary underline-offset-4 hover:underline"},size:{default:"h-9 px-4 py-2 has-[>svg]:px-3",xs:"h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",sm:"h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",lg:"h-10 rounded-md px-6 has-[>svg]:px-4",icon:"size-9","icon-xs":"size-6 rounded-md [&_svg:not([class*='size-'])]:size-3","icon-sm":"size-8","icon-lg":"size-10"}},defaultVariants:{variant:"default",size:"default"}});function $i({className:e,variant:t="default",size:r="default",asChild:a=!1,...o}){const s=a?Dg:"button";return g.jsx(s,{"data-slot":"button","data-variant":t,"data-size":r,className:be(DN({variant:t,size:r,className:e})),...o})}function _n({className:e,...t}){return g.jsx("div",{"data-slot":"card",className:be("bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",e),...t})}function Bn({className:e,...t}){return g.jsx("div",{"data-slot":"card-header",className:be("@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",e),...t})}function zn({className:e,...t}){return g.jsx("div",{"data-slot":"card-title",className:be("leading-none font-semibold",e),...t})}function Qg({className:e,...t}){return g.jsx("div",{"data-slot":"card-description",className:be("text-muted-foreground text-sm",e),...t})}function Nn({className:e,...t}){return g.jsx("div",{"data-slot":"card-content",className:be("px-6",e),...t})}function KB({className:e,...t}){return g.jsx("div",{"data-slot":"card-footer",className:be("flex items-center px-6 [.border-t]:pt-6",e),...t})}function XB({props:e}){const t=e?.title||"Statistics",r=e?.value||"0",a=e?.change||"",o=e?.changeType||"positive";return g.jsxs(_n,{className:"mb-4",children:[g.jsxs(Bn,{className:"pb-2",children:[g.jsx(Qg,{children:t}),g.jsx(zn,{className:"text-2xl",children:r})]}),a&&g.jsx(Nn,{className:"pt-0",children:g.jsx("span",{className:o==="positive"?"text-green-500 text-sm":"text-red-500 text-sm",children:a})})]})}function ZB({props:e}){const t=e?.title||"Quick Links",r=e?.links||[];return g.jsxs(_n,{className:"mb-4",children:[g.jsx(Bn,{className:"pb-2",children:g.jsx(zn,{className:"text-sm font-medium",children:t})}),g.jsx(Nn,{className:"space-y-1",children:r.map((a,o)=>g.jsx("a",{href:a.href,className:"block text-sm text-muted-foreground hover:text-foreground transition-colors",children:a.label},o))})]})}function QB({
Line truncated
|
||||
`);p=b.pop()||"";for(const v of b)if(v.startsWith("data: "))try{const E=JSON.parse(v.slice(6));Ez(e,E,o)}catch{}}}catch(s){if(s.name!=="AbortError"){const u=Ua.get(e);u&&An(e,{thinkingSteps:[...u.state.thinkingSteps,`Error: ${s.message}`]})}}finally{const s=Ua.get(e);s&&(s.abortController=null),An(e,{isStreaming:!1}),o?.onEnd?.()}}function Ez(e,t,r){const a=Ua.get(e);if(!a)return;const o=a.toolCallMap;switch(t.type){case"session":t.session_id&&typeof t.session_id=="string"&&r?.onSessionId?.(t.session_id);break;case"token":case"run_content":case"team_run_content":{const u=t.token??t.content??"";u&&An(e,{currentResponse:a.state.currentResponse+u});break}case"message":case"run_completed":case"team_run_completed":{const u=t.content;u&&An(e,{currentResponse:u});break}case"thinking":case"reasoning_step":case"team_reasoning_step":{const u=t.step;u&&An(e,{thinkingSteps:[...a.state.thinkingSteps,u]});break}case"reasoning_started":case"team_reasoning_started":An(e,{thinkingSteps:[...a.state.thinkingSteps,"Starting reasoning..."]});break;case"tool_call":case"tool_call_started":case"team_tool_call_started":{const u={name:t.name,description:t.description,icon:t.icon,step_number:t.step_number,status:"running",tool_call_id:t.tool_call_id,args:t.args};u.tool_call_id&&o.set(u.tool_call_id,u);const d=qA(a.state.toolCalls,u);An(e,{toolCalls:d});break}case"tool_call_completed":case"team_tool_call_completed":{const u=t.extra_data,d=t.a2ui??u?.a2ui,p=t.elements??u?.elements,f={name:t.name,description:t.description,icon:t.icon,step_number:t.step_number,status:"done",tool_call_id:t.tool_call_id,args:t.args,formatted_result:t.formatted_result,result:t.result,surface_id:t.surface_id??u?.surface_id,a2ui:d?.length?{messages:d,surface_id:t.surface_id??u?.surface_id}:void 0,elements:p};f.tool_call_id&&o.set(f.tool_call_id,f);const h=qA(a.state.toolCalls,f),b=p?.length?[...a.state.pendingElements,...p]:a.state.pendingElements;An(e,{toolCalls:h,pendingElements:b});break}case"message_element":{const u=t.extra_data,d=t.element??u?.element;d&&An(e,{pendingElements:[...a.state.pendingElements,d]});break}case"error":case"run_error":case"team_run_error":{const u=t.error;u&&An(e,{thinkingSteps:[...a.state.thinkingSteps,`Error: ${u}`]});break}case"end":case"done":case"run_cancelled":case"team_run_cancelled":break;default:gz(e,t);break}}function qA(e,t){if(t.tool_call_id){const r=e.findIndex(a=>a.tool_call_id===t.tool_call_id);if(r>=0){const a=[...e];return a[r]={...a[r],...t},a}}return[...e,t]}function UN(e={}){const{externalSessionId:t}=e,[r,a]=S.useState(()=>yu(t));S.useEffect(()=>{if(!t){a(yu(null));return}return a(yu(t)),mz(t,()=>{a(yu(t))})},[t]);const o=S.useCallback((u,d)=>{const p=t??`new-${Date.now()}`;bz(p,u,d,{onSessionId:f=>{e.onSessionId?.(f)},onEnd:e.onEnd})},[t,e.onSessionId,e.onEnd]),s=S.useCallback(()=>{t&&hz(t)},[t]);return{isStreaming:r.isStreaming,currentResponse:r.currentResponse,toolCalls:r.toolCalls,thinkingSteps:r.thinkingSteps,pendingElements:r.pendingElements,sendMessage:o,cancel:s}}function vz(e,t){if(e==null)return{};var r={};for(var a in e)if({}.hasOwnProperty.call(e,a)){if(t.indexOf(a)!==-1)continue;r[a]=e[a]}return r}function Sz(e,t){if(e==null)return{};var r,a,o=vz(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(a=0;a<s.length;a++)r=s[a],t.indexOf(r)===-1&&{}.propertyIsEnumerable.call(e,r)&&(o[r]=e[r])}return o}function ig(e,t){(t==null||t>e.length)&&(t=e.length);for(var r=0,a=Array(t);r<t;r++)a[r]=e[r];return a}function xz(e){if(Array.isArray(e))return ig(e)}function wz(e){if(typeof Symbol<"u"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}function Tz(e,t){if(e){if(typeof e=="string")return ig(e,t);var r={}.toString.call(e).slice(8,-1);return r==="Object"&&e.constructor&&(r=e.constructor.name),r==="Map"||r==="Set"?Array.from(e):r==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?ig(e,t):void 0}}function Az(){throw new TypeError(`Invalid attempt to spread non-iterable instance.
|
||||
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function og(e){return xz(e)||wz(e)||Tz(e)||Az()}function Tl(e){"@babel/helpers - typeof";return Tl=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Tl(e)}function kz(e,t){if(Tl(e)!="object"||!e)return e;var r=e[Symbol.toPrimitive];if(r!==void 0){var a=r.call(e,t);if(Tl(a)!="object")return a;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}function _z(e){var t=kz(e,"string");return Tl(t)=="symbol"?t:t+""}function FN(e,t,r){return(t=_z(t))in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function lg(){return lg=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var a in r)({}).hasOwnProperty.call(r,a)&&(e[a]=r[a])}return e},lg.apply(null,arguments)}function YA(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter(function(o){return Object.getOwnPropertyDescriptor(e,o).enumerable})),r.push.apply(r,a)}return r}function Gi(e){for(var t=1;t<arguments.length;t++){var r=arguments[t]!=null?arguments[t]:{};t%2?YA(Object(r),!0).forEach(function(a){FN(e,a,r[a])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(r)):YA(Object(r)).forEach(function(a){Object.defineProperty(e,a,Object.getOwnPropertyDescriptor(r,a))})}return e}function Nz(e){var t=e.length;if(t===0||t===1)return e;if(t===2)return[e[0],e[1],"".concat(e[0],".").concat(e[1]),"".concat(e[1],".").concat(e[0])];if(t===3)return[e[0],e[1],e[2],"".concat(e[0],".").concat(e[1]),"".concat(e[0],".").concat(e[2]),"".concat(e[1],".").concat(e[0]),"".concat(e[1],".").concat(e[2]),"".concat(e[2],".").concat(e[0]),"".concat(e[2],".").concat(e[1]),"".concat(e[0],".").concat(e[1],".").concat(e[2]),"".concat(e[0],".").concat(e[2],".").concat(e[1]),"".concat(e[1],".").concat(e[0],".").concat(e[2]),"".concat(e[1],".").concat(e[2],".").concat(e[0]),"".concat(e[2],".").concat(e[0],".").concat(e[1]),"".concat(e[2],".").concat(e[1],".").concat(e[0])];if(t>=4)return[e[0],e[1],e[2],e[3],"".concat(e[0],".").concat(e[1]),"".concat(e[0],".").concat(e[2]),"".concat(e[0],".").concat(e[3]),"".concat(e[1],".").concat(e[0]),"".concat(e[1],".").concat(e[2]),"".concat(e[1],".").concat(e[3]),"".concat(e[2],".").concat(e[0]),"".concat(e[2],".").concat(e[1]),"".concat(e[2],".").concat(e[3]),"".concat(e[3],".").concat(e[0]),"".concat(e[3],".").concat(e[1]),"".concat(e[3],".").concat(e[2]),"".concat(e[0],".").concat(e[1],".").concat(e[2]),"".concat(e[0],".").concat(e[1],".").concat(e[3]),"".concat(e[0],".").concat(e[2],".").concat(e[1]),"".concat(e[0],".").concat(e[2],".").concat(e[3]),"".concat(e[0],".").concat(e[3],".").concat(e[1]),"".concat(e[0],".").concat(e[3],".").concat(e[2]),"".concat(e[1],".").concat(e[0],".").concat(e[2]),"".concat(e[1],".").concat(e[0],".").concat(e[3]),"".concat(e[1],".").concat(e[2],".").concat(e[0]),"".concat(e[1],".").concat(e[2],".").concat(e[3]),"".concat(e[1],".").concat(e[3],".").concat(e[0]),"".concat(e[1],".").concat(e[3],".").concat(e[2]),"".concat(e[2],".").concat(e[0],".").concat(e[1]),"".concat(e[2],".").concat(e[0],".").concat(e[3]),"".concat(e[2],".").concat(e[1],".").concat(e[0]),"".concat(e[2],".").concat(e[1],".").concat(e[3]),"".concat(e[2],".").concat(e[3],".").concat(e[0]),"".concat(e[2],".").concat(e[3],".").concat(e[1]),"".concat(e[3],".").concat(e[0],".").concat(e[1]),"".concat(e[3],".").concat(e[0],".").concat(e[2]),"".concat(e[3],".").concat(e[1],".").concat(e[0]),"".concat(e[3],".").concat(e[1],".").concat(e[2]),"".concat(e[3],".").concat(e[2],".").concat(e[0]),"".concat(e[3],".").concat(e[2],".").concat(e[1]),"".concat(e[0],".").concat(e[1],".").concat(e[2],".").concat(e[3]),"".concat(e[0],".").concat(e[1],".").concat(e[3],".").concat(e[2]),"".concat(e[0],".").concat(e[2],".").concat(e[1],".").concat(e[3]),"".concat(e[0],".").concat(e[2],".").concat(e[3],".").concat(e[1]),"".concat(e[0],".").concat(e[3],".").concat(e[1],".").concat(e[2]),"".concat(e[0],".").concat(e[3],".").concat(e[2],".").concat(e[1]),"".concat(e[1],".").concat(e[0],".").concat(e[2],".").concat(e[3]),"".concat(e[1],".").concat(e[0],".").concat(e[3],".").concat(e[2]),"".concat(e[1],".").concat(e[2],".").concat(e[0],".").concat(e[3]),"".concat(e[1],".").concat(e[2],".").concat(e[3],".").concat(e[0]),"".concat(e[1],".").concat(e[3],".").concat(e[0],".").concat(e[2]),"".concat(e[1],".").concat(e[3],".").concat(e[2],".").concat(e[0]),"".concat(e[2],".").concat(e[0],".").concat(e[1],".").concat(e[3]),"".concat(e[2],".").concat(e[0],".").concat(e[3],".").concat(e[1]),"".concat(e[2],".").concat(e[1],".").concat(e[0],".").concat(e[3]),"".concat(e[2],".").concat(e[1],".").concat(e[3],".").concat(e[
Line truncated
|
||||
`))})}function Uz(e){var t=e.codeString,r=e.codeStyle,a=e.containerStyle,o=a===void 0?{float:"left",paddingRight:"10px"}:a,s=e.numberStyle,u=s===void 0?{}:s,d=e.startingLineNumber;return Ke.createElement("code",{style:Object.assign({},r,o)},jz({lines:t.replace(/\n$/,"").split(`
|
||||
`),style:u,startingLineNumber:d}))}function Fz(e){return"".concat(e.toString().length,".25em")}function zN(e,t){return{type:"element",tagName:"span",properties:{key:"line-number--".concat(e),className:["comment","linenumber","react-syntax-highlighter-line-number"],style:t},children:[{type:"text",value:e}]}}function PN(e,t,r){var a={display:"inline-block",minWidth:Fz(r),paddingRight:"1em",textAlign:"right",userSelect:"none"},o=typeof e=="function"?e(t):e,s=oa(oa({},a),o);return s}function Ru(e){var t=e.children,r=e.lineNumber,a=e.lineNumberStyle,o=e.largestLineNumber,s=e.showInlineLineNumbers,u=e.lineProps,d=u===void 0?{}:u,p=e.className,f=p===void 0?[]:p,h=e.showLineNumbers,b=e.wrapLongLines,v=e.wrapLines,E=v===void 0?!1:v,w=E?oa({},typeof d=="function"?d(r):d):{};if(w.className=w.className?[].concat(og(w.className.trim().split(/\s+/)),og(f)):f,r&&s){var T=PN(a,r,o);t.unshift(zN(r,T))}return b&h&&(w.style=oa({display:"flex"},w.style)),{type:"element",tagName:"span",properties:w,children:t}}function $N(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:[],r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:[];e.length===void 0&&(e=[e]);for(var a=0;a<e.length;a++){var o=e[a];if(o.type==="text")r.push(Ru({children:[o],className:og(new Set(t))}));else if(o.children){var s,u=t.concat(((s=o.properties)===null||s===void 0?void 0:s.className)||[]);$N(o.children,u).forEach(function(d){return r.push(d)})}}return r}function Bz(e,t,r,a,o,s,u,d,p){var f,h=$N(e.value),b=[],v=-1,E=0;function w(j,D){var z=arguments.length>2&&arguments[2]!==void 0?arguments[2]:[];return Ru({children:j,lineNumber:D,lineNumberStyle:d,largestLineNumber:u,showInlineLineNumbers:o,lineProps:r,className:z,showLineNumbers:a,wrapLongLines:p,wrapLines:t})}function T(j,D){if(a&&D&&o){var z=PN(d,D,u);j.unshift(zN(D,z))}return j}function k(j,D){var z=arguments.length>2&&arguments[2]!==void 0?arguments[2]:[];return t||z.length>0?w(j,D,z):T(j,D)}for(var A=function(){var D=h[E],z=D.children[0].value,F=Mz(z);if(F){var V=z.split(`
|
||||
`);V.forEach(function(M,H){var K=a&&b.length+s,Z={type:"text",value:"".concat(M,`
|
||||
`)};if(H===0){var te=h.slice(v+1,E).concat(Ru({children:[Z],className:D.properties.className})),Q=k(te,K);b.push(Q)}else if(H===V.length-1){var P=h[E+1]&&h[E+1].children&&h[E+1].children[0],W={type:"text",value:"".concat(M)};if(P){var ne=Ru({children:[W],className:D.properties.className});h.splice(E+1,0,ne)}else{var ce=[W],C=k(ce,K,D.properties.className);b.push(C)}}else{var U=[Z],J=k(U,K,D.properties.className);b.push(J)}}),v=E}E++};E<h.length;)A();if(v!==h.length-1){var _=h.slice(v+1,h.length);if(_&&_.length){var R=a&&b.length+s,L=k(_,R);b.push(L)}}return t?b:(f=[]).concat.apply(f,b)}function zz(e){var t=e.rows,r=e.stylesheet,a=e.useInlineStyles;return t.map(function(o,s){return BN({node:o,stylesheet:r,useInlineStyles:a,key:"code-segment-".concat(s)})})}function GN(e){return e&&typeof e.highlightAuto<"u"}function Pz(e){var t=e.astGenerator,r=e.language,a=e.code,o=e.defaultCodeValue;if(GN(t)){var s=Iz(t,r);return r==="text"?{value:o,language:"text"}:s?t.highlight(r,a):t.highlightAuto(a)}try{return r&&r!=="text"?{value:t.highlight(a,r)}:{value:o}}catch{return{value:o}}}function $z(e,t){return function(a){var o,s,u=a.language,d=a.children,p=a.style,f=p===void 0?t:p,h=a.customStyle,b=h===void 0?{}:h,v=a.codeTagProps,E=v===void 0?{className:u?"language-".concat(u):void 0,style:oa(oa({},f['code[class*="language-"]']),f['code[class*="language-'.concat(u,'"]')])}:v,w=a.useInlineStyles,T=w===void 0?!0:w,k=a.showLineNumbers,A=k===void 0?!1:k,_=a.showInlineLineNumbers,R=_===void 0?!0:_,L=a.startingLineNumber,j=L===void 0?1:L,D=a.lineNumberContainerStyle,z=a.lineNumberStyle,F=z===void 0?{}:z,V=a.wrapLines,M=a.wrapLongLines,H=M===void 0?!1:M,K=a.lineProps,Z=K===void 0?{}:K,te=a.renderer,Q=a.PreTag,P=Q===void 0?"pre":Q,W=a.CodeTag,ne=W===void 0?"code":W,ce=a.code,C=ce===void 0?(Array.isArray(d)?d[0]:d)||"":ce,U=a.astGenerator,J=Sz(a,Lz);U=U||e;var I=A?Ke.createElement(Uz,{containerStyle:D,codeStyle:E.style||{},numberStyle:F,startingLineNumber:j,codeString:C}):null,ue=f.hljs||f['pre[class*="language-"]']||{backgroundColor:"#fff"},fe=GN(U)?"hljs":"prismjs",de=T?Object.assign({},J,{style:Object.assign({},ue,b)}):Object.assign({},J,{className:J.className?"".concat(fe," ").concat(J.className):fe,style:Object.assign({},b)});if(H?E.style=oa({whiteSpace:"pre-wrap"},E.style):E.style=oa({whiteSpace:"pre"},E.style),!U)return Ke.createElement(P,de,I,Ke.createElement(ne,E,C));(V===void 0&&te||H)&&(V=!0),te=te||zz;var le=[{type:"text",value:C}],we=Pz({astGenerator:U,language:u,code:C,defaultCodeValue:le});we.language===null&&(we.value=le);var me=(o=(s=C.match(/\n/g))===null||s===void 0?void 0:s.length)!==null&&o!==void 0?o:0,Re=j+me,Oe=Bz(we,V,Z,A,R,j,Re,F,H);return Ke.createElement(P,de,Ke.createElement(ne,E,!R&&I,te({rows:Oe,stylesheet:f,useInlineStyles:T})))}}const Gz="modulepreload",Hz=function(e){return"/"+e},XA={},ZA=function(t,r,a){let o=Promise.resolve();if(r&&r.length>0){let p=function(f){return Promise.all(f.map(h=>Promise.resolve(h).then(b=>({status:"fulfilled",value:b}),b=>({status:"rejected",reason:b}))))};document.getElementsByTagName("link");const u=document.querySelector("meta[property=csp-nonce]"),d=u?.nonce||u?.getAttribute("nonce");o=p(r.map(f=>{if(f=Hz(f),f in XA)return;XA[f]=!0;const h=f.endsWith(".css"),b=h?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${f}"]${b}`))return;const v=document.createElement("link");if(v.rel=h?"stylesheet":Gz,h||(v.as="script"),v.crossOrigin="",v.href=f,d&&v.setAttribute("nonce",d),document.head.appendChild(v),h)return new Promise((E,w)=>{v.addEventListener("load",E),v.addEventListener("error",()=>w(new Error(`Unable to preload CSS for ${f}`)))})}))}function s(u){const d=new Event("vite:preloadError",{cancelable:!0});if(d.payload=u,window.dispatchEvent(d),!d.defaultPrevented)throw u}return o.then(u=>{for(const d of u||[])d.status==="rejected"&&s(d.reason);return t().catch(s)})},Vz=["abap","abnf","actionscript","ada","agda","al","antlr4","apacheconf","apex","apl","applescript","aql","arduino","arff","armasm","arturo","asciidoc","asm6502","asmatmel","aspnet","autohotkey","autoit","avisynth","avro-idl","awk","bash","basic","batch","bbcode","bbj","bicep","birb","bison","bnf","bqn","brainfuck","brightscript","bro","bsl","c","cfscript","chaiscript","cil","cilkc","cilkcpp","clike","clojure","cmake","cobol","coffeescript","concurnas","cooklang","coq","cpp","crystal","csharp","cshtml","csp","css-extras","css","csv","cue","cypher","d","dart","dataweave","dax","dhall","diff","django","dns-zone-file","docker","dot","ebnf","editorconfig","eiffel","ejs","elixir","elm","erb","erlang","etlua","excel-formula","factor","false","firestore-security-rules","flow","fortran","fsharp","ftl","gap","gcode","gdscript","gedcom","gettext","gherkin","git","glsl","gml","gn","go-module","go","gradle","graphql","groovy","haml","handlebars","haskell","haxe","hcl","hlsl","hoon","hpkp","hsts","http","ichigojam","icon","icu-message-format","idris","iecst","ignore","inform7","ini","io","j","java","javado
Line truncated
|
||||
?|
|
||||
|(?![\\s\\S])))+`,"m"),alias:s,inside:{line:{pattern:/(.)(?=[\s\S]).*(?:\r\n?|\n)?/,lookbehind:!0},prefix:{pattern:/[\s\S]/,alias:/\w+/.exec(a)[0]}}}}),Object.defineProperty(t.languages.diff,"PREFIXES",{value:r})})(e)}Bt.displayName="markup-templating";Bt.aliases=[];function Bt(e){e.register(Ot),(function(t){function r(a,o){return"___"+a.toUpperCase()+o+"___"}Object.defineProperties(t.languages["markup-templating"]={},{buildPlaceholders:{value:function(a,o,s,u){if(a.language===o){var d=a.tokenStack=[];a.code=a.code.replace(s,function(p){if(typeof u=="function"&&!u(p))return p;for(var f=d.length,h;a.code.indexOf(h=r(o,f))!==-1;)++f;return d[f]=p,h}),a.grammar=t.languages.markup}}},tokenizePlaceholders:{value:function(a,o){if(a.language!==o||!a.tokenStack)return;a.grammar=t.languages[o];var s=0,u=Object.keys(a.tokenStack);function d(p){for(var f=0;f<p.length&&!(s>=u.length);f++){var h=p[f];if(typeof h=="string"||h.content&&typeof h.content=="string"){var b=u[s],v=a.tokenStack[b],E=typeof h=="string"?h:h.content,w=r(o,b),T=E.indexOf(w);if(T>-1){++s;var k=E.substring(0,T),A=new t.Token(o,t.tokenize(v,a.grammar),"language-"+o,v),_=E.substring(T+w.length),R=[];k&&R.push.apply(R,d([k])),R.push(A),_&&R.push.apply(R,d([_])),typeof h=="string"?p.splice.apply(p,[f,1].concat(R)):h.content=R}}else h.content&&d(h.content)}return p}d(a.tokens)}}})})(e)}uh.displayName="django";uh.aliases=["jinja2"];function uh(e){e.register(Bt),(function(t){t.languages.django={comment:/^\{#[\s\S]*?#\}$/,tag:{pattern:/(^\{%[+-]?\s*)\w+/,lookbehind:!0,alias:"keyword"},delimiter:{pattern:/^\{[{%][+-]?|[+-]?[}%]\}$/,alias:"punctuation"},string:{pattern:/("|')(?:\\.|(?!\1)[^\\\r\n])*\1/,greedy:!0},filter:{pattern:/(\|)\w+/,lookbehind:!0,alias:"function"},test:{pattern:/(\bis\s+(?:not\s+)?)(?!not\b)\w+/,lookbehind:!0,alias:"function"},function:/\b[a-z_]\w+(?=\s*\()/i,keyword:/\b(?:and|as|by|else|for|if|import|in|is|loop|not|or|recursive|with|without)\b/,operator:/[-+%=]=?|!=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,number:/\b\d+(?:\.\d+)?\b/,boolean:/[Ff]alse|[Nn]one|[Tt]rue/,variable:/\b\w+\b/,punctuation:/[{}[\](),.:;]/};var r=/\{\{[\s\S]*?\}\}|\{%[\s\S]*?%\}|\{#[\s\S]*?#\}/g,a=t.languages["markup-templating"];t.hooks.add("before-tokenize",function(o){a.buildPlaceholders(o,"django",r)}),t.hooks.add("after-tokenize",function(o){a.tokenizePlaceholders(o,"django")}),t.languages.jinja2=t.languages.django,t.hooks.add("before-tokenize",function(o){a.buildPlaceholders(o,"jinja2",r)}),t.hooks.add("after-tokenize",function(o){a.tokenizePlaceholders(o,"jinja2")})})(e)}ch.displayName="dns-zone-file";ch.aliases=["dns-zone"];function ch(e){e.languages["dns-zone-file"]={comment:/;.*/,string:{pattern:/"(?:\\.|[^"\\\r\n])*"/,greedy:!0},variable:[{pattern:/(^\$ORIGIN[ \t]+)\S+/m,lookbehind:!0},{pattern:/(^|\s)@(?=\s|$)/,lookbehind:!0}],keyword:/^\$(?:INCLUDE|ORIGIN|TTL)(?=\s|$)/m,class:{pattern:/(^|\s)(?:CH|CS|HS|IN)(?=\s|$)/,lookbehind:!0,alias:"keyword"},type:{pattern:/(^|\s)(?:A|A6|AAAA|AFSDB|APL|ATMA|CAA|CDNSKEY|CDS|CERT|CNAME|DHCID|DLV|DNAME|DNSKEY|DS|EID|GID|GPOS|HINFO|HIP|IPSECKEY|ISDN|KEY|KX|LOC|MAILA|MAILB|MB|MD|MF|MG|MINFO|MR|MX|NAPTR|NB|NBSTAT|NIMLOC|NINFO|NS|NSAP|NSAP-PTR|NSEC|NSEC3|NSEC3PARAM|NULL|NXT|OPENPGPKEY|PTR|PX|RKEY|RP|RRSIG|RT|SIG|SINK|SMIMEA|SOA|SPF|SRV|SSHFP|TA|TKEY|TLSA|TSIG|TXT|UID|UINFO|UNSPEC|URI|WKS|X25)(?=\s|$)/,lookbehind:!0,alias:"keyword"},punctuation:/[()]/},e.languages["dns-zone"]=e.languages["dns-zone-file"]}dh.displayName="docker";dh.aliases=["dockerfile"];function dh(e){(function(t){var r=/\\[\r\n](?:\s|\\[\r\n]|#.*(?!.))*(?![\s#]|\\[\r\n])/.source,a=/(?:[ \t]+(?![ \t])(?:<SP_BS>)?|<SP_BS>)/.source.replace(/<SP_BS>/g,function(){return r}),o=/"(?:[^"\\\r\n]|\\(?:\r\n|[\s\S]))*"|'(?:[^'\\\r\n]|\\(?:\r\n|[\s\S]))*'/.source,s=/--[\w-]+=(?:<STR>|(?!["'])(?:[^\s\\]|\\.)+)/.source.replace(/<STR>/g,function(){return o}),u={pattern:RegExp(o),greedy:!0},d={pattern:/(^[ \t]*)#.*/m,lookbehind:!0,greedy:!0};function p(f,h){return f=f.replace(/<OPT>/g,function(){return s}).replace(/<SP>/g,function(){return a}),RegExp(f,h)}t.languages.docker={instruction:{pattern:/(^[ \t]*)(?:ADD|ARG|CMD|COPY|ENTRYPOINT|ENV|EXPOSE|FROM|HEALTHCHECK|LABEL|MAINTAINER|ONBUILD|RUN|SHELL|STOPSIGNAL|USER|VOLUME|WORKDIR)(?=\s)(?:\\.|[^\r\n\\])*(?:\\$(?:\s|#.*$)*(?![\s#])(?:\\.|[^\r\n\\])*)*/im,lookbehind:!0,greedy:!0,inside:{options:{pattern:p(/(^(?:ONBUILD<SP>)?\w+<SP>)<OPT>(?:<SP><OPT>)*/.source,"i"),lookbehind:!0,greedy:!0,inside:{property:{pattern:/(^|\s)--[\w-]+/,lookbehind:!0},string:[u,{pattern:/(=)(?!["'])(?:[^\s\\]|\\.)+/,lookbehind:!0}],operator:/\\$/m,punctuation:/=/}},keyword:[{pattern:p(/(^(?:ONBUILD<SP>)?HEALTHCHECK<SP>(?:<OPT><SP>)*)(?:CMD|NONE)\b/.source,"i"),lookbehind:!0,greedy:!0},{pattern:p(/(^(?:ONBUILD<SP>)?FROM<SP>(?:<OPT><SP>)*(?!--)[^ \t\\]+<SP>)AS/.source,"i"),lookbehind:!0,greedy:!0},{pattern:p(/(^ONBUILD<SP>)\w+/.source,"i"),lookbehind:!0,greedy:!0},{pattern:/^\w+/,greedy:!0}],comment:d,string:u,variable:/\$(?:\w+|\{[^{}"'\\]*\})/,oper
Line truncated
|
||||
]))(?:`+(/[^\s\\]/.source+'|[ ]+(?:(?![ "])|'+a+")|"+/\\[\r\n]+(?:[#;].*[\r\n]+)*(?![#;])/.source)+")*"),lookbehind:!0,greedy:!0,alias:"attr-value",inside:{comment:r,quoted:{pattern:RegExp(/(^|\s)/.source+a),lookbehind:!0,greedy:!0},punctuation:/\\$/m,boolean:{pattern:/^(?:false|no|off|on|true|yes)$/,greedy:!0}}},punctuation:/=/}})(e)}Ul.displayName="t4-templating";Ul.aliases=[];function Ul(e){(function(t){function r(o,s,u){return{pattern:RegExp("<#"+o+"[\\s\\S]*?#>"),alias:"block",inside:{delimiter:{pattern:RegExp("^<#"+o+"|#>$"),alias:"important"},content:{pattern:/[\s\S]+/,inside:s,alias:u}}}}function a(o){var s=t.languages[o],u="language-"+o;return{block:{pattern:/<#[\s\S]+?#>/,inside:{directive:r("@",{"attr-value":{pattern:/=(?:("|')(?:\\[\s\S]|(?!\1)[^\\])*\1|[^\s'">=]+)/,inside:{punctuation:/^=|^["']|["']$/}},keyword:/\b\w+(?=\s)/,"attr-name":/\b\w+/}),expression:r("=",s,u),"class-feature":r("\\+",s,u),standard:r("",s,u)}}}}t.languages["t4-templating"]=Object.defineProperty({},"createT4",{value:a})})(e)}TE.displayName="t4-cs";TE.aliases=["t4"];function TE(e){e.register(to),e.register(Ul),e.languages.t4=e.languages["t4-cs"]=e.languages["t4-templating"].createT4("csharp")}bc.displayName="vbnet";bc.aliases=[];function bc(e){e.register(dc),e.languages.vbnet=e.languages.extend("basic",{comment:[{pattern:/(?:!|REM\b).+/i,inside:{keyword:/^REM/i}},{pattern:/(^|[^\\:])'.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(^|[^"])"(?:""|[^"])*"(?!")/,lookbehind:!0,greedy:!0},keyword:/(?:\b(?:ADDHANDLER|ADDRESSOF|ALIAS|AND|ANDALSO|AS|BEEP|BLOAD|BOOLEAN|BSAVE|BYREF|BYTE|BYVAL|CALL(?: ABSOLUTE)?|CASE|CATCH|CBOOL|CBYTE|CCHAR|CDATE|CDBL|CDEC|CHAIN|CHAR|CHDIR|CINT|CLASS|CLEAR|CLNG|CLOSE|CLS|COBJ|COM|COMMON|CONST|CONTINUE|CSBYTE|CSHORT|CSNG|CSTR|CTYPE|CUINT|CULNG|CUSHORT|DATA|DATE|DECIMAL|DECLARE|DEF(?: FN| SEG|DBL|INT|LNG|SNG|STR)|DEFAULT|DELEGATE|DIM|DIRECTCAST|DO|DOUBLE|ELSE|ELSEIF|END|ENUM|ENVIRON|ERASE|ERROR|EVENT|EXIT|FALSE|FIELD|FILES|FINALLY|FOR(?: EACH)?|FRIEND|FUNCTION|GET|GETTYPE|GETXMLNAMESPACE|GLOBAL|GOSUB|GOTO|HANDLES|IF|IMPLEMENTS|IMPORTS|IN|INHERITS|INPUT|INTEGER|INTERFACE|IOCTL|IS|ISNOT|KEY|KILL|LET|LIB|LIKE|LINE INPUT|LOCATE|LOCK|LONG|LOOP|LSET|ME|MKDIR|MOD|MODULE|MUSTINHERIT|MUSTOVERRIDE|MYBASE|MYCLASS|NAME|NAMESPACE|NARROWING|NEW|NEXT|NOT|NOTHING|NOTINHERITABLE|NOTOVERRIDABLE|OBJECT|OF|OFF|ON(?: COM| ERROR| KEY| TIMER)?|OPEN|OPERATOR|OPTION(?: BASE)?|OPTIONAL|OR|ORELSE|OUT|OVERLOADS|OVERRIDABLE|OVERRIDES|PARAMARRAY|PARTIAL|POKE|PRIVATE|PROPERTY|PROTECTED|PUBLIC|PUT|RAISEEVENT|READ|READONLY|REDIM|REM|REMOVEHANDLER|RESTORE|RESUME|RETURN|RMDIR|RSET|RUN|SBYTE|SELECT(?: CASE)?|SET|SHADOWS|SHARED|SHELL|SHORT|SINGLE|SLEEP|STATIC|STEP|STOP|STRING|STRUCTURE|SUB|SWAP|SYNCLOCK|SYSTEM|THEN|THROW|TIMER|TO|TROFF|TRON|TRUE|TRY|TRYCAST|TYPE|TYPEOF|UINTEGER|ULONG|UNLOCK|UNTIL|USHORT|USING|VIEW PRINT|WAIT|WEND|WHEN|WHILE|WIDENING|WITH|WITHEVENTS|WRITE|WRITEONLY|XOR)|\B(?:#CONST|#ELSE|#ELSEIF|#END|#IF))(?:\$|\b)/i,punctuation:/[,;:(){}]/})}AE.displayName="t4-vb";AE.aliases=[];function AE(e){e.register(Ul),e.register(bc),e.languages["t4-vb"]=e.languages["t4-templating"].createT4("vbnet")}kE.displayName="tap";kE.aliases=[];function kE(e){e.register(cc),e.languages.tap={fail:/not ok[^#{\n\r]*/,pass:/ok[^#{\n\r]*/,pragma:/pragma [+-][a-z]+/,bailout:/bail out!.*/i,version:/TAP version \d+/i,plan:/\b\d+\.\.\d+(?: +#.*)?/,subtest:{pattern:/# Subtest(?:: .*)?/,greedy:!0},punctuation:/[{}]/,directive:/#.*/,yamlish:{pattern:/(^[ \t]*)---[\s\S]*?[\r\n][ \t]*\.\.\.$/m,lookbehind:!0,inside:e.languages.yaml,alias:"language-yaml"}}}_E.displayName="tcl";_E.aliases=[];function _E(e){e.languages.tcl={comment:{pattern:/(^|[^\\])#.*/,lookbehind:!0},string:{pattern:/"(?:[^"\\\r\n]|\\(?:\r\n|[\s\S]))*"/,greedy:!0},variable:[{pattern:/(\$)(?:::)?(?:[a-zA-Z0-9]+::)*\w+/,lookbehind:!0},{pattern:/(\$)\{[^}]+\}/,lookbehind:!0},{pattern:/(^[\t ]*set[ \t]+)(?:::)?(?:[a-zA-Z0-9]+::)*\w+/m,lookbehind:!0}],function:{pattern:/(^[\t ]*proc[ \t]+)\S+/m,lookbehind:!0},builtin:[{pattern:/(^[\t ]*)(?:break|class|continue|error|eval|exit|for|foreach|if|proc|return|switch|while)\b/m,lookbehind:!0},/\b(?:else|elseif)\b/],scope:{pattern:/(^[\t ]*)(?:global|upvar|variable)\b/m,lookbehind:!0,alias:"constant"},keyword:{pattern:/(^[\t ]*|\[)(?:Safe_Base|Tcl|after|append|apply|array|auto_(?:execok|import|load|mkindex|qualify|reset)|automkindex_old|bgerror|binary|catch|cd|chan|clock|close|concat|dde|dict|encoding|eof|exec|expr|fblocked|fconfigure|fcopy|file(?:event|name)?|flush|gets|glob|history|http|incr|info|interp|join|lappend|lassign|lindex|linsert|list|llength|load|lrange|lrepeat|lreplace|lreverse|lsearch|lset|lsort|math(?:func|op)|memory|msgcat|namespace|open|package|parray|pid|pkg_mkIndex|platform|puts|pwd|re_syntax|read|refchan|regexp|registry|regsub|rename|scan|seek|set|socket|source|split|string|subst|tcl(?:_endOfWord|_findLibrary|startOf(?:Next|Previous)Word|test|vars|wordBreak(?:After|Before))|tell|time|tm|trace|unknown|unload|unset|update|uplevel|vwait
Line truncated
|
||||
- a positive number
|
||||
- less than the value passed to \`max\` (or ${dv} if no \`max\` prop is set)
|
||||
- \`null\` or \`undefined\` if the progress is indeterminate.
|
||||
|
||||
Defaulting to \`null\`.`}var b3=PR,y3=GR;function E3({className:e,value:t,...r}){return g.jsx(b3,{"data-slot":"progress",className:be("bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",e),...r,children:g.jsx(y3,{"data-slot":"progress-indicator",className:"bg-primary h-full w-full flex-1 transition-all",style:{transform:`translateX(-${100-(t||0)}%)`}})})}function Bl(e){const t=S.useRef({value:e,previous:e});return S.useMemo(()=>(t.current.value!==e&&(t.current.previous=t.current.value,t.current.value=e),t.current.previous),[e])}function zl(e){const[t,r]=S.useState(void 0);return gt(()=>{if(e){r({width:e.offsetWidth,height:e.offsetHeight});const a=new ResizeObserver(o=>{if(!Array.isArray(o)||!o.length)return;const s=o[0];let u,d;if("borderBoxSize"in s){const p=s.borderBoxSize,f=Array.isArray(p)?p[0]:p;u=f.inlineSize,d=f.blockSize}else u=e.offsetWidth,d=e.offsetHeight;r({width:u,height:d})});return a.observe(e,{box:"border-box"}),()=>a.unobserve(e)}else r(void 0)},[e]),t}var Ac="Switch",[v3]=jt(Ac),[S3,x3]=v3(Ac),VR=S.forwardRef((e,t)=>{const{__scopeSwitch:r,name:a,checked:o,defaultChecked:s,required:u,disabled:d,value:p="on",onCheckedChange:f,form:h,...b}=e,[v,E]=S.useState(null),w=_e(t,R=>E(R)),T=S.useRef(!1),k=v?h||!!v.closest("form"):!0,[A,_]=en({prop:o,defaultProp:s??!1,onChange:f,caller:Ac});return g.jsxs(S3,{scope:r,checked:A,disabled:d,children:[g.jsx(Se.button,{type:"button",role:"switch","aria-checked":A,"aria-required":u,"data-state":KR(A),"data-disabled":d?"":void 0,disabled:d,value:p,...b,ref:w,onClick:ge(e.onClick,R=>{_(L=>!L),k&&(T.current=R.isPropagationStopped(),T.current||R.stopPropagation())})}),k&&g.jsx(WR,{control:v,bubbles:!T.current,name:a,value:p,checked:A,required:u,disabled:d,form:h,style:{transform:"translateX(-100%)"}})]})});VR.displayName=Ac;var qR="SwitchThumb",YR=S.forwardRef((e,t)=>{const{__scopeSwitch:r,...a}=e,o=x3(qR,r);return g.jsx(Se.span,{"data-state":KR(o.checked),"data-disabled":o.disabled?"":void 0,...a,ref:t})});YR.displayName=qR;var w3="SwitchBubbleInput",WR=S.forwardRef(({__scopeSwitch:e,control:t,checked:r,bubbles:a=!0,...o},s)=>{const u=S.useRef(null),d=_e(u,s),p=Bl(r),f=zl(t);return S.useEffect(()=>{const h=u.current;if(!h)return;const b=window.HTMLInputElement.prototype,E=Object.getOwnPropertyDescriptor(b,"checked").set;if(p!==r&&E){const w=new Event("click",{bubbles:a});E.call(h,r),h.dispatchEvent(w)}},[p,r,a]),g.jsx("input",{type:"checkbox","aria-hidden":!0,defaultChecked:r,...o,tabIndex:-1,ref:d,style:{...o.style,...f,position:"absolute",pointerEvents:"none",opacity:0,margin:0}})});WR.displayName=w3;function KR(e){return e?"checked":"unchecked"}var T3=VR,A3=YR;function k3({className:e,size:t="default",...r}){return g.jsx(T3,{"data-slot":"switch","data-size":t,className:be("peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6",e),...r,children:g.jsx(A3,{"data-slot":"switch-thumb",className:be("bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block rounded-full ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0")})})}var kc="Checkbox",[_3]=jt(kc),[N3,pv]=_3(kc);function R3(e){const{__scopeCheckbox:t,checked:r,children:a,defaultChecked:o,disabled:s,form:u,name:d,onCheckedChange:p,required:f,value:h="on",internal_do_not_use_render:b}=e,[v,E]=en({prop:r,defaultProp:o??!1,onChange:p,caller:kc}),[w,T]=S.useState(null),[k,A]=S.useState(null),_=S.useRef(!1),R=w?!!u||!!w.closest("form"):!0,L={checked:v,disabled:s,setChecked:E,control:w,setControl:T,name:d,form:u,value:h,hasConsumerStoppedPropagationRef:_,required:f,defaultChecked:la(o)?!1:o,isFormControl:R,bubbleInput:k,setBubbleInput:A};return g.jsx(N3,{scope:t,...L,children:C3(b)?b(L):a})}var XR="CheckboxTrigger",ZR=S.forwardRef(({__scopeCheckbox:e,onKeyDown:t,onClick:r,...a},o)=>{const{control:s,value:u,disabled:d,checked:p,required:f,setControl:h,setChecked:b,hasConsumerStoppedPropagationRef:v,isFormControl:E,bubbleInput:w}=pv(XR,e),T=_e(o,h),k=S.useRef(p);return S.useEffect(()=>{const A=s?.form;if(A){const _=()=>b(k.current);return A.addEventListener("reset",_),()=>A.removeEventListener("reset",_)}},[s,b]),g.jsx(Se.button,{type:"button",role:"checkbox","aria-checked":la(p)?"mixed":p,"aria-required":f,"data-state":rC(p),"data-disabled":d?"":void 0,disabled:d,value:u,...a,ref:T,onKeyDown:ge(t,A=>{A.key==="Enter"&&A.preventDefault()}),onClick:ge(r,A=>{b(_=>la(_)?!0:!_),w&&E&&(v.current=A.isPropagatiLine truncated
|
||||
.`.concat(OG,` {
|
||||
`+qe+n+dt}var Yn=!1;function Wn(n,i){if(!n||Yn)return"";Yn=!0;var l=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var d={DetermineComponentFrameRoot:function(){try{if(i){var ce=function(){throw Error()};if(Object.defineProperty(ce.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(ce,[])}catch(te){var Z=te}Reflect.construct(n,[],ce)}else{try{ce.call()}catch(te){Z=te}n.call(ce.prototype)}}else{try{throw Error()}catch(te){Z=te}(ce=n())&&typeof ce.catch=="function"&&ce.catch(function(){})}}catch(te){if(te&&Z&&typeof te.stack=="string")return[te.stack,Z.stack]}return[null,null]}};d.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var b=Object.getOwnPropertyDescriptor(d.DetermineComponentFrameRoot,"name");b&&b.configurable&&Object.defineProperty(d.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var y=d.DetermineComponentFrameRoot(),T=y[0],R=y[1];if(T&&R){var P=T.split(`
|
||||
`),W=R.split(`
|
||||
`);for(b=d=0;d<P.length&&!P[d].includes("DetermineComponentFrameRoot");)d++;for(;b<W.length&&!W[b].includes("DetermineComponentFrameRoot");)b++;if(d===P.length||b===W.length)for(d=P.length-1,b=W.length-1;1<=d&&0<=b&&P[d]!==W[b];)b--;for(;1<=d&&0<=b;d--,b--)if(P[d]!==W[b]){if(d!==1||b!==1)do if(d--,b--,0>b||P[d]!==W[b]){var oe=`
|
||||
`+P[d].replace(" at new "," at ");return n.displayName&&oe.includes("<anonymous>")&&(oe=oe.replace("<anonymous>",n.displayName)),oe}while(1<=d&&0<=b);break}}}finally{Yn=!1,Error.prepareStackTrace=l}return(l=n?n.displayName||n.name:"")?st(l):""}function Ir(n,i){switch(n.tag){case 26:case 27:case 5:return st(n.type);case 16:return st("Lazy");case 13:return n.child!==i&&i!==null?st("Suspense Fallback"):st("Suspense");case 19:return st("SuspenseList");case 0:case 15:return Wn(n.type,!1);case 11:return Wn(n.type.render,!1);case 1:return Wn(n.type,!0);case 31:return st("Activity");default:return""}}function Jl(n){try{var i="",l=null;do i+=Ir(n,l),l=n,n=n.return;while(n);return i}catch(d){return`
|
||||
Error generating stack: `+d.message+`
|
||||
`+d.stack}}var Za=Object.prototype.hasOwnProperty,Qa=e.unstable_scheduleCallback,bo=e.unstable_cancelCallback,Zc=e.unstable_shouldYield,Qc=e.unstable_requestPaint,Pt=e.unstable_now,Jc=e.unstable_getCurrentPriorityLevel,le=e.unstable_ImmediatePriority,ge=e.unstable_UserBlockingPriority,_e=e.unstable_NormalPriority,Me=e.unstable_LowPriority,We=e.unstable_IdlePriority,an=e.log,lr=e.unstable_setDisableYieldValue,$t=null,Et=null;function qt(n){if(typeof an=="function"&&lr(n),Et&&typeof Et.setStrictMode=="function")try{Et.setStrictMode($t,n)}catch{}}var tt=Math.clz32?Math.clz32:gO,Or=Math.log,Ln=Math.LN2;function gO(n){return n>>>=0,n===0?32:31-(Or(n)/Ln|0)|0}var es=256,ts=262144,ns=4194304;function ba(n){var i=n&42;if(i!==0)return i;switch(n&-n){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return n&261888;case 262144:case 524288:case 1048576:case 2097152:return n&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return n&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return n}}function rs(n,i,l){var d=n.pendingLanes;if(d===0)return 0;var b=0,y=n.suspendedLanes,T=n.pingedLanes;n=n.warmLanes;var R=d&134217727;return R!==0?(d=R&~y,d!==0?b=ba(d):(T&=R,T!==0?b=ba(T):l||(l=R&~n,l!==0&&(b=ba(l))))):(R=d&~y,R!==0?b=ba(R):T!==0?b=ba(T):l||(l=d&~n,l!==0&&(b=ba(l)))),b===0?0:i!==0&&i!==b&&(i&y)===0&&(y=b&-b,l=i&-i,y>=l||y===32&&(l&4194048)!==0)?i:b}function yo(n,i){return(n.pendingLanes&~(n.suspendedLanes&~n.pingedLanes)&i)===0}function mO(n,i){switch(n){case 1:case 2:case 4:case 8:case 64:return i+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return i+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function eS(){var n=ns;return ns<<=1,(ns&62914560)===0&&(ns=4194304),n}function ed(n){for(var i=[],l=0;31>l;l++)i.push(n);return i}function vo(n,i){n.pendingLanes|=i,i!==268435456&&(n.suspendedLanes=0,n.pingedLanes=0,n.warmLanes=0)}function hO(n,i,l,d,b,y){var T=n.pendingLanes;n.pendingLanes=l,n.suspendedLanes=0,n.pingedLanes=0,n.warmLanes=0,n.expiredLanes&=l,n.entangledLanes&=l,n.errorRecoveryDisabledLanes&=l,n.shellSuspendCounter=0;var R=n.entanglements,P=n.expirationTimes,W=n.hiddenUpdates;for(l=T&~l;0<l;){var oe=31-tt(l),ce=1<<oe;R[oe]=0,P[oe]=-1;var Z=W[oe];if(Z!==null)for(W[oe]=null,oe=0;oe<Z.length;oe++){var te=Z[oe];te!==null&&(te.lane&=-536870913)}l&=~ce}d!==0&&tS(n,d,0),y!==0&&b===0&&n.tag!==0&&(n.suspendedLanes|=y&~(T&~i))}function tS(n,i,l){n.pendingLanes|=i,n.suspendedLanes&=~i;var d=31-tt(i);n.entangledLanes|=i,n.entanglements[d]=n.entanglements[d]|1073741824|l&261930}function nS(n,i){var l=n.entangledLanes|=i;for(n=n.entanglements;l;){var d=31-tt(l),b=1<<d;b&i|n[d]&i&&(n[d]|=i),l&=~b}}function rS(n,i){var l=i&-i;return l=(l&42)!==0?1:td(l),(l&(n.suspendedLanes|i))!==0?0:l}function td(n){switch(n){case 2:n=1;break;case 8:n=4;break;case 32:n=16;break;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:n=128;break;case 268435456:n=134217728;break;default:n=0}return n}function nd(n){return n&=-n,2<n?8<n?(n&134217727)!==0?32:268435456:8:2}function aS(){var n=H.p;return n!==0?n:(n=window.event,n===void 0?32:GT(n.type))}function iS(n,i){var l=H.p;try{return H.p=n,i()}finally{H.p=l}}var Lr=Math.random().toString(36).slice(2),_t="__reactFiber$"+Lr,Yt="__reactProps$"+Lr,Ja="__reactContainer$"+Lr,rd="__reactEvents$"+Lr,bO="__reactListeners$"+Lr,yO="__reactHandles$"+Lr,oS="__reactResources$"+Lr,Eo="__reactMarker$"+Lr;function ad(n){delete n[_t],delete n[Yt],delete n[rd],delete n[bO],delete n[yO]}function ei(n){var i=n[_t];if(i)return i;for(var l=n.parentNode;l;){if(i=l[Ja]||l[_t]){if(l=i.alternate,i.child!==null||l!==null&&l.child!==null)for(n=_T(n);n!==null;){if(l=n[_t])return l;n=_T(n)}return i}n=l,l=n.parentNode}return null}function ti(n){if(n=n[_t]||n[Ja]){var i=n.tag;if(i===5||i===6||i===13||i===31||i===26||i===27||i===3)return n}return null}function So(n){var i=n.tag;if(i===5||i===26||i===27||i===6)return n.stateNode;throw Error(a(33))}function ni(n){var i=n[oS];return i||(i=n[oS]={hoistableStyles:new Map,hoistableScripts:new Map}),i}function wt(n){n[Eo]=!0}var lS=new Set,sS={};function ya(n,i){ri(n,i),ri(n+"Capture",i)}function ri(n,i){for(sS[n]=i,n=0;n<i.length;n++)lS.add(i[n])}var vO=RegExp("^[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u20
Line truncated
|
||||
`).replace(X1,"")}function bT(n,i){return i=hT(i),hT(n)===i}function Je(n,i,l,d,b,y){switch(l){case"children":typeof d=="string"?i==="body"||i==="textarea"&&d===""||ii(n,d):(typeof d=="number"||typeof d=="bigint")&&i!=="body"&&ii(n,""+d);break;case"className":is(n,"class",d);break;case"tabIndex":is(n,"tabindex",d);break;case"dir":case"role":case"viewBox":case"width":case"height":is(n,l,d);break;case"style":bS(n,d,y);break;case"data":if(i!=="object"){is(n,"data",d);break}case"src":case"href":if(d===""&&(i!=="a"||l!=="href")){n.removeAttribute(l);break}if(d==null||typeof d=="function"||typeof d=="symbol"||typeof d=="boolean"){n.removeAttribute(l);break}d=ls(""+d),n.setAttribute(l,d);break;case"action":case"formAction":if(typeof d=="function"){n.setAttribute(l,"javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')");break}else typeof y=="function"&&(l==="formAction"?(i!=="input"&&Je(n,i,"name",b.name,b,null),Je(n,i,"formEncType",b.formEncType,b,null),Je(n,i,"formMethod",b.formMethod,b,null),Je(n,i,"formTarget",b.formTarget,b,null)):(Je(n,i,"encType",b.encType,b,null),Je(n,i,"method",b.method,b,null),Je(n,i,"target",b.target,b,null)));if(d==null||typeof d=="symbol"||typeof d=="boolean"){n.removeAttribute(l);break}d=ls(""+d),n.setAttribute(l,d);break;case"onClick":d!=null&&(n.onclick=ur);break;case"onScroll":d!=null&&Ue("scroll",n);break;case"onScrollEnd":d!=null&&Ue("scrollend",n);break;case"dangerouslySetInnerHTML":if(d!=null){if(typeof d!="object"||!("__html"in d))throw Error(a(61));if(l=d.__html,l!=null){if(b.children!=null)throw Error(a(60));n.innerHTML=l}}break;case"multiple":n.multiple=d&&typeof d!="function"&&typeof d!="symbol";break;case"muted":n.muted=d&&typeof d!="function"&&typeof d!="symbol";break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"defaultValue":case"defaultChecked":case"innerHTML":case"ref":break;case"autoFocus":break;case"xlinkHref":if(d==null||typeof d=="function"||typeof d=="boolean"||typeof d=="symbol"){n.removeAttribute("xlink:href");break}l=ls(""+d),n.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",l);break;case"contentEditable":case"spellCheck":case"draggable":case"value":case"autoReverse":case"externalResourcesRequired":case"focusable":case"preserveAlpha":d!=null&&typeof d!="function"&&typeof d!="symbol"?n.setAttribute(l,""+d):n.removeAttribute(l);break;case"inert":case"allowFullScreen":case"async":case"autoPlay":case"controls":case"default":case"defer":case"disabled":case"disablePictureInPicture":case"disableRemotePlayback":case"formNoValidate":case"hidden":case"loop":case"noModule":case"noValidate":case"open":case"playsInline":case"readOnly":case"required":case"reversed":case"scoped":case"seamless":case"itemScope":d&&typeof d!="function"&&typeof d!="symbol"?n.setAttribute(l,""):n.removeAttribute(l);break;case"capture":case"download":d===!0?n.setAttribute(l,""):d!==!1&&d!=null&&typeof d!="function"&&typeof d!="symbol"?n.setAttribute(l,d):n.removeAttribute(l);break;case"cols":case"rows":case"size":case"span":d!=null&&typeof d!="function"&&typeof d!="symbol"&&!isNaN(d)&&1<=d?n.setAttribute(l,d):n.removeAttribute(l);break;case"rowSpan":case"start":d==null||typeof d=="function"||typeof d=="symbol"||isNaN(d)?n.removeAttribute(l):n.setAttribute(l,d);break;case"popover":Ue("beforetoggle",n),Ue("toggle",n),as(n,"popover",d);break;case"xlinkActuate":sr(n,"http://www.w3.org/1999/xlink","xlink:actuate",d);break;case"xlinkArcrole":sr(n,"http://www.w3.org/1999/xlink","xlink:arcrole",d);break;case"xlinkRole":sr(n,"http://www.w3.org/1999/xlink","xlink:role",d);break;case"xlinkShow":sr(n,"http://www.w3.org/1999/xlink","xlink:show",d);break;case"xlinkTitle":sr(n,"http://www.w3.org/1999/xlink","xlink:title",d);break;case"xlinkType":sr(n,"http://www.w3.org/1999/xlink","xlink:type",d);break;case"xmlBase":sr(n,"http://www.w3.org/XML/1998/namespace","xml:base",d);break;case"xmlLang":sr(n,"http://www.w3.org/XML/1998/namespace","xml:lang",d);break;case"xmlSpace":sr(n,"http://www.w3.org/XML/1998/namespace","xml:space",d);break;case"is":as(n,"is",d);break;case"innerText":case"textContent":break;default:(!(2<l.length)||l[0]!=="o"&&l[0]!=="O"||l[1]!=="n"&&l[1]!=="N")&&(l=TO.get(l)||l,as(n,l,d))}}function Wp(n,i,l,d,b,y){switch(l){case"style":bS(n,d,y);break;case"dangerouslySetInnerHTML":if(d!=null){if(typeof d!="object"||!("__html"in d))throw Error(a(61));if(l=d.__html,l!=null){if(b.children!=null)throw Error(a(60));n.innerHTML=l}}break;case"children":typeof d=="string"?ii(n,d):(typeof d=="number"||typeof d=="bigint")&&ii(n,""+d);break;case"onScroll":d!=null&&Ue("scroll",n);break;case"onScrollEnd":d!=null&&Ue("scrollend",n);break;case"onClick":d!=null&&(n.onclick=ur);break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"inne
Line truncated
|
||||
.`.concat(y2,` {
|
||||
overflow: hidden `).concat(a,`;
|
||||
padding-right: `).concat(d,"px ").concat(a,`;
|
||||
padding-right: `).concat(c,"px ").concat(a,`;
|
||||
}
|
||||
body[`).concat(qi,`] {
|
||||
body[`).concat(Wi,`] {
|
||||
overflow: hidden `).concat(a,`;
|
||||
overscroll-behavior: contain;
|
||||
`).concat([t&&"position: relative ".concat(a,";"),r==="margin"&&`
|
||||
@@ -68,41 +21,90 @@ Defaulting to \`null\`.`}var b3=PR,y3=GR;function E3({className:e,value:t,...r})
|
||||
padding-right: `).concat(u,`px;
|
||||
margin-left:0;
|
||||
margin-top:0;
|
||||
margin-right: `).concat(d,"px ").concat(a,`;
|
||||
`),r==="padding"&&"padding-right: ".concat(d,"px ").concat(a,";")].filter(Boolean).join(""),`
|
||||
margin-right: `).concat(c,"px ").concat(a,`;
|
||||
`),r==="padding"&&"padding-right: ".concat(c,"px ").concat(a,";")].filter(Boolean).join(""),`
|
||||
}
|
||||
|
||||
.`).concat(Iu,` {
|
||||
right: `).concat(d,"px ").concat(a,`;
|
||||
.`).concat(Du,` {
|
||||
right: `).concat(c,"px ").concat(a,`;
|
||||
}
|
||||
|
||||
.`).concat(Lu,` {
|
||||
margin-right: `).concat(d,"px ").concat(a,`;
|
||||
.`).concat(Mu,` {
|
||||
margin-right: `).concat(c,"px ").concat(a,`;
|
||||
}
|
||||
|
||||
.`).concat(Iu," .").concat(Iu,` {
|
||||
.`).concat(Du," .").concat(Du,` {
|
||||
right: 0 `).concat(a,`;
|
||||
}
|
||||
|
||||
.`).concat(Lu," .").concat(Lu,` {
|
||||
.`).concat(Mu," .").concat(Mu,` {
|
||||
margin-right: 0 `).concat(a,`;
|
||||
}
|
||||
|
||||
body[`).concat(qi,`] {
|
||||
`).concat(IG,": ").concat(d,`px;
|
||||
body[`).concat(Wi,`] {
|
||||
`).concat(v2,": ").concat(c,`px;
|
||||
}
|
||||
`)},Dk=function(){var e=parseInt(document.body.getAttribute(qi)||"0",10);return isFinite(e)?e:0},ZG=function(){S.useEffect(function(){return document.body.setAttribute(qi,(Dk()+1).toString()),function(){var e=Dk()-1;e<=0?document.body.removeAttribute(qi):document.body.setAttribute(qi,e.toString())}},[])},QG=function(e){var t=e.noRelative,r=e.noImportant,a=e.gapMode,o=a===void 0?"margin":a;ZG();var s=S.useMemo(function(){return WG(o)},[o]);return S.createElement(KG,{styles:XG(s,!t,o,r?"":"!important")})},bg=!1;if(typeof window<"u")try{var Tu=Object.defineProperty({},"passive",{get:function(){return bg=!0,!0}});window.addEventListener("test",Tu,Tu),window.removeEventListener("test",Tu,Tu)}catch{bg=!1}var Ui=bg?{passive:!1}:!1,JG=function(e){return e.tagName==="TEXTAREA"},YC=function(e,t){if(!(e instanceof Element))return!1;var r=window.getComputedStyle(e);return r[t]!=="hidden"&&!(r.overflowY===r.overflowX&&!JG(e)&&r[t]==="visible")},eH=function(e){return YC(e,"overflowY")},tH=function(e){return YC(e,"overflowX")},Mk=function(e,t){var r=t.ownerDocument,a=t;do{typeof ShadowRoot<"u"&&a instanceof ShadowRoot&&(a=a.host);var o=WC(e,a);if(o){var s=KC(e,a),u=s[1],d=s[2];if(u>d)return!0}a=a.parentNode}while(a&&a!==r.body);return!1},nH=function(e){var t=e.scrollTop,r=e.scrollHeight,a=e.clientHeight;return[t,r,a]},rH=function(e){var t=e.scrollLeft,r=e.scrollWidth,a=e.clientWidth;return[t,r,a]},WC=function(e,t){return e==="v"?eH(t):tH(t)},KC=function(e,t){return e==="v"?nH(t):rH(t)},aH=function(e,t){return e==="h"&&t==="rtl"?-1:1},iH=function(e,t,r,a,o){var s=aH(e,window.getComputedStyle(t).direction),u=s*a,d=r.target,p=t.contains(d),f=!1,h=u>0,b=0,v=0;do{if(!d)break;var E=KC(e,d),w=E[0],T=E[1],k=E[2],A=T-k-s*w;(w||A)&&WC(e,d)&&(b+=A,v+=w);var _=d.parentNode;d=_&&_.nodeType===Node.DOCUMENT_FRAGMENT_NODE?_.host:_}while(!p&&d!==document.body||p&&(t.contains(d)||t===d));return(h&&Math.abs(b)<1||!h&&Math.abs(v)<1)&&(f=!0),f},Au=function(e){return"changedTouches"in e?[e.changedTouches[0].clientX,e.changedTouches[0].clientY]:[0,0]},jk=function(e){return[e.deltaX,e.deltaY]},Uk=function(e){return e&&"current"in e?e.current:e},oH=function(e,t){return e[0]===t[0]&&e[1]===t[1]},lH=function(e){return`
|
||||
`)},kA=function(){var e=parseInt(document.body.getAttribute(Wi)||"0",10);return isFinite(e)?e:0},F2=function(){S.useEffect(function(){return document.body.setAttribute(Wi,(kA()+1).toString()),function(){var e=kA()-1;e<=0?document.body.removeAttribute(Wi):document.body.setAttribute(Wi,e.toString())}},[])},B2=function(e){var t=e.noRelative,r=e.noImportant,a=e.gapMode,o=a===void 0?"margin":a;F2();var s=S.useMemo(function(){return M2(o)},[o]);return S.createElement(j2,{styles:U2(s,!t,o,r?"":"!important")})},ig=!1;if(typeof window<"u")try{var vu=Object.defineProperty({},"passive",{get:function(){return ig=!0,!0}});window.addEventListener("test",vu,vu),window.removeEventListener("test",vu,vu)}catch{ig=!1}var Ui=ig?{passive:!1}:!1,z2=function(e){return e.tagName==="TEXTAREA"},uN=function(e,t){if(!(e instanceof Element))return!1;var r=window.getComputedStyle(e);return r[t]!=="hidden"&&!(r.overflowY===r.overflowX&&!z2(e)&&r[t]==="visible")},P2=function(e){return uN(e,"overflowY")},$2=function(e){return uN(e,"overflowX")},_A=function(e,t){var r=t.ownerDocument,a=t;do{typeof ShadowRoot<"u"&&a instanceof ShadowRoot&&(a=a.host);var o=cN(e,a);if(o){var s=dN(e,a),u=s[1],c=s[2];if(u>c)return!0}a=a.parentNode}while(a&&a!==r.body);return!1},G2=function(e){var t=e.scrollTop,r=e.scrollHeight,a=e.clientHeight;return[t,r,a]},H2=function(e){var t=e.scrollLeft,r=e.scrollWidth,a=e.clientWidth;return[t,r,a]},cN=function(e,t){return e==="v"?P2(t):$2(t)},dN=function(e,t){return e==="v"?G2(t):H2(t)},V2=function(e,t){return e==="h"&&t==="rtl"?-1:1},q2=function(e,t,r,a,o){var s=V2(e,window.getComputedStyle(t).direction),u=s*a,c=r.target,p=t.contains(c),f=!1,h=u>0,m=0,v=0;do{if(!c)break;var E=dN(e,c),x=E[0],w=E[1],k=E[2],A=w-k-s*x;(x||A)&&cN(e,c)&&(m+=A,v+=x);var _=c.parentNode;c=_&&_.nodeType===Node.DOCUMENT_FRAGMENT_NODE?_.host:_}while(!p&&c!==document.body||p&&(t.contains(c)||t===c));return(h&&Math.abs(m)<1||!h&&Math.abs(v)<1)&&(f=!0),f},Eu=function(e){return"changedTouches"in e?[e.changedTouches[0].clientX,e.changedTouches[0].clientY]:[0,0]},NA=function(e){return[e.deltaX,e.deltaY]},RA=function(e){return e&&"current"in e?e.current:e},Y2=function(e,t){return e[0]===t[0]&&e[1]===t[1]},W2=function(e){return`
|
||||
.block-interactivity-`.concat(e,` {pointer-events: none;}
|
||||
.allow-interactivity-`).concat(e,` {pointer-events: all;}
|
||||
`)},sH=0,Fi=[];function uH(e){var t=S.useRef([]),r=S.useRef([0,0]),a=S.useRef(),o=S.useState(sH++)[0],s=S.useState(qC)[0],u=S.useRef(e);S.useEffect(function(){u.current=e},[e]),S.useEffect(function(){if(e.inert){document.body.classList.add("block-interactivity-".concat(o));var T=CG([e.lockRef.current],(e.shards||[]).map(Uk),!0).filter(Boolean);return T.forEach(function(k){return k.classList.add("allow-interactivity-".concat(o))}),function(){document.body.classList.remove("block-interactivity-".concat(o)),T.forEach(function(k){return k.classList.remove("allow-interactivity-".concat(o))})}}},[e.inert,e.lockRef.current,e.shards]);var d=S.useCallback(function(T,k){if("touches"in T&&T.touches.length===2||T.type==="wheel"&&T.ctrlKey)return!u.current.allowPinchZoom;var A=Au(T),_=r.current,R="deltaX"in T?T.deltaX:_[0]-A[0],L="deltaY"in T?T.deltaY:_[1]-A[1],j,D=T.target,z=Math.abs(R)>Math.abs(L)?"h":"v";if("touches"in T&&z==="h"&&D.type==="range")return!1;var F=window.getSelection(),V=F&&F.anchorNode,M=V?V===D||V.contains(D):!1;if(M)return!1;var H=Mk(z,D);if(!H)return!0;if(H?j=z:(j=z==="v"?"h":"v",H=Mk(z,D)),!H)return!1;if(!a.current&&"changedTouches"in T&&(R||L)&&(a.current=j),!j)return!0;var K=a.current||j;return iH(K,k,T,K==="h"?R:L)},[]),p=S.useCallback(function(T){var k=T;if(!(!Fi.length||Fi[Fi.length-1]!==s)){var A="deltaY"in k?jk(k):Au(k),_=t.current.filter(function(j){return j.name===k.type&&(j.target===k.target||k.target===j.shadowParent)&&oH(j.delta,A)})[0];if(_&&_.should){k.cancelable&&k.preventDefault();return}if(!_){var R=(u.current.shards||[]).map(Uk).filter(Boolean).filter(function(j){return j.contains(k.target)}),L=R.length>0?d(k,R[0]):!u.current.noIsolation;L&&k.cancelable&&k.preventDefault()}}},[]),f=S.useCallback(function(T,k,A,_){var R={name:T,delta:k,target:A,should:_,shadowParent:cH(A)};t.current.push(R),setTimeout(function(){t.current=t.current.filter(function(L){return L!==R})},1)},[]),h=S.useCallback(function(T){r.current=Au(T),a.current=void 0},[]),b=S.useCallback(function(T){f(T.type,jk(T),T.target,d(T,e.lockRef.current))},[]),v=S.useCallback(function(T){f(T.type,Au(T),T.target,d(T,e.lockRef.current))},[]);S.useEffect(function(){return Fi.push(s),e.setCallbacks({onScrollCapture:b,onWheelCapture:b,onTouchMoveCapture:v}),document.addEventListener("wheel",p,Ui),document.addEventListener("touchmove",p,Ui),document.addEventListener("touchstart",h,Ui),function(){Fi=Fi.filter(function(T){return T!==s}),document.removeEventListener("wheel",p,Ui),document.removeEventListener("touchmove",p,Ui),document.removeEventListener("touchstart",h,Ui)}},[]);var E=e.removeScrollBar,w=e.inert;return S.createElement(S.Fragment,null,w?S.createElement(s,{styles:lH(o)}):null,E?S.createElement(QG,{noRelative:e.noRelative,gapMode:e.gapMode}):null)}function cH(e){for(var t=null;e!==null;)e instanceof ShadowRoot&&(t=e.host,e=e.host),e=e.parentNode;return t}const dH=BG(VC,uH);var Mc=S.forwardRef(function(e,t){return S.createElement(Dc,Jn({},e,{ref:t,sideCar:dH}))});Mc.classNames=Dc.classNames;var pH=[" ","Enter","ArrowUp","ArrowDown"],fH=[" ","Enter"],za="Select",[jc,Uc,gH]=yc(za),[uo]=jt(za,[gH,so]),Fc=so(),[mH,pa]=uo(za),[hH,bH]=uo(za),XC=e=>{const{__scopeSelect:t,children:r,open:a,defaultOpen:o,onOpenChange:s,value:u,defaultValue:d,onValueChange:p,dir:f,name:h,autoComplete:b,disabled:v,required:E,form:w}=e,T=Fc(t),[k,A]=S.useState(null),[_,R]=S.useState(null),[L,j]=S.useState(!1),D=Ha(f),[z,F]=en({prop:a,defaultProp:o??!1,onChange:s,caller:za}),[V,M]=en({prop:u,defaultProp:d,onChange:p,caller:za}),H=S.useRef(null),K=k?w||!!k.closest("form"):!0,[Z,te]=S.useState(new Set),Q=Array.from(Z).map(P=>P.props.value).join(";");return g.jsx(wv,{...T,children:g.jsxs(mH,{required:E,scope:t,trigger:k,onTriggerChange:A,valueNode:_,onValueNodeChange:R,valueNodeHasChildren:L,onValueNodeHasChildrenChange:j,contentId:Rn(),value:V,onValueChange:M,open:z,onOpenChange:F,dir:D,triggerPointerDownPosRef:H,disabled:v,children:[g.jsx(jc.Provider,{scope:t,children:g.jsx(hH,{scope:e.__scopeSelect,onNativeOptionAdd:S.useCallback(P=>{te(W=>new Set(W).add(P))},[]),onNativeOptionRemove:S.useCallback(P=>{te(W=>{const ne=new Set(W);return ne.delete(P),ne})},[]),children:r})}),K?g.jsxs(y0,{"aria-hidden":!0,required:E,tabIndex:-1,name:h,autoComplete:b,value:V,onChange:P=>M(P.target.value),disabled:v,form:w,children:[V===void 0?g.jsx("option",{value:""}):null,Array.from(Z)]},Q):null]})})};XC.displayName=za;var ZC="SelectTrigger",QC=S.forwardRef((e,t)=>{const{__scopeSelect:r,disabled:a=!1,...o}=e,s=Fc(r),u=pa(ZC,r),d=u.disabled||a,p=_e(t,u.onTriggerChange),f=Uc(r),h=S.useRef("touch"),[b,v,E]=v0(T=>{const k=f().filter(R=>!R.disabled),A=k.find(R=>R.value===u.value),_=S0(k,T,A);_!==void 0&&u.onValueChange(_.value)}),w=T=>{d||(u.onOpenChange(!0),E()),T&&(u.triggerPointerDownPosRef.current={x:Math.round(T.pageX),y:Math.round(T.pageY)})};return g.jsx(Lc,{asChild:!0,...s,children:g.jsx(Se.button,{type:"button",role:"combobox","aria-controls":u.contentId,"aria-ex
Line truncated
|
||||
`)},K2=0,Fi=[];function X2(e){var t=S.useRef([]),r=S.useRef([0,0]),a=S.useRef(),o=S.useState(K2++)[0],s=S.useState(sN)[0],u=S.useRef(e);S.useEffect(function(){u.current=e},[e]),S.useEffect(function(){if(e.inert){document.body.classList.add("block-interactivity-".concat(o));var w=b2([e.lockRef.current],(e.shards||[]).map(RA),!0).filter(Boolean);return w.forEach(function(k){return k.classList.add("allow-interactivity-".concat(o))}),function(){document.body.classList.remove("block-interactivity-".concat(o)),w.forEach(function(k){return k.classList.remove("allow-interactivity-".concat(o))})}}},[e.inert,e.lockRef.current,e.shards]);var c=S.useCallback(function(w,k){if("touches"in w&&w.touches.length===2||w.type==="wheel"&&w.ctrlKey)return!u.current.allowPinchZoom;var A=Eu(w),_=r.current,N="deltaX"in w?w.deltaX:_[0]-A[0],D="deltaY"in w?w.deltaY:_[1]-A[1],F,L=w.target,z=Math.abs(N)>Math.abs(D)?"h":"v";if("touches"in w&&z==="h"&&L.type==="range")return!1;var $=window.getSelection(),K=$&&$.anchorNode,j=K?K===L||K.contains(L):!1;if(j)return!1;var q=_A(z,L);if(!q)return!0;if(q?F=z:(F=z==="v"?"h":"v",q=_A(z,L)),!q)return!1;if(!a.current&&"changedTouches"in w&&(N||D)&&(a.current=F),!F)return!0;var X=a.current||F;return q2(X,k,w,X==="h"?N:D)},[]),p=S.useCallback(function(w){var k=w;if(!(!Fi.length||Fi[Fi.length-1]!==s)){var A="deltaY"in k?NA(k):Eu(k),_=t.current.filter(function(F){return F.name===k.type&&(F.target===k.target||k.target===F.shadowParent)&&Y2(F.delta,A)})[0];if(_&&_.should){k.cancelable&&k.preventDefault();return}if(!_){var N=(u.current.shards||[]).map(RA).filter(Boolean).filter(function(F){return F.contains(k.target)}),D=N.length>0?c(k,N[0]):!u.current.noIsolation;D&&k.cancelable&&k.preventDefault()}}},[]),f=S.useCallback(function(w,k,A,_){var N={name:w,delta:k,target:A,should:_,shadowParent:Z2(A)};t.current.push(N),setTimeout(function(){t.current=t.current.filter(function(D){return D!==N})},1)},[]),h=S.useCallback(function(w){r.current=Eu(w),a.current=void 0},[]),m=S.useCallback(function(w){f(w.type,NA(w),w.target,c(w,e.lockRef.current))},[]),v=S.useCallback(function(w){f(w.type,Eu(w),w.target,c(w,e.lockRef.current))},[]);S.useEffect(function(){return Fi.push(s),e.setCallbacks({onScrollCapture:m,onWheelCapture:m,onTouchMoveCapture:v}),document.addEventListener("wheel",p,Ui),document.addEventListener("touchmove",p,Ui),document.addEventListener("touchstart",h,Ui),function(){Fi=Fi.filter(function(w){return w!==s}),document.removeEventListener("wheel",p,Ui),document.removeEventListener("touchmove",p,Ui),document.removeEventListener("touchstart",h,Ui)}},[]);var E=e.removeScrollBar,x=e.inert;return S.createElement(S.Fragment,null,x?S.createElement(s,{styles:W2(o)}):null,E?S.createElement(B2,{noRelative:e.noRelative,gapMode:e.gapMode}):null)}function Z2(e){for(var t=null;e!==null;)e instanceof ShadowRoot&&(t=e.host,e=e.host),e=e.parentNode;return t}const Q2=k2(lN,X2);var dc=S.forwardRef(function(e,t){return S.createElement(cc,er({},e,{ref:t,sideCar:Q2}))});dc.classNames=cc.classNames;var J2=function(e){if(typeof document>"u")return null;var t=Array.isArray(e)?e[0]:e;return t.ownerDocument.body},Bi=new WeakMap,Su=new WeakMap,xu={},kf=0,pN=function(e){return e&&(e.host||pN(e.parentNode))},ej=function(e,t){return t.map(function(r){if(e.contains(r))return r;var a=pN(r);return a&&e.contains(a)?a:(console.error("aria-hidden",r,"in not contained inside",e,". Doing nothing"),null)}).filter(function(r){return!!r})},tj=function(e,t,r,a){var o=ej(t,Array.isArray(e)?e:[e]);xu[r]||(xu[r]=new WeakMap);var s=xu[r],u=[],c=new Set,p=new Set(o),f=function(m){!m||c.has(m)||(c.add(m),f(m.parentNode))};o.forEach(f);var h=function(m){!m||p.has(m)||Array.prototype.forEach.call(m.children,function(v){if(c.has(v))h(v);else try{var E=v.getAttribute(a),x=E!==null&&E!=="false",w=(Bi.get(v)||0)+1,k=(s.get(v)||0)+1;Bi.set(v,w),s.set(v,k),u.push(v),w===1&&x&&Su.set(v,!0),k===1&&v.setAttribute(r,"true"),x||v.setAttribute(a,"true")}catch(A){console.error("aria-hidden: cannot operate on ",v,A)}})};return h(t),c.clear(),kf++,function(){u.forEach(function(m){var v=Bi.get(m)-1,E=s.get(m)-1;Bi.set(m,v),s.set(m,E),v||(Su.has(m)||m.removeAttribute(a),Su.delete(m)),E||m.removeAttribute(r)}),kf--,kf||(Bi=new WeakMap,Bi=new WeakMap,Su=new WeakMap,xu={})}},Xg=function(e,t,r){r===void 0&&(r="data-aria-hidden");var a=Array.from(Array.isArray(e)?e:[e]),o=J2(e);return o?(a.push.apply(a,Array.from(o.querySelectorAll("[aria-live], script"))),tj(a,o,r,"aria-hidden")):function(){return null}};function nj(e){const t=rj(e),r=S.forwardRef((a,o)=>{const{children:s,...u}=a,c=S.Children.toArray(s),p=c.find(ij);if(p){const f=p.props.children,h=c.map(m=>m===p?S.Children.count(f)>1?S.Children.only(null):S.isValidElement(f)?f.props.children:null:m);return g.jsx(t,{...u,ref:o,children:S.isValidElement(f)?S.cloneElement(f,void 0,h):null})}return g.jsx(t,{...u,ref:o,children:s})});return r.displayName=`${e}.Slot`,r}function rj(e){const t=S.forwardRef((r,a)=>{const{children:o
Line truncated
|
||||
|
||||
If you want to hide the \`${t.titleName}\`, you can wrap it with our VisuallyHidden component.
|
||||
|
||||
For more information, see https://radix-ui.com/primitives/docs/components/${t.docsSlug}`;return S.useEffect(()=>{e&&(document.getElementById(e)||console.error(r))},[r,e]),null},L5="DialogDescriptionWarning",D5=({contentRef:e,descriptionId:t})=>{const a=`Warning: Missing \`Description\` or \`aria-describedby={undefined}\` for {${Z0(L5).contentName}}.`;return S.useEffect(()=>{const o=e.current?.getAttribute("aria-describedby");t&&o&&(document.getElementById(t)||console.warn(a))},[a,e,t]),null},M5=U0,j5=B0,U5=P0,F5=$0,B5=G0,z5=V0,P5=Y0,$5=K0;function G5({...e}){return g.jsx(M5,{"data-slot":"dialog",...e})}function H5({...e}){return g.jsx(j5,{"data-slot":"dialog-trigger",...e})}function V5({...e}){return g.jsx(U5,{"data-slot":"dialog-portal",...e})}function q5({className:e,...t}){return g.jsx(F5,{"data-slot":"dialog-overlay",className:be("data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",e),...t})}function Y5({className:e,children:t,showCloseButton:r=!0,...a}){return g.jsxs(V5,{"data-slot":"dialog-portal",children:[g.jsx(q5,{}),g.jsxs(B5,{"data-slot":"dialog-content",className:be("bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",e),...a,children:[t,r&&g.jsxs($5,{"data-slot":"dialog-close",className:"ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",children:[g.jsx(z$,{}),g.jsx("span",{className:"sr-only",children:"Close"})]})]})]})}function W5({className:e,...t}){return g.jsx("div",{"data-slot":"dialog-header",className:be("flex flex-col gap-2 text-center sm:text-left",e),...t})}function K5({className:e,...t}){return g.jsx(z5,{"data-slot":"dialog-title",className:be("text-lg leading-none font-semibold",e),...t})}function X5({className:e,...t}){return g.jsx(P5,{"data-slot":"dialog-description",className:be("text-muted-foreground text-sm",e),...t})}var Z5=Symbol("radix.slottable");function Q5(e){const t=({children:r})=>g.jsx(g.Fragment,{children:r});return t.displayName=`${e}.Slottable`,t.__radixId=Z5,t}var[Pc]=jt("Tooltip",[so]),$c=so(),Q0="TooltipProvider",J5=700,kg="tooltip.open",[e6,Iv]=Pc(Q0),J0=e=>{const{__scopeTooltip:t,delayDuration:r=J5,skipDelayDuration:a=300,disableHoverableContent:o=!1,children:s}=e,u=S.useRef(!0),d=S.useRef(!1),p=S.useRef(0);return S.useEffect(()=>{const f=p.current;return()=>window.clearTimeout(f)},[]),g.jsx(e6,{scope:t,isOpenDelayedRef:u,delayDuration:r,onOpen:S.useCallback(()=>{window.clearTimeout(p.current),u.current=!1},[]),onClose:S.useCallback(()=>{window.clearTimeout(p.current),p.current=window.setTimeout(()=>u.current=!0,a)},[a]),isPointerInTransitRef:d,onPointerInTransitChange:S.useCallback(f=>{d.current=f},[]),disableHoverableContent:o,children:s})};J0.displayName=Q0;var kl="Tooltip",[t6,Hl]=Pc(kl),eO=e=>{const{__scopeTooltip:t,children:r,open:a,defaultOpen:o,onOpenChange:s,disableHoverableContent:u,delayDuration:d}=e,p=Iv(kl,e.__scopeTooltip),f=$c(t),[h,b]=S.useState(null),v=Rn(),E=S.useRef(0),w=u??p.disableHoverableContent,T=d??p.delayDuration,k=S.useRef(!1),[A,_]=en({prop:a,defaultProp:o??!1,onChange:z=>{z?(p.onOpen(),document.dispatchEvent(new CustomEvent(kg))):p.onClose(),s?.(z)},caller:kl}),R=S.useMemo(()=>A?k.current?"delayed-open":"instant-open":"closed",[A]),L=S.useCallback(()=>{window.clearTimeout(E.current),E.current=0,k.current=!1,_(!0)},[_]),j=S.useCallback(()=>{window.clearTimeout(E.current),E.current=0,_(!1)},[_]),D=S.useCallback(()=>{window.clearTimeout(E.current),E.current=window.setTimeout(()=>{k.current=!0,_(!0),E.current=0},T)},[T,_]);return S.useEffect(()=>()=>{E.current&&(window.clearTimeout(E.current),E.current=0)},[]),g.jsx(wv,{...f,children:g.jsx(t6,{scope:t,contentId:v,open:A,stateAttribute:R,trigger:h,onTriggerChange:b,onTriggerEnter:S.useCallback(()=>{p.isOpenDelayedRef.current?D():L()},[p.isOpenDelayedRef,D,L]),onTriggerLeave:S.useCallback(()=>{w?j():(window.clearTimeout(E.current),E.current=0)},[j,w]),onOpen:L,onClose:j,disableHoverableContent:w,children:r})})};eO.displayName=kl;var _g="TooltipTrigger",tO=S.forwardRef((e,t)=>{const{__scopeTooltip:r,...a}=e,o=Hl(_g,r),s=Iv(_g,r),u=$c(r),d=S.useRef(null),p=_e(t,d,o.onTriggerChange),f=S.useRef(!1),h=S.useRef(!1),b=S.useCallback(()=>f.current=!1,[]);return S.useEffect(()=>()=>document.removeEventListener("pointerup",b),[b]),g.jsx(Lc,{asChild:!0,...u,children:g.jsx(Se.button,
Line truncated
|
||||
For more information, see https://radix-ui.com/primitives/docs/components/${t.docsSlug}`;return S.useEffect(()=>{e&&(document.getElementById(e)||console.error(r))},[r,e]),null},mj="DialogDescriptionWarning",hj=({contentRef:e,descriptionId:t})=>{const a=`Warning: Missing \`Description\` or \`aria-describedby={undefined}\` for {${NN(mj).contentName}}.`;return S.useEffect(()=>{const o=e.current?.getAttribute("aria-describedby");t&&o&&(document.getElementById(t)||console.warn(a))},[a,e,t]),null},em=gN,bj=hN,tm=yN,nm=vN,rm=EN,RN=xN,yj=TN,CN=kN;function vj({...e}){return g.jsx(em,{"data-slot":"sheet",...e})}function Ej({...e}){return g.jsx(tm,{"data-slot":"sheet-portal",...e})}function Sj({className:e,...t}){return g.jsx(nm,{"data-slot":"sheet-overlay",className:me("data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",e),...t})}function xj({className:e,children:t,side:r="right",showCloseButton:a=!0,...o}){return g.jsxs(Ej,{children:[g.jsx(Sj,{}),g.jsxs(rm,{"data-slot":"sheet-content",className:me("bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",r==="right"&&"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",r==="left"&&"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",r==="top"&&"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",r==="bottom"&&"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",e),...o,children:[t,a&&g.jsxs(CN,{className:"ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none",children:[g.jsx(S_,{className:"size-4"}),g.jsx("span",{className:"sr-only",children:"Close"})]})]})]})}function wj({className:e,...t}){return g.jsx("div",{"data-slot":"sheet-header",className:me("flex flex-col gap-1.5 p-4",e),...t})}function Tj({className:e,...t}){return g.jsx(RN,{"data-slot":"sheet-title",className:me("text-foreground font-semibold",e),...t})}function IN({nav:e,activeItem:t,onItemClick:r}){const a=(o,s=0)=>{const u=(o.path||o.title)===t;return o.children&&o.children.length>0?g.jsxs("div",{children:[g.jsx("div",{className:"flex items-center gap-2 px-3 py-1.5 text-xs font-semibold text-muted-foreground/70 uppercase tracking-widest mt-4 first:mt-0",children:o.title}),g.jsx("div",{className:"space-y-0.5",children:o.children.map(p=>a(p,s+1))})]},o.title+(o.path||"")):g.jsx("button",{type:"button",onClick:()=>r(o),className:`w-full text-left px-3 py-1.5 text-sm rounded-md transition-all duration-150 ${u?"bg-primary/10 text-primary font-medium border-l-2 border-primary":"text-muted-foreground hover:text-foreground hover:bg-muted/50"}`,style:{paddingLeft:`${12+s*12}px`},children:o.title},o.title+(o.path||""))};return g.jsx("nav",{className:"space-y-0.5",children:e.items?.map(o=>a(o))})}function _f({nav:e,activeItem:t,onItemClick:r}){return g.jsx("aside",{className:"w-64 min-w-[16rem] border-r border-border/50 hidden md:block",children:g.jsxs(eN,{className:"h-[calc(100vh-4rem)] py-4 px-2",children:[g.jsx(IN,{nav:e,activeItem:t,onItemClick:r}),g.jsx(qi,{className:"my-4"})]})})}function Aj({open:e,onOpenChange:t,nav:r,activeItem:a,onItemClick:o,title:s="Navigation"}){return g.jsx(vj,{open:e,onOpenChange:t,children:g.jsxs(xj,{side:"left",className:"w-72 p-0",children:[g.jsx(wj,{className:"border-b px-4 py-3",children:g.jsx(Tj,{children:s})}),g.jsx(eN,{className:"h-[calc(100vh-4rem)] px-2 py-4",children:g.jsx(IN,{nav:r,activeItem:a,onItemClick:u=>{o(u),t(!1)}})})]})})}function CA(e){const t=[],r=String(e||"");let a=r.indexOf(","),o=0,s=!1;for(;!s;){a===-1&&(a=r.length,s=!0);const u=r.slice(o,a).trim();(u||!s)&&t.push(u),o=a+1,a=r.indexOf(",",o)}return t}function kj(e,t){const r={};return(e[e.length-1]===""?[...e,""]:e).join((r.padRight?" ":"")+","+(r.padLeft===!1?"":" ")).trim()}const _j=/^[$_\p{ID_Start}][$_\u{200C}\u{200D}\p{ID_Continue}]*$/u,Nj=/^[$_\p{ID_Start}][-$_\u{200C}\u{200D}\p{ID_Continue}]*$/u,Rj={};function IA(e,t){return(Rj.jsx?Nj:_j).test(e)}const Cj=/[ \t\n\f\r]/g;function Ij(e){return typeof e=="object"?e.type==="text"?OA(e.value):!1:OA(e)}function OA(e){return e.replace(Cj,"")===""}class Ul{constructor(t,r,a){this.normal=r,this.property=t,a&&(this.space=a)}}Ul.prototype.normal={};Ul.prototype.property={};Ul.prototype.space=void 0;function ON(e,t){const r={},a={};for(const o of e)Object.assign(r,o.property),Object.assign(a,o.normal);return new Ul(r,a,t)}function kl(e){return e.toLowerCase()}class rn{con
Line truncated
|
||||
`,f="/",h="*",m="",v="comment",E="declaration";function x(k,A){if(typeof k!="string")throw new TypeError("First argument must be a string");if(!k)return[];A=A||{};var _=1,N=1;function D(re){var ee=re.match(t);ee&&(_+=ee.length);var M=re.lastIndexOf(p);N=~M?re.length-M:N+re.length}function F(){var re={line:_,column:N};return function(ee){return ee.position=new L(re),K(),ee}}function L(re){this.start=re,this.end={line:_,column:N},this.source=A.source}L.prototype.content=k;function z(re){var ee=new Error(A.source+":"+_+":"+N+": "+re);if(ee.reason=re,ee.filename=A.source,ee.line=_,ee.column=N,ee.source=k,!A.silent)throw ee}function $(re){var ee=re.exec(k);if(ee){var M=ee[0];return D(M),k=k.slice(M.length),ee}}function K(){$(r)}function j(re){var ee;for(re=re||[];ee=q();)ee!==!1&&re.push(ee);return re}function q(){var re=F();if(!(f!=k.charAt(0)||h!=k.charAt(1))){for(var ee=2;m!=k.charAt(ee)&&(h!=k.charAt(ee)||f!=k.charAt(ee+1));)++ee;if(ee+=2,m===k.charAt(ee-1))return z("End of comment missing");var M=k.slice(2,ee-2);return N+=2,D(M),k=k.slice(ee),N+=2,re({type:v,comment:M})}}function X(){var re=F(),ee=$(a);if(ee){if(q(),!$(o))return z("property missing ':'");var M=$(s),H=re({type:E,property:w(ee[0].replace(e,m)),value:M?w(M[0].replace(e,m)):m});return $(u),H}}function ne(){var re=[];j(re);for(var ee;ee=X();)ee!==!1&&(re.push(ee),j(re));return re}return K(),ne()}function w(k){return k?k.replace(c,m):m}return Rf=x,Rf}var UA;function $j(){if(UA)return zi;UA=1;var e=zi&&zi.__importDefault||function(a){return a&&a.__esModule?a:{default:a}};Object.defineProperty(zi,"__esModule",{value:!0}),zi.default=r;const t=e(Pj());function r(a,o){let s=null;if(!a||typeof a!="string")return s;const u=(0,t.default)(a),c=typeof o=="function";return u.forEach(p=>{if(p.type!=="declaration")return;const{property:f,value:h}=p;c?o(f,h,p):h&&(s=s||{},s[f]=h)}),s}return zi}var pl={},FA;function Gj(){if(FA)return pl;FA=1,Object.defineProperty(pl,"__esModule",{value:!0}),pl.camelCase=void 0;var e=/^--[a-zA-Z0-9_-]+$/,t=/-([a-z])/g,r=/^[^-]+$/,a=/^-(webkit|moz|ms|o|khtml)-/,o=/^-(ms)-/,s=function(f){return!f||r.test(f)||e.test(f)},u=function(f,h){return h.toUpperCase()},c=function(f,h){return"".concat(h,"-")},p=function(f,h){return h===void 0&&(h={}),s(f)?f:(f=f.toLowerCase(),h.reactCompat?f=f.replace(o,c):f=f.replace(a,c),f.replace(t,u))};return pl.camelCase=p,pl}var fl,BA;function Hj(){if(BA)return fl;BA=1;var e=fl&&fl.__importDefault||function(o){return o&&o.__esModule?o:{default:o}},t=e($j()),r=Gj();function a(o,s){var u={};return!o||typeof o!="string"||(0,t.default)(o,function(c,p){c&&p&&(u[(0,r.camelCase)(c,s)]=p)}),u}return a.default=a,fl=a,fl}var Vj=Hj();const qj=ac(Vj),PN=$N("end"),im=$N("start");function $N(e){return t;function t(r){const a=r&&r.position&&r.position[e]||{};if(typeof a.line=="number"&&a.line>0&&typeof a.column=="number"&&a.column>0)return{line:a.line,column:a.column,offset:typeof a.offset=="number"&&a.offset>-1?a.offset:void 0}}}function Yj(e){const t=im(e),r=PN(e);if(t&&r)return{start:t,end:r}}function El(e){return!e||typeof e!="object"?"":"position"in e||"type"in e?zA(e.position):"start"in e||"end"in e?zA(e):"line"in e||"column"in e?sg(e):""}function sg(e){return PA(e&&e.line)+":"+PA(e&&e.column)}function zA(e){return sg(e&&e.start)+"-"+sg(e&&e.end)}function PA(e){return e&&typeof e=="number"?e:1}class Ft extends Error{constructor(t,r,a){super(),typeof r=="string"&&(a=r,r=void 0);let o="",s={},u=!1;if(r&&("line"in r&&"column"in r?s={place:r}:"start"in r&&"end"in r?s={place:r}:"type"in r?s={ancestors:[r],place:r.position}:s={...r}),typeof t=="string"?o=t:!s.cause&&t&&(u=!0,o=t.message,s.cause=t),!s.ruleId&&!s.source&&typeof a=="string"){const p=a.indexOf(":");p===-1?s.ruleId=a:(s.source=a.slice(0,p),s.ruleId=a.slice(p+1))}if(!s.place&&s.ancestors&&s.ancestors){const p=s.ancestors[s.ancestors.length-1];p&&(s.place=p.position)}const c=s.place&&"start"in s.place?s.place.start:s.place;this.ancestors=s.ancestors||void 0,this.cause=s.cause||void 0,this.column=c?c.column:void 0,this.fatal=void 0,this.file="",this.message=o,this.line=c?c.line:void 0,this.name=El(s.place)||"1:1",this.place=s.place||void 0,this.reason=this.message,this.ruleId=s.ruleId||void 0,this.source=s.source||void 0,this.stack=u&&s.cause&&typeof s.cause.stack=="string"?s.cause.stack:"",this.actual=void 0,this.expected=void 0,this.note=void 0,this.url=void 0}}Ft.prototype.file="";Ft.prototype.name="";Ft.prototype.reason="";Ft.prototype.message="";Ft.prototype.stack="";Ft.prototype.column=void 0;Ft.prototype.line=void 0;Ft.prototype.ancestors=void 0;Ft.prototype.cause=void 0;Ft.prototype.fatal=void 0;Ft.prototype.place=void 0;Ft.prototype.ruleId=void 0;Ft.prototype.source=void 0;const om={}.hasOwnProperty,Wj=new Map,Kj=/[A-Z]/g,Xj=new Set(["table","tbody","thead","tfoot","tr"]),Zj=new Set(["td","th"]),GN="https://github.com/syntax-tree/hast-util-to-jsx-runtime";function Qj(e,t){if(!t||t.Fragment===void 0)throw new TypeError("Expected `Fragment` in options
Line truncated
|
||||
`;break}case-3:{u=`\r
|
||||
`;break}case-2:{u=t?" ":" ";break}case-1:{if(!t&&o)continue;u=" ";break}default:u=String.fromCharCode(s)}o=s===-2,a.push(u)}return a.join("")}function oB(e){const a={constructs:WN([nB,...(e||{}).extensions||[]]),content:o(SU),defined:[],document:o(wU),flow:o(PF),lazy:{},string:o(HF),text:o(VF)};return a;function o(s){return u;function u(c){return rB(a,s,c)}}}function lB(e){for(;!JN(e););return e}const ZA=/[\0\t\n\r]/g;function sB(){let e=1,t="",r=!0,a;return o;function o(s,u,c){const p=[];let f,h,m,v,E;for(s=t+(typeof s=="string"?s.toString():new TextDecoder(u||void 0).decode(s)),m=0,t="",r&&(s.charCodeAt(0)===65279&&m++,r=void 0);m<s.length;){if(ZA.lastIndex=m,f=ZA.exec(s),v=f&&f.index!==void 0?f.index:s.length,E=s.charCodeAt(v),!f){t=s.slice(m);break}if(E===10&&m===v&&a)p.push(-3),a=void 0;else switch(a&&(p.push(-5),a=void 0),m<v&&(p.push(s.slice(m,v)),e+=v-m),E){case 0:{p.push(65533),e++;break}case 9:{for(h=Math.ceil(e/4)*4,p.push(-2);e++<h;)p.push(-1);break}case 10:{p.push(-4),e=1;break}default:a=!0,e=1}m=v+1}return c&&(a&&p.push(-5),t&&p.push(t),p.push(null)),p}}const uB=/\\([!-/:-@[-`{-~])|&(#(?:\d{1,7}|x[\da-f]{1,6})|[\da-z]{1,31});/gi;function cB(e){return e.replace(uB,dB)}function dB(e,t,r){if(t)return t;if(r.charCodeAt(0)===35){const o=r.charCodeAt(1),s=o===120||o===88;return KN(r.slice(s?2:1),s?16:10)}return Nl(r)||e}const iR={}.hasOwnProperty;function pB(e,t,r){return t&&typeof t=="object"&&(r=t,t=void 0),fB(r)(lB(oB(r).document().write(sB()(e,t,!0))))}function fB(e){const t={transforms:[],canContainEols:["emphasis","fragment","heading","paragraph","strong"],enter:{autolink:s(Za),autolinkProtocol:K,autolinkEmail:K,atxHeading:s(Yn),blockQuote:s(ke),characterEscape:K,characterReference:K,codeFenced:s(Ie),codeFencedFenceInfo:u,codeFencedFenceMeta:u,codeIndented:s(Ie,u),codeText:s(qe,u),codeTextData:K,data:K,codeFlowValue:K,definition:s(dt),definitionDestinationString:u,definitionLabelString:u,definitionTitleString:u,emphasis:s(st),hardBreakEscape:s(Wn),hardBreakTrailing:s(Wn),htmlFlow:s(Ir,u),htmlFlowData:K,htmlText:s(Ir,u),htmlTextData:K,image:s(Jl),label:u,link:s(Za),listItem:s(bo),listItemValue:v,listOrdered:s(Qa,m),listUnordered:s(Qa),paragraph:s(Zc),reference:C,referenceString:u,resourceDestinationString:u,resourceTitleString:u,setextHeading:s(Yn),strong:s(Qc),thematicBreak:s(Jc)},exit:{atxHeading:p(),atxHeadingSequence:F,autolink:p(),autolinkEmail:fe,autolinkProtocol:he,blockQuote:p(),characterEscapeValue:j,characterReferenceMarkerHexadecimal:ae,characterReferenceMarkerNumeric:ae,characterReferenceValue:ue,characterReference:ie,codeFenced:p(k),codeFencedFence:w,codeFencedFenceInfo:E,codeFencedFenceMeta:x,codeFlowValue:j,codeIndented:p(A),codeText:p(ee),codeTextData:j,data:j,definition:p(),definitionDestinationString:D,definitionLabelString:_,definitionTitleString:N,emphasis:p(),hardBreakEscape:p(X),hardBreakTrailing:p(X),htmlFlow:p(ne),htmlFlowData:j,htmlText:p(re),htmlTextData:j,image:p(H),label:de,labelText:Q,lineEnding:q,link:p(M),listItem:p(),listOrdered:p(),listUnordered:p(),paragraph:p(),referenceString:J,resourceDestinationString:I,resourceTitleString:U,resource:B,setextHeading:p($),setextHeadingLineSequence:z,setextHeadingText:L,strong:p(),thematicBreak:p()}};oR(t,(e||{}).mdastExtensions||[]);const r={};return a;function a(le){let ge={type:"root",children:[]};const _e={stack:[ge],tokenStack:[],config:t,enter:c,exit:f,buffer:u,resume:h,data:r},Me=[];let We=-1;for(;++We<le.length;)if(le[We][1].type==="listOrdered"||le[We][1].type==="listUnordered")if(le[We][0]==="enter")Me.push(We);else{const an=Me.pop();We=o(le,an,We)}for(We=-1;++We<le.length;){const an=t[le[We][0]];iR.call(an,le[We][1].type)&&an[le[We][1].type].call(Object.assign({sliceSerialize:le[We][2].sliceSerialize},_e),le[We][1])}if(_e.tokenStack.length>0){const an=_e.tokenStack[_e.tokenStack.length-1];(an[1]||QA).call(_e,void 0,an[0])}for(ge.position={start:ia(le.length>0?le[0][1].start:{line:1,column:1,offset:0}),end:ia(le.length>0?le[le.length-2][1].end:{line:1,column:1,offset:0})},We=-1;++We<t.transforms.length;)ge=t.transforms[We](ge)||ge;return ge}function o(le,ge,_e){let Me=ge-1,We=-1,an=!1,lr,$t,Et,qt;for(;++Me<=_e;){const tt=le[Me];switch(tt[1].type){case"listUnordered":case"listOrdered":case"blockQuote":{tt[0]==="enter"?We++:We--,qt=void 0;break}case"lineEndingBlank":{tt[0]==="enter"&&(lr&&!qt&&!We&&!Et&&(Et=Me),qt=void 0);break}case"linePrefix":case"listItemValue":case"listItemMarker":case"listItemPrefix":case"listItemPrefixWhitespace":break;default:qt=void 0}if(!We&&tt[0]==="enter"&&tt[1].type==="listItemPrefix"||We===-1&&tt[0]==="exit"&&(tt[1].type==="listUnordered"||tt[1].type==="listOrdered")){if(lr){let Or=Me;for($t=void 0;Or--;){const Ln=le[Or];if(Ln[1].type==="lineEnding"||Ln[1].type==="lineEndingBlank"){if(Ln[0]==="exit")continue;$t&&(le[$t][1].type="lineEndingBlank",an=!0),Ln[1].type="lineEnding",$t=Or}else if(!(Ln[1].type==="linePrefix"||Ln[1].type==="blockQuotePrefix"||Ln[1].type==="blockQuotePrefixWhitespLine truncated
|
||||
`}]}function yB(e,t){const r=t.value?t.value+`
|
||||
`:"",a={},o=t.lang?t.lang.split(/\s+/):[];o.length>0&&(a.className=["language-"+o[0]]);let s={type:"element",tagName:"code",properties:a,children:[{type:"text",value:r}]};return t.meta&&(s.data={meta:t.meta}),e.patch(t,s),s=e.applyData(t,s),s={type:"element",tagName:"pre",properties:{},children:[s]},e.patch(t,s),s}function vB(e,t){const r={type:"element",tagName:"del",properties:{},children:e.all(t)};return e.patch(t,r),e.applyData(t,r)}function EB(e,t){const r={type:"element",tagName:"em",properties:{},children:e.all(t)};return e.patch(t,r),e.applyData(t,r)}function SB(e,t){const r=typeof e.options.clobberPrefix=="string"?e.options.clobberPrefix:"user-content-",a=String(t.identifier).toUpperCase(),o=ro(a.toLowerCase()),s=e.footnoteOrder.indexOf(a);let u,c=e.footnoteCounts.get(a);c===void 0?(c=0,e.footnoteOrder.push(a),u=e.footnoteOrder.length):u=s+1,c+=1,e.footnoteCounts.set(a,c);const p={type:"element",tagName:"a",properties:{href:"#"+r+"fn-"+o,id:r+"fnref-"+o+(c>1?"-"+c:""),dataFootnoteRef:!0,ariaDescribedBy:["footnote-label"]},children:[{type:"text",value:String(u)}]};e.patch(t,p);const f={type:"element",tagName:"sup",properties:{},children:[p]};return e.patch(t,f),e.applyData(t,f)}function xB(e,t){const r={type:"element",tagName:"h"+t.depth,properties:{},children:e.all(t)};return e.patch(t,r),e.applyData(t,r)}function wB(e,t){if(e.options.allowDangerousHtml){const r={type:"raw",value:t.value};return e.patch(t,r),e.applyData(t,r)}}function lR(e,t){const r=t.referenceType;let a="]";if(r==="collapsed"?a+="[]":r==="full"&&(a+="["+(t.label||t.identifier)+"]"),t.type==="imageReference")return[{type:"text",value:"!["+t.alt+a}];const o=e.all(t),s=o[0];s&&s.type==="text"?s.value="["+s.value:o.unshift({type:"text",value:"["});const u=o[o.length-1];return u&&u.type==="text"?u.value+=a:o.push({type:"text",value:a}),o}function TB(e,t){const r=String(t.identifier).toUpperCase(),a=e.definitionById.get(r);if(!a)return lR(e,t);const o={src:ro(a.url||""),alt:t.alt};a.title!==null&&a.title!==void 0&&(o.title=a.title);const s={type:"element",tagName:"img",properties:o,children:[]};return e.patch(t,s),e.applyData(t,s)}function AB(e,t){const r={src:ro(t.url)};t.alt!==null&&t.alt!==void 0&&(r.alt=t.alt),t.title!==null&&t.title!==void 0&&(r.title=t.title);const a={type:"element",tagName:"img",properties:r,children:[]};return e.patch(t,a),e.applyData(t,a)}function kB(e,t){const r={type:"text",value:t.value.replace(/\r?\n|\r/g," ")};e.patch(t,r);const a={type:"element",tagName:"code",properties:{},children:[r]};return e.patch(t,a),e.applyData(t,a)}function _B(e,t){const r=String(t.identifier).toUpperCase(),a=e.definitionById.get(r);if(!a)return lR(e,t);const o={href:ro(a.url||"")};a.title!==null&&a.title!==void 0&&(o.title=a.title);const s={type:"element",tagName:"a",properties:o,children:e.all(t)};return e.patch(t,s),e.applyData(t,s)}function NB(e,t){const r={href:ro(t.url)};t.title!==null&&t.title!==void 0&&(r.title=t.title);const a={type:"element",tagName:"a",properties:r,children:e.all(t)};return e.patch(t,a),e.applyData(t,a)}function RB(e,t,r){const a=e.all(t),o=r?CB(r):sR(t),s={},u=[];if(typeof t.checked=="boolean"){const h=a[0];let m;h&&h.type==="element"&&h.tagName==="p"?m=h:(m={type:"element",tagName:"p",properties:{},children:[]},a.unshift(m)),m.children.length>0&&m.children.unshift({type:"text",value:" "}),m.children.unshift({type:"element",tagName:"input",properties:{type:"checkbox",checked:t.checked,disabled:!0},children:[]}),s.className=["task-list-item"]}let c=-1;for(;++c<a.length;){const h=a[c];(o||c!==0||h.type!=="element"||h.tagName!=="p")&&u.push({type:"text",value:`
|
||||
`}),h.type==="element"&&h.tagName==="p"&&!o?u.push(...h.children):u.push(h)}const p=a[a.length-1];p&&(o||p.type!=="element"||p.tagName!=="p")&&u.push({type:"text",value:`
|
||||
`});const f={type:"element",tagName:"li",properties:s,children:u};return e.patch(t,f),e.applyData(t,f)}function CB(e){let t=!1;if(e.type==="list"){t=e.spread||!1;const r=e.children;let a=-1;for(;!t&&++a<r.length;)t=sR(r[a])}return t}function sR(e){const t=e.spread;return t??e.children.length>1}function IB(e,t){const r={},a=e.all(t);let o=-1;for(typeof t.start=="number"&&t.start!==1&&(r.start=t.start);++o<a.length;){const u=a[o];if(u.type==="element"&&u.tagName==="li"&&u.properties&&Array.isArray(u.properties.className)&&u.properties.className.includes("task-list-item")){r.className=["contains-task-list"];break}}const s={type:"element",tagName:t.ordered?"ol":"ul",properties:r,children:e.wrap(a,!0)};return e.patch(t,s),e.applyData(t,s)}function OB(e,t){const r={type:"element",tagName:"p",properties:{},children:e.all(t)};return e.patch(t,r),e.applyData(t,r)}function LB(e,t){const r={type:"root",children:e.wrap(e.all(t))};return e.patch(t,r),e.applyData(t,r)}function DB(e,t){const r={type:"element",tagName:"strong",properties:{},children:e.all(t)};return e.patch(t,r),e.applyData(t,r)}function MB(e,t){const r=e.all(t),a=r.shift(),o=[];if(a){const u={type:"element",tagName:"thead",properties:{},children:e.wrap([a],!0)};e.patch(t.children[0],u),o.push(u)}if(r.length>0){const u={type:"element",tagName:"tbody",properties:{},children:e.wrap(r,!0)},c=im(t.children[1]),p=PN(t.children[t.children.length-1]);c&&p&&(u.position={start:c,end:p}),o.push(u)}const s={type:"element",tagName:"table",properties:{},children:e.wrap(o,!0)};return e.patch(t,s),e.applyData(t,s)}function jB(e,t,r){const a=r?r.children:void 0,s=(a?a.indexOf(t):1)===0?"th":"td",u=r&&r.type==="table"?r.align:void 0,c=u?u.length:t.children.length;let p=-1;const f=[];for(;++p<c;){const m=t.children[p],v={},E=u?u[p]:void 0;E&&(v.align=E);let x={type:"element",tagName:s,properties:v,children:[]};m&&(x.children=e.all(m),e.patch(m,x),x=e.applyData(m,x)),f.push(x)}const h={type:"element",tagName:"tr",properties:{},children:e.wrap(f,!0)};return e.patch(t,h),e.applyData(t,h)}function UB(e,t){const r={type:"element",tagName:"td",properties:{},children:e.all(t)};return e.patch(t,r),e.applyData(t,r)}const JA=9,ek=32;function FB(e){const t=String(e),r=/\r?\n|\r/g;let a=r.exec(t),o=0;const s=[];for(;a;)s.push(tk(t.slice(o,a.index),o>0,!0),a[0]),o=a.index+a[0].length,a=r.exec(t);return s.push(tk(t.slice(o),o>0,!1)),s.join("")}function tk(e,t,r){let a=0,o=e.length;if(t){let s=e.codePointAt(a);for(;s===JA||s===ek;)a++,s=e.codePointAt(a)}if(r){let s=e.codePointAt(o-1);for(;s===JA||s===ek;)o--,s=e.codePointAt(o-1)}return o>a?e.slice(a,o):""}function BB(e,t){const r={type:"text",value:FB(String(t.value))};return e.patch(t,r),e.applyData(t,r)}function zB(e,t){const r={type:"element",tagName:"hr",properties:{},children:[]};return e.patch(t,r),e.applyData(t,r)}const PB={blockquote:hB,break:bB,code:yB,delete:vB,emphasis:EB,footnoteReference:SB,heading:xB,html:wB,imageReference:TB,image:AB,inlineCode:kB,linkReference:_B,link:NB,listItem:RB,list:IB,paragraph:OB,root:LB,strong:DB,table:MB,tableCell:UB,tableRow:jB,text:BB,thematicBreak:zB,toml:wu,yaml:wu,definition:wu,footnoteDefinition:wu};function wu(){}const uR=-1,hc=0,xl=1,Yu=2,dm=3,pm=4,fm=5,gm=6,cR=7,dR=8,nk=typeof self=="object"?self:globalThis,$B=(e,t)=>{const r=(o,s)=>(e.set(s,o),o),a=o=>{if(e.has(o))return e.get(o);const[s,u]=t[o];switch(s){case hc:case uR:return r(u,o);case xl:{const c=r([],o);for(const p of u)c.push(a(p));return c}case Yu:{const c=r({},o);for(const[p,f]of u)c[a(p)]=a(f);return c}case dm:return r(new Date(u),o);case pm:{const{source:c,flags:p}=u;return r(new RegExp(c,p),o)}case fm:{const c=r(new Map,o);for(const[p,f]of u)c.set(a(p),a(f));return c}case gm:{const c=r(new Set,o);for(const p of u)c.add(a(p));return c}case cR:{const{name:c,message:p}=u;return r(new nk[c](p),o)}case dR:return r(BigInt(u),o);case"BigInt":return r(Object(BigInt(u)),o);case"ArrayBuffer":return r(new Uint8Array(u).buffer,u);case"DataView":{const{buffer:c}=new Uint8Array(u);return r(new DataView(c),u)}}return r(new nk[s](u),o)};return a},rk=e=>$B(new Map,e)(0),Pi="",{toString:GB}={},{keys:HB}=Object,ml=e=>{const t=typeof e;if(t!=="object"||!e)return[hc,t];const r=GB.call(e).slice(8,-1);switch(r){case"Array":return[xl,Pi];case"Object":return[Yu,Pi];case"Date":return[dm,Pi];case"RegExp":return[pm,Pi];case"Map":return[fm,Pi];case"Set":return[gm,Pi];case"DataView":return[xl,r]}return r.includes("Array")?[xl,r]:r.includes("Error")?[cR,r]:[Yu,r]},Tu=([e,t])=>e===hc&&(t==="function"||t==="symbol"),VB=(e,t,r,a)=>{const o=(u,c)=>{const p=a.push(u)-1;return r.set(c,p),p},s=u=>{if(r.has(u))return r.get(u);let[c,p]=ml(u);switch(c){case hc:{let h=u;switch(p){case"bigint":c=dR,h=u.toString();break;case"function":case"symbol":if(e)throw new TypeError("unable to serialize "+p);h=null;break;case"undefined":return o([uR],u)}return o([c,h],u)}case xl:{if(p){let v=u;return p==="DataView"?v=new Uint8Array(u.buffer):p==="ArrayBuffer"&&(v=new Uint8Array(
Line truncated
|
||||
`},{type:"element",tagName:"ol",properties:{},children:e.wrap(c,!0)},{type:"text",value:`
|
||||
`}]}}const bc=(function(e){if(e==null)return QB;if(typeof e=="function")return yc(e);if(typeof e=="object")return Array.isArray(e)?KB(e):XB(e);if(typeof e=="string")return ZB(e);throw new Error("Expected function, string, or object as test")});function KB(e){const t=[];let r=-1;for(;++r<e.length;)t[r]=bc(e[r]);return yc(a);function a(...o){let s=-1;for(;++s<t.length;)if(t[s].apply(this,o))return!0;return!1}}function XB(e){const t=e;return yc(r);function r(a){const o=a;let s;for(s in e)if(o[s]!==t[s])return!1;return!0}}function ZB(e){return yc(t);function t(r){return r&&r.type===e}}function yc(e){return t;function t(r,a,o){return!!(JB(r)&&e.call(this,r,typeof a=="number"?a:void 0,o||void 0))}}function QB(){return!0}function JB(e){return e!==null&&typeof e=="object"&&"type"in e}const pR=[],ez=!0,dg=!1,tz="skip";function fR(e,t,r,a){let o;typeof t=="function"&&typeof r!="function"?(a=r,r=t):o=t;const s=bc(o),u=a?-1:1;c(e,void 0,[])();function c(p,f,h){const m=p&&typeof p=="object"?p:{};if(typeof m.type=="string"){const E=typeof m.tagName=="string"?m.tagName:typeof m.name=="string"?m.name:void 0;Object.defineProperty(v,"name",{value:"node ("+(p.type+(E?"<"+E+">":""))+")"})}return v;function v(){let E=pR,x,w,k;if((!t||s(p,f,h[h.length-1]||void 0))&&(E=nz(r(p,h)),E[0]===dg))return E;if("children"in p&&p.children){const A=p;if(A.children&&E[0]!==tz)for(w=(a?A.children.length:-1)+u,k=h.concat(A);w>-1&&w<A.children.length;){const _=A.children[w];if(x=c(_,w,k)(),x[0]===dg)return x;w=typeof x[1]=="number"?x[1]:w+u}}return E}}}function nz(e){return Array.isArray(e)?e:typeof e=="number"?[ez,e]:e==null?pR:[e]}function mm(e,t,r,a){let o,s,u;typeof t=="function"&&typeof r!="function"?(s=void 0,u=t,o=r):(s=t,u=r,o=a),fR(e,s,c,o);function c(p,f){const h=f[f.length-1],m=h?h.children.indexOf(p):void 0;return u(p,m,h)}}const pg={}.hasOwnProperty,rz={};function az(e,t){const r=t||rz,a=new Map,o=new Map,s=new Map,u={...PB,...r.handlers},c={all:f,applyData:oz,definitionById:a,footnoteById:o,footnoteCounts:s,footnoteOrder:[],handlers:u,one:p,options:r,patch:iz,wrap:sz};return mm(e,function(h){if(h.type==="definition"||h.type==="footnoteDefinition"){const m=h.type==="definition"?a:o,v=String(h.identifier).toUpperCase();m.has(v)||m.set(v,h)}}),c;function p(h,m){const v=h.type,E=c.handlers[v];if(pg.call(c.handlers,v)&&E)return E(c,h,m);if(c.options.passThrough&&c.options.passThrough.includes(v)){if("children"in h){const{children:w,...k}=h,A=Wu(k);return A.children=c.all(h),A}return Wu(h)}return(c.options.unknownHandler||lz)(c,h,m)}function f(h){const m=[];if("children"in h){const v=h.children;let E=-1;for(;++E<v.length;){const x=c.one(v[E],h);if(x){if(E&&v[E-1].type==="break"&&(!Array.isArray(x)&&x.type==="text"&&(x.value=ik(x.value)),!Array.isArray(x)&&x.type==="element")){const w=x.children[0];w&&w.type==="text"&&(w.value=ik(w.value))}Array.isArray(x)?m.push(...x):m.push(x)}}}return m}}function iz(e,t){e.position&&(t.position=Yj(e))}function oz(e,t){let r=t;if(e&&e.data){const a=e.data.hName,o=e.data.hChildren,s=e.data.hProperties;if(typeof a=="string")if(r.type==="element")r.tagName=a;else{const u="children"in r?r.children:[r];r={type:"element",tagName:a,properties:{},children:u}}r.type==="element"&&s&&Object.assign(r.properties,Wu(s)),"children"in r&&r.children&&o!==null&&o!==void 0&&(r.children=o)}return r}function lz(e,t){const r=t.data||{},a="value"in t&&!(pg.call(r,"hProperties")||pg.call(r,"hChildren"))?{type:"text",value:t.value}:{type:"element",tagName:"div",properties:{},children:e.all(t)};return e.patch(t,a),e.applyData(t,a)}function sz(e,t){const r=[];let a=-1;for(t&&r.push({type:"text",value:`
|
||||
`});++a<e.length;)a&&r.push({type:"text",value:`
|
||||
`}),r.push(e[a]);return t&&e.length>0&&r.push({type:"text",value:`
|
||||
`}),r}function ik(e){let t=0,r=e.charCodeAt(t);for(;r===9||r===32;)t++,r=e.charCodeAt(t);return e.slice(t)}function ok(e,t){const r=az(e,t),a=r.one(e,void 0),o=WB(r),s=Array.isArray(a)?{type:"root",children:a}:a||{type:"root",children:[]};return o&&s.children.push({type:"text",value:`
|
||||
`},o),s}function uz(e,t){return e&&"run"in e?async function(r,a){const o=ok(r,{file:a,...t});await e.run(o,a)}:function(r,a){return ok(r,{file:a,...e||t})}}function lk(e){if(e)throw e}var Lf,sk;function cz(){if(sk)return Lf;sk=1;var e=Object.prototype.hasOwnProperty,t=Object.prototype.toString,r=Object.defineProperty,a=Object.getOwnPropertyDescriptor,o=function(f){return typeof Array.isArray=="function"?Array.isArray(f):t.call(f)==="[object Array]"},s=function(f){if(!f||t.call(f)!=="[object Object]")return!1;var h=e.call(f,"constructor"),m=f.constructor&&f.constructor.prototype&&e.call(f.constructor.prototype,"isPrototypeOf");if(f.constructor&&!h&&!m)return!1;var v;for(v in f);return typeof v>"u"||e.call(f,v)},u=function(f,h){r&&h.name==="__proto__"?r(f,h.name,{enumerable:!0,configurable:!0,value:h.newValue,writable:!0}):f[h.name]=h.newValue},c=function(f,h){if(h==="__proto__")if(e.call(f,h)){if(a)return a(f,h).value}else return;return f[h]};return Lf=function p(){var f,h,m,v,E,x,w=arguments[0],k=1,A=arguments.length,_=!1;for(typeof w=="boolean"&&(_=w,w=arguments[1]||{},k=2),(w==null||typeof w!="object"&&typeof w!="function")&&(w={});k<A;++k)if(f=arguments[k],f!=null)for(h in f)m=c(w,h),v=c(f,h),w!==v&&(_&&v&&(s(v)||(E=o(v)))?(E?(E=!1,x=m&&o(m)?m:[]):x=m&&s(m)?m:{},u(w,{name:h,newValue:p(_,x,v)})):typeof v<"u"&&u(w,{name:h,newValue:v}));return w},Lf}var dz=cz();const Df=ac(dz);function fg(e){if(typeof e!="object"||e===null)return!1;const t=Object.getPrototypeOf(e);return(t===null||t===Object.prototype||Object.getPrototypeOf(t)===null)&&!(Symbol.toStringTag in e)&&!(Symbol.iterator in e)}function pz(){const e=[],t={run:r,use:a};return t;function r(...o){let s=-1;const u=o.pop();if(typeof u!="function")throw new TypeError("Expected function as last argument, not "+u);c(null,...o);function c(p,...f){const h=e[++s];let m=-1;if(p){u(p);return}for(;++m<o.length;)(f[m]===null||f[m]===void 0)&&(f[m]=o[m]);o=f,h?fz(h,c)(...f):u(null,...f)}}function a(o){if(typeof o!="function")throw new TypeError("Expected `middelware` to be a function, not "+o);return e.push(o),t}}function fz(e,t){let r;return a;function a(...u){const c=e.length>u.length;let p;c&&u.push(o);try{p=e.apply(this,u)}catch(f){const h=f;if(c&&r)throw h;return o(h)}c||(p&&p.then&&typeof p.then=="function"?p.then(s,o):p instanceof Error?o(p):s(p))}function o(u,...c){r||(r=!0,t(u,...c))}function s(u){o(null,u)}}const Jn={basename:gz,dirname:mz,extname:hz,join:bz,sep:"/"};function gz(e,t){if(t!==void 0&&typeof t!="string")throw new TypeError('"ext" argument must be a string');Bl(e);let r=0,a=-1,o=e.length,s;if(t===void 0||t.length===0||t.length>e.length){for(;o--;)if(e.codePointAt(o)===47){if(s){r=o+1;break}}else a<0&&(s=!0,a=o+1);return a<0?"":e.slice(r,a)}if(t===e)return"";let u=-1,c=t.length-1;for(;o--;)if(e.codePointAt(o)===47){if(s){r=o+1;break}}else u<0&&(s=!0,u=o+1),c>-1&&(e.codePointAt(o)===t.codePointAt(c--)?c<0&&(a=o):(c=-1,a=u));return r===a?a=u:a<0&&(a=e.length),e.slice(r,a)}function mz(e){if(Bl(e),e.length===0)return".";let t=-1,r=e.length,a;for(;--r;)if(e.codePointAt(r)===47){if(a){t=r;break}}else a||(a=!0);return t<0?e.codePointAt(0)===47?"/":".":t===1&&e.codePointAt(0)===47?"//":e.slice(0,t)}function hz(e){Bl(e);let t=e.length,r=-1,a=0,o=-1,s=0,u;for(;t--;){const c=e.codePointAt(t);if(c===47){if(u){a=t+1;break}continue}r<0&&(u=!0,r=t+1),c===46?o<0?o=t:s!==1&&(s=1):o>-1&&(s=-1)}return o<0||r<0||s===0||s===1&&o===r-1&&o===a+1?"":e.slice(o,r)}function bz(...e){let t=-1,r;for(;++t<e.length;)Bl(e[t]),e[t]&&(r=r===void 0?e[t]:r+"/"+e[t]);return r===void 0?".":yz(r)}function yz(e){Bl(e);const t=e.codePointAt(0)===47;let r=vz(e,!t);return r.length===0&&!t&&(r="."),r.length>0&&e.codePointAt(e.length-1)===47&&(r+="/"),t?"/"+r:r}function vz(e,t){let r="",a=0,o=-1,s=0,u=-1,c,p;for(;++u<=e.length;){if(u<e.length)c=e.codePointAt(u);else{if(c===47)break;c=47}if(c===47){if(!(o===u-1||s===1))if(o!==u-1&&s===2){if(r.length<2||a!==2||r.codePointAt(r.length-1)!==46||r.codePointAt(r.length-2)!==46){if(r.length>2){if(p=r.lastIndexOf("/"),p!==r.length-1){p<0?(r="",a=0):(r=r.slice(0,p),a=r.length-1-r.lastIndexOf("/")),o=u,s=0;continue}}else if(r.length>0){r="",a=0,o=u,s=0;continue}}t&&(r=r.length>0?r+"/..":"..",a=2)}else r.length>0?r+="/"+e.slice(o+1,u):r=e.slice(o+1,u),a=u-o-1;o=u,s=0}else c===46&&s>-1?s++:s=-1}return r}function Bl(e){if(typeof e!="string")throw new TypeError("Path must be a string. Received "+JSON.stringify(e))}const Ez={cwd:Sz};function Sz(){return"/"}function gg(e){return!!(e!==null&&typeof e=="object"&&"href"in e&&e.href&&"protocol"in e&&e.protocol&&e.auth===void 0)}function xz(e){if(typeof e=="string")e=new URL(e);else if(!gg(e)){const t=new TypeError('The "path" argument must be of type string or an instance of URL. Received `'+e+"`");throw t.code="ERR_INVALID_ARG_TYPE",t}if(e.protocol!=="file:"){const t=new TypeError("The URL must be of scheme file");throw t.code="ERR_INVALID_URL_SCHEME",t}return wz(e)}function wz(e){if(e.hostname!==""){consLine truncated
|
||||
`:" ")+s.indentLines(s.containerFlow(a,c.current()),t?yR:pP))),f(),p}}function pP(e,t,r){return t===0?e:yR(e,t,r)}function yR(e,t,r){return(r?"":" ")+e}const fP=["autolink","destinationLiteral","destinationRaw","reference","titleQuote","titleApostrophe"];vR.peek=yP;function gP(){return{canContainEols:["delete"],enter:{strikethrough:hP},exit:{strikethrough:bP}}}function mP(){return{unsafe:[{character:"~",inConstruct:"phrasing",notInConstruct:fP}],handlers:{delete:vR}}}function hP(e){this.enter({type:"delete",children:[]},e)}function bP(e){this.exit(e)}function vR(e,t,r,a){const o=r.createTracker(a),s=r.enter("strikethrough");let u=o.move("~~");return u+=r.containerPhrasing(e,{...o.current(),before:u,after:"~"}),u+=o.move("~~"),s(),u}function yP(){return"~"}function vP(e){return e.length}function EP(e,t){const r=t||{},a=(r.align||[]).concat(),o=r.stringLength||vP,s=[],u=[],c=[],p=[];let f=0,h=-1;for(;++h<e.length;){const w=[],k=[];let A=-1;for(e[h].length>f&&(f=e[h].length);++A<e[h].length;){const _=SP(e[h][A]);if(r.alignDelimiters!==!1){const N=o(_);k[A]=N,(p[A]===void 0||N>p[A])&&(p[A]=N)}w.push(_)}u[h]=w,c[h]=k}let m=-1;if(typeof a=="object"&&"length"in a)for(;++m<f;)s[m]=mk(a[m]);else{const w=mk(a);for(;++m<f;)s[m]=w}m=-1;const v=[],E=[];for(;++m<f;){const w=s[m];let k="",A="";w===99?(k=":",A=":"):w===108?k=":":w===114&&(A=":");let _=r.alignDelimiters===!1?1:Math.max(1,p[m]-k.length-A.length);const N=k+"-".repeat(_)+A;r.alignDelimiters!==!1&&(_=k.length+_+A.length,_>p[m]&&(p[m]=_),E[m]=_),v[m]=N}u.splice(1,0,v),c.splice(1,0,E),h=-1;const x=[];for(;++h<u.length;){const w=u[h],k=c[h];m=-1;const A=[];for(;++m<f;){const _=w[m]||"";let N="",D="";if(r.alignDelimiters!==!1){const F=p[m]-(k[m]||0),L=s[m];L===114?N=" ".repeat(F):L===99?F%2?(N=" ".repeat(F/2+.5),D=" ".repeat(F/2-.5)):(N=" ".repeat(F/2),D=N):D=" ".repeat(F)}r.delimiterStart!==!1&&!m&&A.push("|"),r.padding!==!1&&!(r.alignDelimiters===!1&&_==="")&&(r.delimiterStart!==!1||m)&&A.push(" "),r.alignDelimiters!==!1&&A.push(N),A.push(_),r.alignDelimiters!==!1&&A.push(D),r.padding!==!1&&A.push(" "),(r.delimiterEnd!==!1||m!==f-1)&&A.push("|")}x.push(r.delimiterEnd===!1?A.join("").replace(/ +$/,""):A.join(""))}return x.join(`
|
||||
`)}function SP(e){return e==null?"":String(e)}function mk(e){const t=typeof e=="string"?e.codePointAt(0):0;return t===67||t===99?99:t===76||t===108?108:t===82||t===114?114:0}function xP(e,t,r,a){const o=r.enter("blockquote"),s=r.createTracker(a);s.move("> "),s.shift(2);const u=r.indentLines(r.containerFlow(e,s.current()),wP);return o(),u}function wP(e,t,r){return">"+(r?"":" ")+e}function TP(e,t){return hk(e,t.inConstruct,!0)&&!hk(e,t.notInConstruct,!1)}function hk(e,t,r){if(typeof t=="string"&&(t=[t]),!t||t.length===0)return r;let a=-1;for(;++a<t.length;)if(e.includes(t[a]))return!0;return!1}function bk(e,t,r,a){let o=-1;for(;++o<r.unsafe.length;)if(r.unsafe[o].character===`
|
||||
`&&TP(r.stack,r.unsafe[o]))return/[ \t]/.test(a.before)?"":" ";return`\\
|
||||
`}function AP(e,t){const r=String(e);let a=r.indexOf(t),o=a,s=0,u=0;if(typeof t!="string")throw new TypeError("Expected substring");for(;a!==-1;)a===o?++s>u&&(u=s):s=1,o=a+t.length,a=r.indexOf(t,o);return u}function kP(e,t){return!!(t.options.fences===!1&&e.value&&!e.lang&&/[^ \r\n]/.test(e.value)&&!/^[\t ]*(?:[\r\n]|$)|(?:^|[\r\n])[\t ]*$/.test(e.value))}function _P(e){const t=e.options.fence||"`";if(t!=="`"&&t!=="~")throw new Error("Cannot serialize code with `"+t+"` for `options.fence`, expected `` ` `` or `~`");return t}function NP(e,t,r,a){const o=_P(r),s=e.value||"",u=o==="`"?"GraveAccent":"Tilde";if(kP(e,r)){const m=r.enter("codeIndented"),v=r.indentLines(s,RP);return m(),v}const c=r.createTracker(a),p=o.repeat(Math.max(AP(s,o)+1,3)),f=r.enter("codeFenced");let h=c.move(p);if(e.lang){const m=r.enter(`codeFencedLang${u}`);h+=c.move(r.safe(e.lang,{before:h,after:" ",encode:["`"],...c.current()})),m()}if(e.lang&&e.meta){const m=r.enter(`codeFencedMeta${u}`);h+=c.move(" "),h+=c.move(r.safe(e.meta,{before:h,after:`
|
||||
`,encode:["`"],...c.current()})),m()}return h+=c.move(`
|
||||
`),s&&(h+=c.move(s+`
|
||||
`)),h+=c.move(p),f(),h}function RP(e,t,r){return(r?"":" ")+e}function bm(e){const t=e.options.quote||'"';if(t!=='"'&&t!=="'")throw new Error("Cannot serialize title with `"+t+"` for `options.quote`, expected `\"`, or `'`");return t}function CP(e,t,r,a){const o=bm(r),s=o==='"'?"Quote":"Apostrophe",u=r.enter("definition");let c=r.enter("label");const p=r.createTracker(a);let f=p.move("[");return f+=p.move(r.safe(r.associationId(e),{before:f,after:"]",...p.current()})),f+=p.move("]: "),c(),!e.url||/[\0- \u007F]/.test(e.url)?(c=r.enter("destinationLiteral"),f+=p.move("<"),f+=p.move(r.safe(e.url,{before:f,after:">",...p.current()})),f+=p.move(">")):(c=r.enter("destinationRaw"),f+=p.move(r.safe(e.url,{before:f,after:e.title?" ":`
|
||||
`,...p.current()}))),c(),e.title&&(c=r.enter(`title${s}`),f+=p.move(" "+o),f+=p.move(r.safe(e.title,{before:f,after:o,...p.current()})),f+=p.move(o),c()),u(),f}function IP(e){const t=e.options.emphasis||"*";if(t!=="*"&&t!=="_")throw new Error("Cannot serialize emphasis with `"+t+"` for `options.emphasis`, expected `*`, or `_`");return t}function Rl(e){return"&#x"+e.toString(16).toUpperCase()+";"}function Ku(e,t,r){const a=Qi(e),o=Qi(t);return a===void 0?o===void 0?r==="_"?{inside:!0,outside:!0}:{inside:!1,outside:!1}:o===1?{inside:!0,outside:!0}:{inside:!1,outside:!0}:a===1?o===void 0?{inside:!1,outside:!1}:o===1?{inside:!0,outside:!0}:{inside:!1,outside:!1}:o===void 0?{inside:!1,outside:!1}:o===1?{inside:!0,outside:!1}:{inside:!1,outside:!1}}ER.peek=OP;function ER(e,t,r,a){const o=IP(r),s=r.enter("emphasis"),u=r.createTracker(a),c=u.move(o);let p=u.move(r.containerPhrasing(e,{after:o,before:c,...u.current()}));const f=p.charCodeAt(0),h=Ku(a.before.charCodeAt(a.before.length-1),f,o);h.inside&&(p=Rl(f)+p.slice(1));const m=p.charCodeAt(p.length-1),v=Ku(a.after.charCodeAt(0),m,o);v.inside&&(p=p.slice(0,-1)+Rl(m));const E=u.move(o);return s(),r.attentionEncodeSurroundingInfo={after:v.outside,before:h.outside},c+p+E}function OP(e,t,r){return r.options.emphasis||"*"}function LP(e,t){let r=!1;return mm(e,function(a){if("value"in a&&/\r?\n|\r/.test(a.value)||a.type==="break")return r=!0,dg}),!!((!e.depth||e.depth<3)&&um(e)&&(t.options.setext||r))}function DP(e,t,r,a){const o=Math.max(Math.min(6,e.depth||1),1),s=r.createTracker(a);if(LP(e,r)){const h=r.enter("headingSetext"),m=r.enter("phrasing"),v=r.containerPhrasing(e,{...s.current(),before:`
|
||||
`,after:`
|
||||
`});return m(),h(),v+`
|
||||
`+(o===1?"=":"-").repeat(v.length-(Math.max(v.lastIndexOf("\r"),v.lastIndexOf(`
|
||||
`))+1))}const u="#".repeat(o),c=r.enter("headingAtx"),p=r.enter("phrasing");s.move(u+" ");let f=r.containerPhrasing(e,{before:"# ",after:`
|
||||
`,...s.current()});return/^[\t ]/.test(f)&&(f=Rl(f.charCodeAt(0))+f.slice(1)),f=f?u+" "+f:u,r.options.closeAtx&&(f+=" "+u),p(),c(),f}SR.peek=MP;function SR(e){return e.value||""}function MP(){return"<"}xR.peek=jP;function xR(e,t,r,a){const o=bm(r),s=o==='"'?"Quote":"Apostrophe",u=r.enter("image");let c=r.enter("label");const p=r.createTracker(a);let f=p.move("![");return f+=p.move(r.safe(e.alt,{before:f,after:"]",...p.current()})),f+=p.move("]("),c(),!e.url&&e.title||/[\0- \u007F]/.test(e.url)?(c=r.enter("destinationLiteral"),f+=p.move("<"),f+=p.move(r.safe(e.url,{before:f,after:">",...p.current()})),f+=p.move(">")):(c=r.enter("destinationRaw"),f+=p.move(r.safe(e.url,{before:f,after:e.title?" ":")",...p.current()}))),c(),e.title&&(c=r.enter(`title${s}`),f+=p.move(" "+o),f+=p.move(r.safe(e.title,{before:f,after:o,...p.current()})),f+=p.move(o),c()),f+=p.move(")"),u(),f}function jP(){return"!"}wR.peek=UP;function wR(e,t,r,a){const o=e.referenceType,s=r.enter("imageReference");let u=r.enter("label");const c=r.createTracker(a);let p=c.move("![");const f=r.safe(e.alt,{before:p,after:"]",...c.current()});p+=c.move(f+"]["),u();const h=r.stack;r.stack=[],u=r.enter("reference");const m=r.safe(r.associationId(e),{before:p,after:"]",...c.current()});return u(),r.stack=h,s(),o==="full"||!f||f!==m?p+=c.move(m+"]"):o==="shortcut"?p=p.slice(0,-1):p+=c.move("]"),p}function UP(){return"!"}TR.peek=FP;function TR(e,t,r){let a=e.value||"",o="`",s=-1;for(;new RegExp("(^|[^`])"+o+"([^`]|$)").test(a);)o+="`";for(/[^ \r\n]/.test(a)&&(/^[ \r\n]/.test(a)&&/[ \r\n]$/.test(a)||/^`|`$/.test(a))&&(a=" "+a+" ");++s<r.unsafe.length;){const u=r.unsafe[s],c=r.compilePattern(u);let p;if(u.atBreak)for(;p=c.exec(a);){let f=p.index;a.charCodeAt(f)===10&&a.charCodeAt(f-1)===13&&f--,a=a.slice(0,f)+" "+a.slice(p.index+1)}}return o+a+o}function FP(){return"`"}function AR(e,t){const r=um(e);return!!(!t.options.resourceLink&&e.url&&!e.title&&e.children&&e.children.length===1&&e.children[0].type==="text"&&(r===e.url||"mailto:"+r===e.url)&&/^[a-z][a-z+.-]+:/i.test(e.url)&&!/[\0- <>\u007F]/.test(e.url))}kR.peek=BP;function kR(e,t,r,a){const o=bm(r),s=o==='"'?"Quote":"Apostrophe",u=r.createTracker(a);let c,p;if(AR(e,r)){const h=r.stack;r.stack=[],c=r.enter("autolink");let m=u.move("<");return m+=u.move(r.containerPhrasing(e,{before:m,after:">",...u.current()})),m+=u.move(">"),c(),r.stack=h,m}c=r.enter("link"),p=r.enter("label");let f=u.move("[");return f+=u.move(r.containerPhrasing(e,{before:f,after:"](",...u.current()})),f+=u.move("]("),p(),!e.url&&e.title||/[\0- \u007F]/.test(e.url)?(p=r.enter("destinationLiteral"),f+=u.move("<"),f+=u.move(r.safe(e.url,{before:f,after:">",...u.current()})),f+=u.move(">")):(p=r.enter("destinationRaw"),f+=u.move(r.safe(e.url,{before:f,after:e.title?" ":")",...u.current()}))),p(),e.title&&(p=r.enter(`title${s}`),f+=u.move(" "+o),f+=u.move(r.safe(e.title,{before:f,after:o,...u.current()})),f+=u.move(o),p()),f+=u.move(")"),c(),f}function BP(e,t,r){return AR(e,r)?"<":"["}_R.peek=zP;function _R(e,t,r,a){const o=e.referenceType,s=r.enter("linkReference");let u=r.enter("label");const c=r.createTracker(a);let p=c.move("[");const f=r.containerPhrasing(e,{before:p,after:"]",...c.current()});p+=c.move(f+"]["),u();const h=r.stack;r.stack=[],u=r.enter("reference");const m=r.safe(r.associationId(e),{before:p,after:"]",...c.current()});return u(),r.stack=h,s(),o==="full"||!f||f!==m?p+=c.move(m+"]"):o==="shortcut"?p=p.slice(0,-1):p+=c.move("]"),p}function zP(){return"["}function ym(e){const t=e.options.bullet||"*";if(t!=="*"&&t!=="+"&&t!=="-")throw new Error("Cannot serialize items with `"+t+"` for `options.bullet`, expected `*`, `+`, or `-`");return t}function PP(e){const t=ym(e),r=e.options.bulletOther;if(!r)return t==="*"?"-":"*";if(r!=="*"&&r!=="+"&&r!=="-")throw new Error("Cannot serialize items with `"+r+"` for `options.bulletOther`, expected `*`, `+`, or `-`");if(r===t)throw new Error("Expected `bullet` (`"+t+"`) and `bulletOther` (`"+r+"`) to be different");return r}function $P(e){const t=e.options.bulletOrdered||".";if(t!=="."&&t!==")")throw new Error("Cannot serialize items with `"+t+"` for `options.bulletOrdered`, expected `.` or `)`");return t}function NR(e){const t=e.options.rule||"*";if(t!=="*"&&t!=="-"&&t!=="_")throw new Error("Cannot serialize rules with `"+t+"` for `options.rule`, expected `*`, `-`, or `_`");return t}function GP(e,t,r,a){const o=r.enter("list"),s=r.bulletCurrent;let u=e.ordered?$P(r):ym(r);const c=e.ordered?u==="."?")":".":PP(r);let p=t&&r.bulletLastUsed?u===r.bulletLastUsed:!1;if(!e.ordered){const h=e.children?e.children[0]:void 0;if((u==="*"||u==="-")&&h&&(!h.children||!h.children[0])&&r.stack[r.stack.length-1]==="list"&&r.stack[r.stack.length-2]==="listItem"&&r.stack[r.stack.length-3]==="list"&&r.stack[r.stack.length-4]==="listItem"&&r.indexStack[r.indexStack.length-1]===0&&r.indexStack[r.indexStack.length-2]===0&&r.indexStack[r.indexStack.length-3]===0&&(p=!0),NR(r)===u&&h){let m=-1;for(;++m<e.chilLine truncated
|
||||
`,inConstruct:"tableCell"},{atBreak:!0,character:"|",after:"[ :-]"},{character:"|",inConstruct:"tableCell"},{atBreak:!0,character:":",after:"-"},{atBreak:!0,character:"-",after:"[:|-]"}],handlers:{inlineCode:v,table:u,tableCell:p,tableRow:c}};function u(E,x,w,k){return f(h(E,w,k),E.align)}function c(E,x,w,k){const A=m(E,w,k),_=f([A]);return _.slice(0,_.indexOf(`
|
||||
`))}function p(E,x,w,k){const A=w.enter("tableCell"),_=w.enter("phrasing"),N=w.containerPhrasing(E,{...k,before:s,after:s});return _(),A(),N}function f(E,x){return EP(E,{align:x,alignDelimiters:a,padding:r,stringLength:o})}function h(E,x,w){const k=E.children;let A=-1;const _=[],N=x.enter("table");for(;++A<k.length;)_[A]=m(k[A],x,w);return N(),_}function m(E,x,w){const k=E.children;let A=-1;const _=[],N=x.enter("tableRow");for(;++A<k.length;)_[A]=p(k[A],E,x,w);return N(),_}function v(E,x,w){let k=CR.inlineCode(E,x,w);return w.stack.includes("tableCell")&&(k=k.replace(/\|/g,"\\$&")),k}}function l$(){return{exit:{taskListCheckValueChecked:vk,taskListCheckValueUnchecked:vk,paragraph:u$}}}function s$(){return{unsafe:[{atBreak:!0,character:"-",after:"[:|-]"}],handlers:{listItem:c$}}}function vk(e){const t=this.stack[this.stack.length-2];t.type,t.checked=e.type==="taskListCheckValueChecked"}function u$(e){const t=this.stack[this.stack.length-2];if(t&&t.type==="listItem"&&typeof t.checked=="boolean"){const r=this.stack[this.stack.length-1];r.type;const a=r.children[0];if(a&&a.type==="text"){const o=t.children;let s=-1,u;for(;++s<o.length;){const c=o[s];if(c.type==="paragraph"){u=c;break}}u===r&&(a.value=a.value.slice(1),a.value.length===0?r.children.shift():r.position&&a.position&&typeof a.position.start.offset=="number"&&(a.position.start.column++,a.position.start.offset++,r.position.start=Object.assign({},a.position.start)))}}this.exit(e)}function c$(e,t,r,a){const o=e.children[0],s=typeof e.checked=="boolean"&&o&&o.type==="paragraph",u="["+(e.checked?"x":" ")+"] ",c=r.createTracker(a);s&&c.move(u);let p=CR.listItem(e,t,r,{...a,...c.current()});return s&&(p=p.replace(/^(?:[*+-]|\d+\.)([\r\n]| {1,3})/,f)),p;function f(h){return h+u}}function d$(){return[Gz(),cP(),gP(),e$(),l$()]}function p$(e){return{extensions:[Hz(),dP(e),mP(),o$(e),s$()]}}const f$={tokenize:v$,partial:!0},IR={tokenize:E$,partial:!0},OR={tokenize:S$,partial:!0},LR={tokenize:x$,partial:!0},g$={tokenize:w$,partial:!0},DR={name:"wwwAutolink",tokenize:b$,previous:jR},MR={name:"protocolAutolink",tokenize:y$,previous:UR},Rr={name:"emailAutolink",tokenize:h$,previous:FR},ar={};function m$(){return{text:ar}}let ja=48;for(;ja<123;)ar[ja]=Rr,ja++,ja===58?ja=65:ja===91&&(ja=97);ar[43]=Rr;ar[45]=Rr;ar[46]=Rr;ar[95]=Rr;ar[72]=[Rr,MR];ar[104]=[Rr,MR];ar[87]=[Rr,DR];ar[119]=[Rr,DR];function h$(e,t,r){const a=this;let o,s;return u;function u(m){return!mg(m)||!FR.call(a,a.previous)||vm(a.events)?r(m):(e.enter("literalAutolink"),e.enter("literalAutolinkEmail"),c(m))}function c(m){return mg(m)?(e.consume(m),c):m===64?(e.consume(m),p):r(m)}function p(m){return m===46?e.check(g$,h,f)(m):m===45||m===95||Mt(m)?(s=!0,e.consume(m),p):h(m)}function f(m){return e.consume(m),o=!0,p}function h(m){return s&&o&&Vt(a.previous)?(e.exit("literalAutolinkEmail"),e.exit("literalAutolink"),t(m)):r(m)}}function b$(e,t,r){const a=this;return o;function o(u){return u!==87&&u!==119||!jR.call(a,a.previous)||vm(a.events)?r(u):(e.enter("literalAutolink"),e.enter("literalAutolinkWww"),e.check(f$,e.attempt(IR,e.attempt(OR,s),r),r)(u))}function s(u){return e.exit("literalAutolinkWww"),e.exit("literalAutolink"),t(u)}}function y$(e,t,r){const a=this;let o="",s=!1;return u;function u(m){return(m===72||m===104)&&UR.call(a,a.previous)&&!vm(a.events)?(e.enter("literalAutolink"),e.enter("literalAutolinkHttp"),o+=String.fromCodePoint(m),e.consume(m),c):r(m)}function c(m){if(Vt(m)&&o.length<5)return o+=String.fromCodePoint(m),e.consume(m),c;if(m===58){const v=o.toLowerCase();if(v==="http"||v==="https")return e.consume(m),p}return r(m)}function p(m){return m===47?(e.consume(m),s?f:(s=!0,p)):r(m)}function f(m){return m===null||qu(m)||rt(m)||$a(m)||gc(m)?r(m):e.attempt(IR,e.attempt(OR,h),r)(m)}function h(m){return e.exit("literalAutolinkHttp"),e.exit("literalAutolink"),t(m)}}function v$(e,t,r){let a=0;return o;function o(u){return(u===87||u===119)&&a<3?(a++,e.consume(u),o):u===46&&a===3?(e.consume(u),s):r(u)}function s(u){return u===null?r(u):t(u)}}function E$(e,t,r){let a,o,s;return u;function u(f){return f===46||f===95?e.check(LR,p,c)(f):f===null||rt(f)||$a(f)||f!==45&&gc(f)?p(f):(s=!0,e.consume(f),u)}function c(f){return f===95?a=!0:(o=a,a=void 0),e.consume(f),u}function p(f){return o||a||!s?r(f):t(f)}}function S$(e,t){let r=0,a=0;return o;function o(u){return u===40?(r++,e.consume(u),o):u===41&&a<r?s(u):u===33||u===34||u===38||u===39||u===41||u===42||u===44||u===46||u===58||u===59||u===60||u===63||u===93||u===95||u===126?e.check(LR,t,s)(u):u===null||rt(u)||$a(u)?t(u):(e.consume(u),o)}function s(u){return u===41&&a++,e.consume(u),o}}function x$(e,t,r){return a;function a(c){return c===33||c===34||c===39||c===41||c===42||c===44||c===46||c===58||c===59||c===63||c===95||c===126?(e.consume(c),a):c===38?(e.consume(c),s):c===93?(e.consume(c),o):c===60||c===null||rt(c)||$a(c)?t(c):r(c)}function o(c){return c===null||c===40||c===91||rt(c)||$a(c)?t(c):a(c)}function s(c){return Vt(c)?u(c):r(c)}function u(c){re
Line truncated
|
||||
`),r=[];let a=0;for(;a<t.length;){const o=t[a].match(/^===\s+"([^"]+)"\s*$/);if(!o){r.push(t[a]),a++;continue}const s=o[1];for(a++;a<t.length&&t[a].trim()==="";)a++;const u=[];for(;a<t.length&&!(/^===\s+"[^"]+"\s*$/.test(t[a])||t[a].trim()===""&&a+1<t.length&&/^===\s+"[^"]+"\s*$/.test(t[a+1]));){if(t[a].startsWith(" ")||t[a].trimStart().startsWith("```")){u.push(t[a].replace(/^ /,"")),a++;continue}if(u.length>0&&t[a].trim()===""){u.push(""),a++;continue}break}r.push(`### ${zR}${s}`),r.push(""),r.push(...u),r.push("")}return r.join(`
|
||||
`)}function X$(e){let t=W$(e);return t=Y$(t),t=K$(t),t}function Sk(e){return e.startsWith(zR)||e.startsWith("tab:")}function Z$(e){return e.replace(/^\u200Btab:/,"").replace(/^tab:/,"")}function Q$(e){const t=new Set;e.matches("article.prose, main .prose, .prose, main.flex-1, #main-content")&&t.add(e),e.querySelectorAll("article.prose, main .prose, .prose").forEach(r=>t.add(r));for(const r of t)J$(r),e3(r),t3(r)}function J$(e){for(const t of e.querySelectorAll("p")){const r=t.textContent?.trim()??"";(/^<\/?div[\s>]/.test(r)||r==="</div>")&&t.remove()}}function e3(e){for(const t of e.querySelectorAll("ul")){if(t.classList.contains("aiui-feature-grid"))continue;const r=t.querySelector("li");if(r?.textContent?.includes("YAML-Driven")||r?.textContent?.includes("Component Slots")||r?.querySelector("strong")){t.classList.add("aiui-feature-grid","grid","grid-cols-1","md:grid-cols-2","gap-4","my-6","list-none","pl-0");for(const o of t.querySelectorAll("li"))o.classList.add("border","border-border","rounded-lg","p-4","bg-card")}}}function t3(e){if(e.querySelector("[data-aiui-tabs]"))return;const t=Array.from(e.querySelectorAll("h3")).filter(c=>Sk(c.textContent?.trim()??""));if(t.length<2)return;const r=t.map(c=>{const p=Z$(c.textContent?.trim()??""),f=[];let h=c.nextSibling;for(;h;){if(h.nodeType===Node.ELEMENT_NODE){const v=h;if(v.tagName==="H3"&&Sk(v.textContent?.trim()??"")||["H1","H2"].includes(v.tagName))break}const m=h.nextSibling;f.push(h),h=m}return{heading:c,title:p,nodes:f}}),a=document.createElement("div");a.className="aiui-tabs my-6 border border-border rounded-lg overflow-hidden",a.dataset.aiuiTabs="true";const o=document.createElement("div");o.className="aiui-tab-buttons flex flex-wrap gap-0 border-b border-border bg-muted/30",a.appendChild(o);const s=document.createElement("div");s.className="aiui-tab-panels",a.appendChild(s),r.forEach((c,p)=>{const f=document.createElement("button");f.type="button",f.className=["aiui-tab-button px-4 py-2 text-sm font-medium transition-colors",p===0?"bg-background text-foreground border-b-2 border-primary":"text-muted-foreground hover:text-foreground"].join(" "),f.textContent=c.title,f.dataset.tabIndex=String(p),o.appendChild(f);const h=document.createElement("div");h.className=p===0?"aiui-tab-panel p-4 block":"aiui-tab-panel p-4 hidden",h.dataset.tabPanel=String(p);for(const m of c.nodes)h.appendChild(m);s.appendChild(h)});const u=r[0].heading;e.insertBefore(a,u);for(const c of r)c.heading.remove();o.addEventListener("click",c=>{const p=c.target.closest(".aiui-tab-button");if(!p)return;const f=p.dataset.tabIndex??"0";for(const h of o.querySelectorAll(".aiui-tab-button")){const m=h.dataset.tabIndex===f;h.classList.toggle("bg-background",m),h.classList.toggle("text-foreground",m),h.classList.toggle("border-b-2",m),h.classList.toggle("border-primary",m),h.classList.toggle("text-muted-foreground",!m)}for(const h of s.querySelectorAll(".aiui-tab-panel"))h.classList.toggle("hidden",h.dataset.tabPanel!==f),h.classList.toggle("block",h.dataset.tabPanel===f)})}function n3(e){return e.toLowerCase().replace(/[^\w\s-]/g,"").replace(/\s+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"")}function Rn({className:e,...t}){return g.jsx("div",{"data-slot":"card",className:me("bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",e),...t})}function zn({className:e,...t}){return g.jsx("div",{"data-slot":"card-header",className:me("@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",e),...t})}function Pn({className:e,...t}){return g.jsx("div",{"data-slot":"card-title",className:me("leading-none font-semibold",e),...t})}function Em({className:e,...t}){return g.jsx("div",{"data-slot":"card-description",className:me("text-muted-foreground text-sm",e),...t})}function Cn({className:e,...t}){return g.jsx("div",{"data-slot":"card-content",className:me("px-6",e),...t})}function r3({className:e,...t}){return g.jsx("div",{"data-slot":"card-footer",className:me("flex items-center px-6 [.border-t]:pt-6",e),...t})}function a3({props:e}){const t=e?.title||"Statistics",r=e?.value||"0",a=e?.change||"",o=e?.changeType||"positive";return g.jsxs(Rn,{className:"mb-4",children:[g.jsxs(zn,{className:"pb-2",children:[g.jsx(Em,{children:t}),g.jsx(Pn,{className:"text-2xl",children:r})]}),a&&g.jsx(Cn,{className:"pt-0",children:g.jsx("span",{className:o==="positive"?"text-green-500 text-sm":"text-red-500 text-sm",children:a})})]})}function i3({props:e}){const t=e?.title||"Quick Links",r=e?.links||[];return g.jsxs(Rn,{className:"mb-4",children:[g.jsx(zn,{className:"pb-2",children:g.jsx(Pn,{className:"text-sm font-medium",children:t})}),g.jsx(Cn,{className:"space-y-1",children:r.map((a,o)=>g.jsx("a",{href:a.href,className:"block text-sm text-muted-foreground hover:text-foreground transition-colors",children:a.label},o))})]})}function o3({props:e}){const t=e?.title||"Newsletter",r=e?.buttonT
Line truncated
|
||||
`)===-1?g.jsx("code",{className:"bg-primary/10 text-primary px-1.5 py-0.5 rounded text-sm font-mono",children:v}):g.jsx("code",{className:`block font-mono text-foreground ${m??""}`,children:v}),pre:({children:m})=>g.jsx("pre",{className:"bg-muted text-foreground border border-border p-4 rounded-lg text-sm overflow-auto my-4 font-mono whitespace-pre",children:m}),table:({children:m})=>g.jsx("div",{className:"overflow-auto my-4",children:g.jsx("table",{className:"w-full border-collapse text-sm",children:m})}),thead:({children:m})=>g.jsx("thead",{className:"bg-muted/50",children:m}),tr:({children:m})=>g.jsx("tr",{className:"border-b",children:m}),th:({children:m})=>g.jsx("th",{className:"px-4 py-2 text-left font-medium",children:m}),td:({children:m})=>g.jsx("td",{className:"px-4 py-2 text-muted-foreground",children:m}),hr:()=>g.jsx("hr",{className:"my-6 border-border"}),strong:({children:m})=>g.jsx("strong",{className:"font-semibold text-foreground",children:m}),em:({children:m})=>g.jsx("em",{children:m})};return r?g.jsxs("main",{id:"main-content",className:"flex-1 p-8 max-w-3xl",children:[g.jsx(h3,{selectedItem:r}),s?g.jsx("div",{className:"text-muted-foreground",children:"Loading content..."}):a?g.jsx("article",{ref:c,className:"prose prose-neutral dark:prose-invert max-w-none prose-pre:bg-muted prose-pre:text-foreground prose-code:text-foreground",children:g.jsx(mR,{remarkPlugins:[BR],components:h,children:a})}):g.jsx("div",{className:"bg-muted/50 border rounded-lg p-6",children:g.jsxs("p",{className:"text-muted-foreground",children:["Content for ",g.jsx("strong",{className:"text-primary",children:r.title})," would be displayed here."]})})]}):g.jsxs("main",{className:"flex-1 p-8 max-w-3xl",children:[g.jsx("h1",{className:"text-4xl font-bold tracking-tight mb-4",children:e.site?.title||"Documentation"}),g.jsx("p",{className:"text-muted-foreground text-lg mb-8",children:e.site?.description||"Welcome to the documentation."}),g.jsx(qi,{className:"my-8"}),g.jsx("h2",{className:"text-2xl font-semibold mb-4 text-primary",children:"Theme Configuration"}),g.jsxs("div",{className:"bg-muted/50 border rounded-lg p-4 mb-6",children:[g.jsx("p",{className:"text-sm text-muted-foreground mb-2",children:g.jsx("strong",{children:"Current theme from YAML:"})}),g.jsx("pre",{className:"bg-muted text-foreground border border-border p-3 rounded-lg text-sm overflow-auto",children:`site:
|
||||
theme:
|
||||
preset: "${p?.preset||"zinc"}"
|
||||
radius: "${p?.radius||"md"}"
|
||||
darkMode: ${p?.darkMode!==!1}`})]}),g.jsx("h3",{className:"text-lg font-medium mb-3",children:"Available Presets"}),g.jsx("div",{className:"flex flex-wrap gap-2 mb-6",children:Object.keys(BL).map(m=>g.jsx("span",{className:`px-3 py-1 rounded-full text-sm ${m===(p?.preset||"zinc")?"bg-primary text-primary-foreground":"bg-muted text-muted-foreground"}`,children:m},m))}),g.jsxs("p",{className:"text-muted-foreground mb-4",children:[g.jsx("strong",{children:"Click any item in the sidebar"})," to navigate."]}),g.jsx("h2",{className:"text-2xl font-semibold mt-8 mb-4",children:"Routes"}),g.jsxs("p",{className:"text-muted-foreground mb-4",children:[g.jsx("span",{className:"font-medium text-primary",children:t.routes?.length||0})," routes configured."]}),g.jsx("pre",{className:"bg-muted text-foreground border border-border p-4 rounded-lg text-sm overflow-auto",children:JSON.stringify(t.routes?.slice(0,3),null,2)})]})}function _u({config:e,templateKey:t="docs"}){const r=e.templates?.[t]?.slots?.footer,a=r?.ref,s=(a?e.components?.[a]:e.components?.footer)?.props||r?.props;return g.jsx("footer",{className:"border-t border-border/50 py-8 px-6 bg-muted/20",children:g.jsxs("div",{className:"max-w-screen-2xl mx-auto flex flex-col sm:flex-row justify-between items-center gap-4 text-sm text-muted-foreground",children:[g.jsxs("div",{className:"flex items-center gap-2",children:[g.jsx("div",{className:"w-5 h-5 rounded bg-gradient-to-br from-primary/60 to-primary flex items-center justify-center",children:g.jsx("span",{className:"text-primary-foreground text-[8px] font-bold",children:"AI"})}),g.jsx("span",{children:s?.text||`© ${new Date().getFullYear()} PraisonAIUI`})]}),g.jsx("nav",{className:"flex items-center gap-6",children:s?.links?.map(u=>g.jsx("a",{href:u.href,className:"hover:text-foreground transition-colors",children:u.label},u.href))})]})})}const b3=()=>({taskLists:[],collapsed:!1}),Bn=new Map,hg=100,y3=1440*60*1e3;function v3(){const e=Date.now(),t=Array.from(Bn.entries());for(const[r,a]of t)e-a.lastAccessed>y3&&Bn.delete(r);if(Bn.size>hg){const r=Array.from(Bn.entries()).sort(([,o],[,s])=>o.lastAccessed-s.lastAccessed),a=Bn.size-hg;for(let o=0;o<a;o++)Bn.delete(r[o][0])}}function bg(e){if(!Bn.has(e))Bn.size>=hg&&v3(),Bn.set(e,{state:b3(),listeners:new Set,lastAccessed:Date.now()});else{const t=Bn.get(e);t.lastAccessed=Date.now()}return Bn.get(e)}function Sm(e,t){const r=bg(e),a=t(r.state);r.state=a,r.listeners.forEach(o=>o())}function E3(e,t){Sm(e,r=>{const a=r.taskLists.filter(s=>s.id!==t.task_list_id),o={id:t.task_list_id,name:t.name,tasks:t.tasks,stats:GR(t.tasks),sequence:t.sequence,created_at:t.timestamp,updated_at:t.timestamp};return{...r,taskLists:[...a,o],collapsed:r.taskLists.length===0?t.tasks.length<=1:r.collapsed}})}function S3(e,t){Sm(e,r=>{const a=r.taskLists.map(o=>o.id===t.task_list_id?{...o,tasks:t.tasks,stats:GR(t.tasks),sequence:t.sequence,updated_at:t.timestamp}:o);return{...r,taskLists:a}})}function GR(e){return e.reduce((t,r)=>(t.total+=1,t[r.status.toLowerCase()]+=1,t),{total:0,ready:0,running:0,done:0,failed:0})}function x3(e,t){Sm(e,r=>({...r,collapsed:t}))}function w3(e){const[,t]=S.useState({});return S.useEffect(()=>{const r=bg(e),a=()=>t({});return r.listeners.add(a),()=>{r.listeners.delete(a)}},[e]),bg(e).state}function T3(e,t){return t.type==="task_list.init"?(E3(e,t),!0):t.type==="task_list.update"?(S3(e,t),!0):!1}const HR=()=>({isStreaming:!1,currentResponse:"",toolCalls:[],thinkingSteps:[],pendingElements:[]}),za=new Map;function vc(e){return za.has(e)||za.set(e,{state:HR(),listeners:new Set,abortController:null,toolCallMap:new Map}),za.get(e)}function Nu(e){return e?vc(e).state:HR()}function A3(e,t){const r=vc(e);return r.listeners.add(t),()=>r.listeners.delete(t)}function _n(e,t){const r=vc(e);r.state={...r.state,...t},r.listeners.forEach(a=>a())}async function k3(e){const t=za.get(e);if(t){t.abortController?.abort(),t.abortController=null,_n(e,{isStreaming:!1});try{await fetch("/cancel",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({session_id:e})})}catch{}}}function _3(e,t,r,a){const o=vc(e);if(o.state.isStreaming)return;_n(e,{isStreaming:!0,currentResponse:"",toolCalls:[],thinkingSteps:[],pendingElements:[]}),o.toolCallMap=new Map;const s=new AbortController;o.abortController=s,N3(e,t,r,s,a)}async function N3(e,t,r,a,o){try{const s=await fetch("/run",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:t,session_id:e,agent:r}),signal:a.signal});if(!s.ok)throw new Error(`HTTP ${s.status}`);const u=s.body?.getReader();if(!u)throw new Error("No response body");const c=new TextDecoder;let p="";for(;;){const{done:f,value:h}=await u.read();if(f)break;p+=c.decode(h,{stream:!0});const m=p.split(`
|
||||
`);p=m.pop()||"";for(const v of m)if(v.startsWith("data: "))try{const E=JSON.parse(v.slice(6));R3(e,E,o)}catch{}}}catch(s){if(s.name!=="AbortError"){const u=za.get(e);u&&_n(e,{thinkingSteps:[...u.state.thinkingSteps,`Error: ${s.message}`]})}}finally{const s=za.get(e);s&&(s.abortController=null),_n(e,{isStreaming:!1}),o?.onEnd?.()}}function R3(e,t,r){const a=za.get(e);if(!a)return;const o=a.toolCallMap;switch(t.type){case"session":t.session_id&&typeof t.session_id=="string"&&r?.onSessionId?.(t.session_id);break;case"token":case"run_content":case"team_run_content":{const u=t.token??t.content??"";u&&_n(e,{currentResponse:a.state.currentResponse+u});break}case"message":case"run_completed":case"team_run_completed":{const u=t.content;u&&_n(e,{currentResponse:u});break}case"thinking":case"reasoning_step":case"team_reasoning_step":{const u=t.step;u&&_n(e,{thinkingSteps:[...a.state.thinkingSteps,u]});break}case"reasoning_started":case"team_reasoning_started":_n(e,{thinkingSteps:[...a.state.thinkingSteps,"Starting reasoning..."]});break;case"tool_call":case"tool_call_started":case"team_tool_call_started":{const u={name:t.name,description:t.description,icon:t.icon,step_number:t.step_number,status:"running",tool_call_id:t.tool_call_id,args:t.args};u.tool_call_id&&o.set(u.tool_call_id,u);const c=wk(a.state.toolCalls,u);_n(e,{toolCalls:c});break}case"tool_call_completed":case"team_tool_call_completed":{const u=t.extra_data,c=t.a2ui??u?.a2ui,p=t.elements??u?.elements,f={name:t.name,description:t.description,icon:t.icon,step_number:t.step_number,status:"done",tool_call_id:t.tool_call_id,args:t.args,formatted_result:t.formatted_result,result:t.result,surface_id:t.surface_id??u?.surface_id,a2ui:c?.length?{messages:c,surface_id:t.surface_id??u?.surface_id}:void 0,elements:p};f.tool_call_id&&o.set(f.tool_call_id,f);const h=wk(a.state.toolCalls,f),m=p?.length?[...a.state.pendingElements,...p]:a.state.pendingElements;_n(e,{toolCalls:h,pendingElements:m});break}case"message_element":{const u=t.extra_data,c=t.element??u?.element;c&&_n(e,{pendingElements:[...a.state.pendingElements,c]});break}case"error":case"run_error":case"team_run_error":{const u=t.error;u&&_n(e,{thinkingSteps:[...a.state.thinkingSteps,`Error: ${u}`]});break}case"approval_required":case"approval_pending":case"run_paused":{const u=t.extra_data;if(u?.type==="approval_required"){const c=u;window.dispatchEvent(new CustomEvent("aiui:approval-required",{detail:c}))}break}case"end":case"done":case"run_cancelled":case"team_run_cancelled":break;default:T3(e,t);break}}function wk(e,t){if(t.tool_call_id){const r=e.findIndex(a=>a.tool_call_id===t.tool_call_id);if(r>=0){const a=[...e];return a[r]={...a[r],...t},a}}return[...e,t]}function VR(e={}){const{externalSessionId:t}=e,[r,a]=S.useState(()=>Nu(t));S.useEffect(()=>{if(!t){a(Nu(null));return}return a(Nu(t)),A3(t,()=>{a(Nu(t))})},[t]);const o=S.useCallback((u,c)=>{const p=t??`new-${Date.now()}`;_3(p,u,c,{onSessionId:f=>{e.onSessionId?.(f)},onEnd:e.onEnd})},[t,e.onSessionId,e.onEnd]),s=S.useCallback(()=>{t&&k3(t)},[t]);return{isStreaming:r.isStreaming,currentResponse:r.currentResponse,toolCalls:r.toolCalls,thinkingSteps:r.thinkingSteps,pendingElements:r.pendingElements,sendMessage:o,cancel:s}}function C3(e,t){if(e==null)return{};var r={};for(var a in e)if({}.hasOwnProperty.call(e,a)){if(t.indexOf(a)!==-1)continue;r[a]=e[a]}return r}function I3(e,t){if(e==null)return{};var r,a,o=C3(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(a=0;a<s.length;a++)r=s[a],t.indexOf(r)===-1&&{}.propertyIsEnumerable.call(e,r)&&(o[r]=e[r])}return o}function yg(e,t){(t==null||t>e.length)&&(t=e.length);for(var r=0,a=Array(t);r<t;r++)a[r]=e[r];return a}function O3(e){if(Array.isArray(e))return yg(e)}function L3(e){if(typeof Symbol<"u"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}function D3(e,t){if(e){if(typeof e=="string")return yg(e,t);var r={}.toString.call(e).slice(8,-1);return r==="Object"&&e.constructor&&(r=e.constructor.name),r==="Map"||r==="Set"?Array.from(e):r==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?yg(e,t):void 0}}function M3(){throw new TypeError(`Invalid attempt to spread non-iterable instance.
|
||||
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function vg(e){return O3(e)||L3(e)||D3(e)||M3()}function Cl(e){"@babel/helpers - typeof";return Cl=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Cl(e)}function j3(e,t){if(Cl(e)!="object"||!e)return e;var r=e[Symbol.toPrimitive];if(r!==void 0){var a=r.call(e,t);if(Cl(a)!="object")return a;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}function U3(e){var t=j3(e,"string");return Cl(t)=="symbol"?t:t+""}function qR(e,t,r){return(t=U3(t))in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function Eg(){return Eg=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var a in r)({}).hasOwnProperty.call(r,a)&&(e[a]=r[a])}return e},Eg.apply(null,arguments)}function Tk(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter(function(o){return Object.getOwnPropertyDescriptor(e,o).enumerable})),r.push.apply(r,a)}return r}function Yi(e){for(var t=1;t<arguments.length;t++){var r=arguments[t]!=null?arguments[t]:{};t%2?Tk(Object(r),!0).forEach(function(a){qR(e,a,r[a])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(r)):Tk(Object(r)).forEach(function(a){Object.defineProperty(e,a,Object.getOwnPropertyDescriptor(r,a))})}return e}function F3(e){var t=e.length;if(t===0||t===1)return e;if(t===2)return[e[0],e[1],"".concat(e[0],".").concat(e[1]),"".concat(e[1],".").concat(e[0])];if(t===3)return[e[0],e[1],e[2],"".concat(e[0],".").concat(e[1]),"".concat(e[0],".").concat(e[2]),"".concat(e[1],".").concat(e[0]),"".concat(e[1],".").concat(e[2]),"".concat(e[2],".").concat(e[0]),"".concat(e[2],".").concat(e[1]),"".concat(e[0],".").concat(e[1],".").concat(e[2]),"".concat(e[0],".").concat(e[2],".").concat(e[1]),"".concat(e[1],".").concat(e[0],".").concat(e[2]),"".concat(e[1],".").concat(e[2],".").concat(e[0]),"".concat(e[2],".").concat(e[0],".").concat(e[1]),"".concat(e[2],".").concat(e[1],".").concat(e[0])];if(t>=4)return[e[0],e[1],e[2],e[3],"".concat(e[0],".").concat(e[1]),"".concat(e[0],".").concat(e[2]),"".concat(e[0],".").concat(e[3]),"".concat(e[1],".").concat(e[0]),"".concat(e[1],".").concat(e[2]),"".concat(e[1],".").concat(e[3]),"".concat(e[2],".").concat(e[0]),"".concat(e[2],".").concat(e[1]),"".concat(e[2],".").concat(e[3]),"".concat(e[3],".").concat(e[0]),"".concat(e[3],".").concat(e[1]),"".concat(e[3],".").concat(e[2]),"".concat(e[0],".").concat(e[1],".").concat(e[2]),"".concat(e[0],".").concat(e[1],".").concat(e[3]),"".concat(e[0],".").concat(e[2],".").concat(e[1]),"".concat(e[0],".").concat(e[2],".").concat(e[3]),"".concat(e[0],".").concat(e[3],".").concat(e[1]),"".concat(e[0],".").concat(e[3],".").concat(e[2]),"".concat(e[1],".").concat(e[0],".").concat(e[2]),"".concat(e[1],".").concat(e[0],".").concat(e[3]),"".concat(e[1],".").concat(e[2],".").concat(e[0]),"".concat(e[1],".").concat(e[2],".").concat(e[3]),"".concat(e[1],".").concat(e[3],".").concat(e[0]),"".concat(e[1],".").concat(e[3],".").concat(e[2]),"".concat(e[2],".").concat(e[0],".").concat(e[1]),"".concat(e[2],".").concat(e[0],".").concat(e[3]),"".concat(e[2],".").concat(e[1],".").concat(e[0]),"".concat(e[2],".").concat(e[1],".").concat(e[3]),"".concat(e[2],".").concat(e[3],".").concat(e[0]),"".concat(e[2],".").concat(e[3],".").concat(e[1]),"".concat(e[3],".").concat(e[0],".").concat(e[1]),"".concat(e[3],".").concat(e[0],".").concat(e[2]),"".concat(e[3],".").concat(e[1],".").concat(e[0]),"".concat(e[3],".").concat(e[1],".").concat(e[2]),"".concat(e[3],".").concat(e[2],".").concat(e[0]),"".concat(e[3],".").concat(e[2],".").concat(e[1]),"".concat(e[0],".").concat(e[1],".").concat(e[2],".").concat(e[3]),"".concat(e[0],".").concat(e[1],".").concat(e[3],".").concat(e[2]),"".concat(e[0],".").concat(e[2],".").concat(e[1],".").concat(e[3]),"".concat(e[0],".").concat(e[2],".").concat(e[3],".").concat(e[1]),"".concat(e[0],".").concat(e[3],".").concat(e[1],".").concat(e[2]),"".concat(e[0],".").concat(e[3],".").concat(e[2],".").concat(e[1]),"".concat(e[1],".").concat(e[0],".").concat(e[2],".").concat(e[3]),"".concat(e[1],".").concat(e[0],".").concat(e[3],".").concat(e[2]),"".concat(e[1],".").concat(e[2],".").concat(e[0],".").concat(e[3]),"".concat(e[1],".").concat(e[2],".").concat(e[3],".").concat(e[0]),"".concat(e[1],".").concat(e[3],".").concat(e[0],".").concat(e[2]),"".concat(e[1],".").concat(e[3],".").concat(e[2],".").concat(e[0]),"".concat(e[2],".").concat(e[0],".").concat(e[1],".").concat(e[3]),"".concat(e[2],".").concat(e[0],".").concat(e[3],".").concat(e[1]),"".concat(e[2],".").concat(e[1],".").concat(e[0],".").concat(e[3]),"".concat(e[2],".").concat(e[1],".").concat(e[3],".").concat(e[
Line truncated
|
||||
`))})}function Y3(e){var t=e.codeString,r=e.codeStyle,a=e.containerStyle,o=a===void 0?{float:"left",paddingRight:"10px"}:a,s=e.numberStyle,u=s===void 0?{}:s,c=e.startingLineNumber;return Ke.createElement("code",{style:Object.assign({},r,o)},q3({lines:t.replace(/\n$/,"").split(`
|
||||
`),style:u,startingLineNumber:c}))}function W3(e){return"".concat(e.toString().length,".25em")}function WR(e,t){return{type:"element",tagName:"span",properties:{key:"line-number--".concat(e),className:["comment","linenumber","react-syntax-highlighter-line-number"],style:t},children:[{type:"text",value:e}]}}function KR(e,t,r){var a={display:"inline-block",minWidth:W3(r),paddingRight:"1em",textAlign:"right",userSelect:"none"},o=typeof e=="function"?e(t):e,s=la(la({},a),o);return s}function Uu(e){var t=e.children,r=e.lineNumber,a=e.lineNumberStyle,o=e.largestLineNumber,s=e.showInlineLineNumbers,u=e.lineProps,c=u===void 0?{}:u,p=e.className,f=p===void 0?[]:p,h=e.showLineNumbers,m=e.wrapLongLines,v=e.wrapLines,E=v===void 0?!1:v,x=E?la({},typeof c=="function"?c(r):c):{};if(x.className=x.className?[].concat(vg(x.className.trim().split(/\s+/)),vg(f)):f,r&&s){var w=KR(a,r,o);t.unshift(WR(r,w))}return m&h&&(x.style=la({display:"flex"},x.style)),{type:"element",tagName:"span",properties:x,children:t}}function XR(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:[],r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:[];e.length===void 0&&(e=[e]);for(var a=0;a<e.length;a++){var o=e[a];if(o.type==="text")r.push(Uu({children:[o],className:vg(new Set(t))}));else if(o.children){var s,u=t.concat(((s=o.properties)===null||s===void 0?void 0:s.className)||[]);XR(o.children,u).forEach(function(c){return r.push(c)})}}return r}function K3(e,t,r,a,o,s,u,c,p){var f,h=XR(e.value),m=[],v=-1,E=0;function x(F,L){var z=arguments.length>2&&arguments[2]!==void 0?arguments[2]:[];return Uu({children:F,lineNumber:L,lineNumberStyle:c,largestLineNumber:u,showInlineLineNumbers:o,lineProps:r,className:z,showLineNumbers:a,wrapLongLines:p,wrapLines:t})}function w(F,L){if(a&&L&&o){var z=KR(c,L,u);F.unshift(WR(L,z))}return F}function k(F,L){var z=arguments.length>2&&arguments[2]!==void 0?arguments[2]:[];return t||z.length>0?x(F,L,z):w(F,L)}for(var A=function(){var L=h[E],z=L.children[0].value,$=V3(z);if($){var K=z.split(`
|
||||
`);K.forEach(function(j,q){var X=a&&m.length+s,ne={type:"text",value:"".concat(j,`
|
||||
`)};if(q===0){var re=h.slice(v+1,E).concat(Uu({children:[ne],className:L.properties.className})),ee=k(re,X);m.push(ee)}else if(q===K.length-1){var M=h[E+1]&&h[E+1].children&&h[E+1].children[0],H={type:"text",value:"".concat(j)};if(M){var Q=Uu({children:[H],className:L.properties.className});h.splice(E+1,0,Q)}else{var de=[H],I=k(de,X,L.properties.className);m.push(I)}}else{var U=[ne],B=k(U,X,L.properties.className);m.push(B)}}),v=E}E++};E<h.length;)A();if(v!==h.length-1){var _=h.slice(v+1,h.length);if(_&&_.length){var N=a&&m.length+s,D=k(_,N);m.push(D)}}return t?m:(f=[]).concat.apply(f,m)}function X3(e){var t=e.rows,r=e.stylesheet,a=e.useInlineStyles;return t.map(function(o,s){return YR({node:o,stylesheet:r,useInlineStyles:a,key:"code-segment-".concat(s)})})}function ZR(e){return e&&typeof e.highlightAuto<"u"}function Z3(e){var t=e.astGenerator,r=e.language,a=e.code,o=e.defaultCodeValue;if(ZR(t)){var s=$3(t,r);return r==="text"?{value:o,language:"text"}:s?t.highlight(r,a):t.highlightAuto(a)}try{return r&&r!=="text"?{value:t.highlight(a,r)}:{value:o}}catch{return{value:o}}}function Q3(e,t){return function(a){var o,s,u=a.language,c=a.children,p=a.style,f=p===void 0?t:p,h=a.customStyle,m=h===void 0?{}:h,v=a.codeTagProps,E=v===void 0?{className:u?"language-".concat(u):void 0,style:la(la({},f['code[class*="language-"]']),f['code[class*="language-'.concat(u,'"]')])}:v,x=a.useInlineStyles,w=x===void 0?!0:x,k=a.showLineNumbers,A=k===void 0?!1:k,_=a.showInlineLineNumbers,N=_===void 0?!0:_,D=a.startingLineNumber,F=D===void 0?1:D,L=a.lineNumberContainerStyle,z=a.lineNumberStyle,$=z===void 0?{}:z,K=a.wrapLines,j=a.wrapLongLines,q=j===void 0?!1:j,X=a.lineProps,ne=X===void 0?{}:X,re=a.renderer,ee=a.PreTag,M=ee===void 0?"pre":ee,H=a.CodeTag,Q=H===void 0?"code":H,de=a.code,I=de===void 0?(Array.isArray(c)?c[0]:c)||"":de,U=a.astGenerator,B=I3(a,G3);U=U||e;var C=A?Ke.createElement(Y3,{containerStyle:L,codeStyle:E.style||{},numberStyle:$,startingLineNumber:F,codeString:I}):null,J=f.hljs||f['pre[class*="language-"]']||{backgroundColor:"#fff"},ae=ZR(U)?"hljs":"prismjs",ue=w?Object.assign({},B,{style:Object.assign({},J,m)}):Object.assign({},B,{className:B.className?"".concat(ae," ").concat(B.className):ae,style:Object.assign({},m)});if(q?E.style=la({whiteSpace:"pre-wrap"},E.style):E.style=la({whiteSpace:"pre"},E.style),!U)return Ke.createElement(M,ue,C,Ke.createElement(Q,E,I));(K===void 0&&re||q)&&(K=!0),re=re||X3;var ie=[{type:"text",value:I}],he=Z3({astGenerator:U,language:u,code:I,defaultCodeValue:ie});he.language===null&&(he.value=ie);var fe=(o=(s=I.match(/\n/g))===null||s===void 0?void 0:s.length)!==null&&o!==void 0?o:0,ke=F+fe,Ie=K3(he,K,ne,A,N,F,ke,$,q);return Ke.createElement(M,ue,Ke.createElement(Q,E,!N&&C,re({rows:Ie,stylesheet:f,useInlineStyles:w})))}}const J3="modulepreload",e4=function(e){return"/"+e},_k={},Nk=function(t,r,a){let o=Promise.resolve();if(r&&r.length>0){let p=function(f){return Promise.all(f.map(h=>Promise.resolve(h).then(m=>({status:"fulfilled",value:m}),m=>({status:"rejected",reason:m}))))};document.getElementsByTagName("link");const u=document.querySelector("meta[property=csp-nonce]"),c=u?.nonce||u?.getAttribute("nonce");o=p(r.map(f=>{if(f=e4(f),f in _k)return;_k[f]=!0;const h=f.endsWith(".css"),m=h?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${f}"]${m}`))return;const v=document.createElement("link");if(v.rel=h?"stylesheet":J3,h||(v.as="script"),v.crossOrigin="",v.href=f,c&&v.setAttribute("nonce",c),document.head.appendChild(v),h)return new Promise((E,x)=>{v.addEventListener("load",E),v.addEventListener("error",()=>x(new Error(`Unable to preload CSS for ${f}`)))})}))}function s(u){const c=new Event("vite:preloadError",{cancelable:!0});if(c.payload=u,window.dispatchEvent(c),!c.defaultPrevented)throw u}return o.then(u=>{for(const c of u||[])c.status==="rejected"&&s(c.reason);return t().catch(s)})},t4=["abap","abnf","actionscript","ada","agda","al","antlr4","apacheconf","apex","apl","applescript","aql","arduino","arff","armasm","arturo","asciidoc","asm6502","asmatmel","aspnet","autohotkey","autoit","avisynth","avro-idl","awk","bash","basic","batch","bbcode","bbj","bicep","birb","bison","bnf","bqn","brainfuck","brightscript","bro","bsl","c","cfscript","chaiscript","cil","cilkc","cilkcpp","clike","clojure","cmake","cobol","coffeescript","concurnas","cooklang","coq","cpp","crystal","csharp","cshtml","csp","css-extras","css","csv","cue","cypher","d","dart","dataweave","dax","dhall","diff","django","dns-zone-file","docker","dot","ebnf","editorconfig","eiffel","ejs","elixir","elm","erb","erlang","etlua","excel-formula","factor","false","firestore-security-rules","flow","fortran","fsharp","ftl","gap","gcode","gdscript","gedcom","gettext","gherkin","git","glsl","gml","gn","go-module","go","gradle","graphql","groovy","haml","handlebars","haskell","haxe","hcl","hlsl","hoon","hpkp","hsts","http","ichigojam","icon","icu-message-format","idris","iecst","ignore","inform7","ini","io","j","java","java
Line truncated
|
||||
?|
|
||||
|(?![\\s\\S])))+`,"m"),alias:s,inside:{line:{pattern:/(.)(?=[\s\S]).*(?:\r\n?|\n)?/,lookbehind:!0},prefix:{pattern:/[\s\S]/,alias:/\w+/.exec(a)[0]}}}}),Object.defineProperty(t.languages.diff,"PREFIXES",{value:r})})(e)}zt.displayName="markup-templating";zt.aliases=[];function zt(e){e.register(Ot),(function(t){function r(a,o){return"___"+a.toUpperCase()+o+"___"}Object.defineProperties(t.languages["markup-templating"]={},{buildPlaceholders:{value:function(a,o,s,u){if(a.language===o){var c=a.tokenStack=[];a.code=a.code.replace(s,function(p){if(typeof u=="function"&&!u(p))return p;for(var f=c.length,h;a.code.indexOf(h=r(o,f))!==-1;)++f;return c[f]=p,h}),a.grammar=t.languages.markup}}},tokenizePlaceholders:{value:function(a,o){if(a.language!==o||!a.tokenStack)return;a.grammar=t.languages[o];var s=0,u=Object.keys(a.tokenStack);function c(p){for(var f=0;f<p.length&&!(s>=u.length);f++){var h=p[f];if(typeof h=="string"||h.content&&typeof h.content=="string"){var m=u[s],v=a.tokenStack[m],E=typeof h=="string"?h:h.content,x=r(o,m),w=E.indexOf(x);if(w>-1){++s;var k=E.substring(0,w),A=new t.Token(o,t.tokenize(v,a.grammar),"language-"+o,v),_=E.substring(w+x.length),N=[];k&&N.push.apply(N,c([k])),N.push(A),_&&N.push.apply(N,c([_])),typeof h=="string"?p.splice.apply(p,[f,1].concat(N)):h.content=N}}else h.content&&c(h.content)}return p}c(a.tokens)}}})})(e)}Ih.displayName="django";Ih.aliases=["jinja2"];function Ih(e){e.register(zt),(function(t){t.languages.django={comment:/^\{#[\s\S]*?#\}$/,tag:{pattern:/(^\{%[+-]?\s*)\w+/,lookbehind:!0,alias:"keyword"},delimiter:{pattern:/^\{[{%][+-]?|[+-]?[}%]\}$/,alias:"punctuation"},string:{pattern:/("|')(?:\\.|(?!\1)[^\\\r\n])*\1/,greedy:!0},filter:{pattern:/(\|)\w+/,lookbehind:!0,alias:"function"},test:{pattern:/(\bis\s+(?:not\s+)?)(?!not\b)\w+/,lookbehind:!0,alias:"function"},function:/\b[a-z_]\w+(?=\s*\()/i,keyword:/\b(?:and|as|by|else|for|if|import|in|is|loop|not|or|recursive|with|without)\b/,operator:/[-+%=]=?|!=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,number:/\b\d+(?:\.\d+)?\b/,boolean:/[Ff]alse|[Nn]one|[Tt]rue/,variable:/\b\w+\b/,punctuation:/[{}[\](),.:;]/};var r=/\{\{[\s\S]*?\}\}|\{%[\s\S]*?%\}|\{#[\s\S]*?#\}/g,a=t.languages["markup-templating"];t.hooks.add("before-tokenize",function(o){a.buildPlaceholders(o,"django",r)}),t.hooks.add("after-tokenize",function(o){a.tokenizePlaceholders(o,"django")}),t.languages.jinja2=t.languages.django,t.hooks.add("before-tokenize",function(o){a.buildPlaceholders(o,"jinja2",r)}),t.hooks.add("after-tokenize",function(o){a.tokenizePlaceholders(o,"jinja2")})})(e)}Oh.displayName="dns-zone-file";Oh.aliases=["dns-zone"];function Oh(e){e.languages["dns-zone-file"]={comment:/;.*/,string:{pattern:/"(?:\\.|[^"\\\r\n])*"/,greedy:!0},variable:[{pattern:/(^\$ORIGIN[ \t]+)\S+/m,lookbehind:!0},{pattern:/(^|\s)@(?=\s|$)/,lookbehind:!0}],keyword:/^\$(?:INCLUDE|ORIGIN|TTL)(?=\s|$)/m,class:{pattern:/(^|\s)(?:CH|CS|HS|IN)(?=\s|$)/,lookbehind:!0,alias:"keyword"},type:{pattern:/(^|\s)(?:A|A6|AAAA|AFSDB|APL|ATMA|CAA|CDNSKEY|CDS|CERT|CNAME|DHCID|DLV|DNAME|DNSKEY|DS|EID|GID|GPOS|HINFO|HIP|IPSECKEY|ISDN|KEY|KX|LOC|MAILA|MAILB|MB|MD|MF|MG|MINFO|MR|MX|NAPTR|NB|NBSTAT|NIMLOC|NINFO|NS|NSAP|NSAP-PTR|NSEC|NSEC3|NSEC3PARAM|NULL|NXT|OPENPGPKEY|PTR|PX|RKEY|RP|RRSIG|RT|SIG|SINK|SMIMEA|SOA|SPF|SRV|SSHFP|TA|TKEY|TLSA|TSIG|TXT|UID|UINFO|UNSPEC|URI|WKS|X25)(?=\s|$)/,lookbehind:!0,alias:"keyword"},punctuation:/[()]/},e.languages["dns-zone"]=e.languages["dns-zone-file"]}Lh.displayName="docker";Lh.aliases=["dockerfile"];function Lh(e){(function(t){var r=/\\[\r\n](?:\s|\\[\r\n]|#.*(?!.))*(?![\s#]|\\[\r\n])/.source,a=/(?:[ \t]+(?![ \t])(?:<SP_BS>)?|<SP_BS>)/.source.replace(/<SP_BS>/g,function(){return r}),o=/"(?:[^"\\\r\n]|\\(?:\r\n|[\s\S]))*"|'(?:[^'\\\r\n]|\\(?:\r\n|[\s\S]))*'/.source,s=/--[\w-]+=(?:<STR>|(?!["'])(?:[^\s\\]|\\.)+)/.source.replace(/<STR>/g,function(){return o}),u={pattern:RegExp(o),greedy:!0},c={pattern:/(^[ \t]*)#.*/m,lookbehind:!0,greedy:!0};function p(f,h){return f=f.replace(/<OPT>/g,function(){return s}).replace(/<SP>/g,function(){return a}),RegExp(f,h)}t.languages.docker={instruction:{pattern:/(^[ \t]*)(?:ADD|ARG|CMD|COPY|ENTRYPOINT|ENV|EXPOSE|FROM|HEALTHCHECK|LABEL|MAINTAINER|ONBUILD|RUN|SHELL|STOPSIGNAL|USER|VOLUME|WORKDIR)(?=\s)(?:\\.|[^\r\n\\])*(?:\\$(?:\s|#.*$)*(?![\s#])(?:\\.|[^\r\n\\])*)*/im,lookbehind:!0,greedy:!0,inside:{options:{pattern:p(/(^(?:ONBUILD<SP>)?\w+<SP>)<OPT>(?:<SP><OPT>)*/.source,"i"),lookbehind:!0,greedy:!0,inside:{property:{pattern:/(^|\s)--[\w-]+/,lookbehind:!0},string:[u,{pattern:/(=)(?!["'])(?:[^\s\\]|\\.)+/,lookbehind:!0}],operator:/\\$/m,punctuation:/=/}},keyword:[{pattern:p(/(^(?:ONBUILD<SP>)?HEALTHCHECK<SP>(?:<OPT><SP>)*)(?:CMD|NONE)\b/.source,"i"),lookbehind:!0,greedy:!0},{pattern:p(/(^(?:ONBUILD<SP>)?FROM<SP>(?:<OPT><SP>)*(?!--)[^ \t\\]+<SP>)AS/.source,"i"),lookbehind:!0,greedy:!0},{pattern:p(/(^ONBUILD<SP>)\w+/.source,"i"),lookbehind:!0,greedy:!0},{pattern:/^\w+/,greedy:!0}],comment:c,string:u,variable:/\$(?:\w+|\{[^{}"'\\]*\})/,oper
Line truncated
|
||||
]))(?:`+(/[^\s\\]/.source+'|[ ]+(?:(?![ "])|'+a+")|"+/\\[\r\n]+(?:[#;].*[\r\n]+)*(?![#;])/.source)+")*"),lookbehind:!0,greedy:!0,alias:"attr-value",inside:{comment:r,quoted:{pattern:RegExp(/(^|\s)/.source+a),lookbehind:!0,greedy:!0},punctuation:/\\$/m,boolean:{pattern:/^(?:false|no|off|on|true|yes)$/,greedy:!0}}},punctuation:/=/}})(e)}Vl.displayName="t4-templating";Vl.aliases=[];function Vl(e){(function(t){function r(o,s,u){return{pattern:RegExp("<#"+o+"[\\s\\S]*?#>"),alias:"block",inside:{delimiter:{pattern:RegExp("^<#"+o+"|#>$"),alias:"important"},content:{pattern:/[\s\S]+/,inside:s,alias:u}}}}function a(o){var s=t.languages[o],u="language-"+o;return{block:{pattern:/<#[\s\S]+?#>/,inside:{directive:r("@",{"attr-value":{pattern:/=(?:("|')(?:\\[\s\S]|(?!\1)[^\\])*\1|[^\s'">=]+)/,inside:{punctuation:/^=|^["']|["']$/}},keyword:/\b\w+(?=\s)/,"attr-name":/\b\w+/}),expression:r("=",s,u),"class-feature":r("\\+",s,u),standard:r("",s,u)}}}}t.languages["t4-templating"]=Object.defineProperty({},"createT4",{value:a})})(e)}qv.displayName="t4-cs";qv.aliases=["t4"];function qv(e){e.register(io),e.register(Vl),e.languages.t4=e.languages["t4-cs"]=e.languages["t4-templating"].createT4("csharp")}Nc.displayName="vbnet";Nc.aliases=[];function Nc(e){e.register(xc),e.languages.vbnet=e.languages.extend("basic",{comment:[{pattern:/(?:!|REM\b).+/i,inside:{keyword:/^REM/i}},{pattern:/(^|[^\\:])'.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(^|[^"])"(?:""|[^"])*"(?!")/,lookbehind:!0,greedy:!0},keyword:/(?:\b(?:ADDHANDLER|ADDRESSOF|ALIAS|AND|ANDALSO|AS|BEEP|BLOAD|BOOLEAN|BSAVE|BYREF|BYTE|BYVAL|CALL(?: ABSOLUTE)?|CASE|CATCH|CBOOL|CBYTE|CCHAR|CDATE|CDBL|CDEC|CHAIN|CHAR|CHDIR|CINT|CLASS|CLEAR|CLNG|CLOSE|CLS|COBJ|COM|COMMON|CONST|CONTINUE|CSBYTE|CSHORT|CSNG|CSTR|CTYPE|CUINT|CULNG|CUSHORT|DATA|DATE|DECIMAL|DECLARE|DEF(?: FN| SEG|DBL|INT|LNG|SNG|STR)|DEFAULT|DELEGATE|DIM|DIRECTCAST|DO|DOUBLE|ELSE|ELSEIF|END|ENUM|ENVIRON|ERASE|ERROR|EVENT|EXIT|FALSE|FIELD|FILES|FINALLY|FOR(?: EACH)?|FRIEND|FUNCTION|GET|GETTYPE|GETXMLNAMESPACE|GLOBAL|GOSUB|GOTO|HANDLES|IF|IMPLEMENTS|IMPORTS|IN|INHERITS|INPUT|INTEGER|INTERFACE|IOCTL|IS|ISNOT|KEY|KILL|LET|LIB|LIKE|LINE INPUT|LOCATE|LOCK|LONG|LOOP|LSET|ME|MKDIR|MOD|MODULE|MUSTINHERIT|MUSTOVERRIDE|MYBASE|MYCLASS|NAME|NAMESPACE|NARROWING|NEW|NEXT|NOT|NOTHING|NOTINHERITABLE|NOTOVERRIDABLE|OBJECT|OF|OFF|ON(?: COM| ERROR| KEY| TIMER)?|OPEN|OPERATOR|OPTION(?: BASE)?|OPTIONAL|OR|ORELSE|OUT|OVERLOADS|OVERRIDABLE|OVERRIDES|PARAMARRAY|PARTIAL|POKE|PRIVATE|PROPERTY|PROTECTED|PUBLIC|PUT|RAISEEVENT|READ|READONLY|REDIM|REM|REMOVEHANDLER|RESTORE|RESUME|RETURN|RMDIR|RSET|RUN|SBYTE|SELECT(?: CASE)?|SET|SHADOWS|SHARED|SHELL|SHORT|SINGLE|SLEEP|STATIC|STEP|STOP|STRING|STRUCTURE|SUB|SWAP|SYNCLOCK|SYSTEM|THEN|THROW|TIMER|TO|TROFF|TRON|TRUE|TRY|TRYCAST|TYPE|TYPEOF|UINTEGER|ULONG|UNLOCK|UNTIL|USHORT|USING|VIEW PRINT|WAIT|WEND|WHEN|WHILE|WIDENING|WITH|WITHEVENTS|WRITE|WRITEONLY|XOR)|\B(?:#CONST|#ELSE|#ELSEIF|#END|#IF))(?:\$|\b)/i,punctuation:/[,;:(){}]/})}Yv.displayName="t4-vb";Yv.aliases=[];function Yv(e){e.register(Vl),e.register(Nc),e.languages["t4-vb"]=e.languages["t4-templating"].createT4("vbnet")}Wv.displayName="tap";Wv.aliases=[];function Wv(e){e.register(Sc),e.languages.tap={fail:/not ok[^#{\n\r]*/,pass:/ok[^#{\n\r]*/,pragma:/pragma [+-][a-z]+/,bailout:/bail out!.*/i,version:/TAP version \d+/i,plan:/\b\d+\.\.\d+(?: +#.*)?/,subtest:{pattern:/# Subtest(?:: .*)?/,greedy:!0},punctuation:/[{}]/,directive:/#.*/,yamlish:{pattern:/(^[ \t]*)---[\s\S]*?[\r\n][ \t]*\.\.\.$/m,lookbehind:!0,inside:e.languages.yaml,alias:"language-yaml"}}}Kv.displayName="tcl";Kv.aliases=[];function Kv(e){e.languages.tcl={comment:{pattern:/(^|[^\\])#.*/,lookbehind:!0},string:{pattern:/"(?:[^"\\\r\n]|\\(?:\r\n|[\s\S]))*"/,greedy:!0},variable:[{pattern:/(\$)(?:::)?(?:[a-zA-Z0-9]+::)*\w+/,lookbehind:!0},{pattern:/(\$)\{[^}]+\}/,lookbehind:!0},{pattern:/(^[\t ]*set[ \t]+)(?:::)?(?:[a-zA-Z0-9]+::)*\w+/m,lookbehind:!0}],function:{pattern:/(^[\t ]*proc[ \t]+)\S+/m,lookbehind:!0},builtin:[{pattern:/(^[\t ]*)(?:break|class|continue|error|eval|exit|for|foreach|if|proc|return|switch|while)\b/m,lookbehind:!0},/\b(?:else|elseif)\b/],scope:{pattern:/(^[\t ]*)(?:global|upvar|variable)\b/m,lookbehind:!0,alias:"constant"},keyword:{pattern:/(^[\t ]*|\[)(?:Safe_Base|Tcl|after|append|apply|array|auto_(?:execok|import|load|mkindex|qualify|reset)|automkindex_old|bgerror|binary|catch|cd|chan|clock|close|concat|dde|dict|encoding|eof|exec|expr|fblocked|fconfigure|fcopy|file(?:event|name)?|flush|gets|glob|history|http|incr|info|interp|join|lappend|lassign|lindex|linsert|list|llength|load|lrange|lrepeat|lreplace|lreverse|lsearch|lset|lsort|math(?:func|op)|memory|msgcat|namespace|open|package|parray|pid|pkg_mkIndex|platform|puts|pwd|re_syntax|read|refchan|regexp|registry|regsub|rename|scan|seek|set|socket|source|split|string|subst|tcl(?:_endOfWord|_findLibrary|startOf(?:Next|Previous)Word|test|vars|wordBreak(?:After|Before))|tell|time|tm|trace|unknown|unload|unset|update|uplevel|vwait
Line truncated
|
||||
- a positive number
|
||||
- less than the value passed to \`max\` (or ${OE} if no \`max\` prop is set)
|
||||
- \`null\` or \`undefined\` if the progress is indeterminate.
|
||||
|
||||
Defaulting to \`null\`.`}var JG=VC,eH=YC;function tH({className:e,value:t,...r}){return g.jsx(JG,{"data-slot":"progress",className:me("bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",e),...r,children:g.jsx(eH,{"data-slot":"progress-indicator",className:"bg-primary h-full w-full flex-1 transition-all",style:{transform:`translateX(-${100-(t||0)}%)`}})})}function Yl(e){const t=S.useRef({value:e,previous:e});return S.useMemo(()=>(t.current.value!==e&&(t.current.previous=t.current.value,t.current.value=e),t.current.previous),[e])}function Wl(e){const[t,r]=S.useState(void 0);return gt(()=>{if(e){r({width:e.offsetWidth,height:e.offsetHeight});const a=new ResizeObserver(o=>{if(!Array.isArray(o)||!o.length)return;const s=o[0];let u,c;if("borderBoxSize"in s){const p=s.borderBoxSize,f=Array.isArray(p)?p[0]:p;u=f.inlineSize,c=f.blockSize}else u=e.offsetWidth,c=e.offsetHeight;r({width:u,height:c})});return a.observe(e,{box:"border-box"}),()=>a.unobserve(e)}else r(void 0)},[e]),t}var Mc="Switch",[nH]=Ut(Mc),[rH,aH]=nH(Mc),KC=S.forwardRef((e,t)=>{const{__scopeSwitch:r,name:a,checked:o,defaultChecked:s,required:u,disabled:c,value:p="on",onCheckedChange:f,form:h,...m}=e,[v,E]=S.useState(null),x=Ne(t,N=>E(N)),w=S.useRef(!1),k=v?h||!!v.closest("form"):!0,[A,_]=tn({prop:o,defaultProp:s??!1,onChange:f,caller:Mc});return g.jsxs(rH,{scope:r,checked:A,disabled:c,children:[g.jsx(be.button,{type:"button",role:"switch","aria-checked":A,"aria-required":u,"data-state":JC(A),"data-disabled":c?"":void 0,disabled:c,value:p,...m,ref:x,onClick:ye(e.onClick,N=>{_(D=>!D),k&&(w.current=N.isPropagationStopped(),w.current||N.stopPropagation())})}),k&&g.jsx(QC,{control:v,bubbles:!w.current,name:a,value:p,checked:A,required:u,disabled:c,form:h,style:{transform:"translateX(-100%)"}})]})});KC.displayName=Mc;var XC="SwitchThumb",ZC=S.forwardRef((e,t)=>{const{__scopeSwitch:r,...a}=e,o=aH(XC,r);return g.jsx(be.span,{"data-state":JC(o.checked),"data-disabled":o.disabled?"":void 0,...a,ref:t})});ZC.displayName=XC;var iH="SwitchBubbleInput",QC=S.forwardRef(({__scopeSwitch:e,control:t,checked:r,bubbles:a=!0,...o},s)=>{const u=S.useRef(null),c=Ne(u,s),p=Yl(r),f=Wl(t);return S.useEffect(()=>{const h=u.current;if(!h)return;const m=window.HTMLInputElement.prototype,E=Object.getOwnPropertyDescriptor(m,"checked").set;if(p!==r&&E){const x=new Event("click",{bubbles:a});E.call(h,r),h.dispatchEvent(x)}},[p,r,a]),g.jsx("input",{type:"checkbox","aria-hidden":!0,defaultChecked:r,...o,tabIndex:-1,ref:c,style:{...o.style,...f,position:"absolute",pointerEvents:"none",opacity:0,margin:0}})});QC.displayName=iH;function JC(e){return e?"checked":"unchecked"}var oH=KC,lH=ZC;function sH({className:e,size:t="default",...r}){return g.jsx(oH,{"data-slot":"switch","data-size":t,className:me("peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6",e),...r,children:g.jsx(lH,{"data-slot":"switch-thumb",className:me("bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block rounded-full ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0")})})}var jc="Checkbox",[uH]=Ut(jc),[cH,LE]=uH(jc);function dH(e){const{__scopeCheckbox:t,checked:r,children:a,defaultChecked:o,disabled:s,form:u,name:c,onCheckedChange:p,required:f,value:h="on",internal_do_not_use_render:m}=e,[v,E]=tn({prop:r,defaultProp:o??!1,onChange:p,caller:jc}),[x,w]=S.useState(null),[k,A]=S.useState(null),_=S.useRef(!1),N=x?!!u||!!x.closest("form"):!0,D={checked:v,disabled:s,setChecked:E,control:x,setControl:w,name:c,form:u,value:h,hasConsumerStoppedPropagationRef:_,required:f,defaultChecked:sa(o)?!1:o,isFormControl:N,bubbleInput:k,setBubbleInput:A};return g.jsx(cH,{scope:t,...D,children:pH(m)?m(D):a})}var e0="CheckboxTrigger",t0=S.forwardRef(({__scopeCheckbox:e,onKeyDown:t,onClick:r,...a},o)=>{const{control:s,value:u,disabled:c,checked:p,required:f,setControl:h,setChecked:m,hasConsumerStoppedPropagationRef:v,isFormControl:E,bubbleInput:x}=LE(e0,e),w=Ne(o,h),k=S.useRef(p);return S.useEffect(()=>{const A=s?.form;if(A){const _=()=>m(k.current);return A.addEventListener("reset",_),()=>A.removeEventListener("reset",_)}},[s,m]),g.jsx(be.button,{type:"button",role:"checkbox","aria-checked":sa(p)?"mixed":p,"aria-required":f,"data-state":l0(p),"data-disabled":c?"":void 0,disabled:c,value:u,...a,ref:w,onKeyDown:ye(t,A=>{A.key==="Enter"&&A.preventDefault()}),onClick:ye(r,A=>{m(_=>sa(_)?!0:!_),x&&E&&(v.current=A.isPropagatiLine truncated
|
||||
`);for(;t.length&&!t[0].trim();)t.shift();for(;t.length&&!t[t.length-1].trim();)t.pop();if(!t.length)return"";const r=Math.min(...t.filter(a=>a.trim().length>0).map(a=>a.match(/^ */)?.[0].length??0));return r===0?t.join(`
|
||||
`):t.map(a=>a.slice(r)).join(`
|
||||
`)}function wV({language:e,children:t}){const[r,a]=S.useState(!1),o=S.useCallback(async()=>{await navigator.clipboard.writeText(t),a(!0),setTimeout(()=>a(!1),2e3)},[t]);return g.jsxs("div",{className:"relative group/code my-3 rounded-lg overflow-hidden",children:[g.jsxs("div",{className:"flex items-center justify-between px-4 py-1.5 bg-[#1e1e2e] text-xs text-gray-400",children:[g.jsx("span",{children:e||"code"}),g.jsx("button",{onClick:o,className:"flex items-center gap-1 hover:text-white transition-colors",children:r?"✓ Copied":"Copy"})]}),g.jsx(KN,{style:yP,language:e||"text",PreTag:"div",customStyle:{margin:0,borderRadius:0,fontSize:"13px"},children:t})]})}function TV(e){const t=e.split(`
|
||||
`);let r=!1,a=!1,o="",s=0;for(let u=0;u<t.length;u++){const d=t[u];if(/^[ \t]*```/.test(d)){r=!r;continue}if(r)continue;const p=d.match(/^([ \t]*)(\d+)\.[ \t]/);if(p){const f=p[1];!a||f!==o?(a=!0,o=f,s=1):s++,t[u]=d.replace(/^([ \t]*)\d+\./,`${f}${s}.`)}else d.trim()===""&&a||a&&/^[ \t]+\S/.test(d)||a&&(a=!1,s=0)}return t.join(`
|
||||
`)}function vO({content:e}){const t=TV(e);return g.jsx("div",{className:"prose prose-sm dark:prose-invert max-w-none prose-p:leading-relaxed prose-pre:p-0 prose-pre:bg-transparent",children:g.jsx(uN,{remarkPlugins:[LN],components:{ul({children:r,ordered:a,start:o}){if(a){const s=o!=null?Number(o):1;return g.jsx("ol",{start:s,style:{listStyleType:"decimal",paddingLeft:"1.5rem",margin:"0.5rem 0"},children:r})}return g.jsx("ul",{style:{listStyleType:"disc",paddingLeft:"1.5rem",margin:"0.5rem 0"},children:r})},li({children:r}){return g.jsx("li",{style:{display:"list-item",marginBottom:"0.25rem"},children:r})},code({className:r,children:a,...o}){const s=/language-(\w+)/.exec(r||""),u=xV(String(a));return!s&&!u.includes(`
|
||||
`)?g.jsx("code",{className:"px-1.5 py-0.5 rounded bg-muted text-sm font-mono",...o,children:a}):g.jsx(wV,{language:s?.[1],children:u})},a({href:r,children:a}){return g.jsx("a",{href:r,target:"_blank",rel:"noopener noreferrer",className:"text-primary hover:underline",children:a})},table({children:r}){return g.jsx("div",{className:"overflow-x-auto my-3",children:g.jsx("table",{className:"min-w-full text-sm",children:r})})}},children:t})})}function AV({message:e,sessionId:t}){return e.role==="user"?g.jsx("div",{className:"flex justify-end",children:g.jsxs("div",{className:"flex gap-3 max-w-[80%]",children:[g.jsx("div",{className:"rounded-2xl px-4 py-2.5 bg-primary text-primary-foreground",children:g.jsx("p",{className:"text-sm whitespace-pre-wrap",children:e.content})}),g.jsx(Zu,{role:"user"})]})}):g.jsxs("div",{className:"flex gap-3 group",children:[g.jsx(Zu,{role:"assistant"}),g.jsxs("div",{className:"flex-1 min-w-0",children:[e.toolCalls&&e.toolCalls.length>0&&g.jsx("div",{className:"mb-2 space-y-1",children:e.toolCalls.map((a,o)=>g.jsx(EO,{toolCall:a},o))}),g.jsx(vO,{content:e.content}),e.elements&&e.elements.length>0&&g.jsx("div",{className:"mt-3 space-y-3",children:e.elements.map((a,o)=>g.jsx(jv,{element:a,className:"w-full"},o))}),e.images&&e.images.length>0&&g.jsx("div",{className:"mt-2 flex flex-wrap gap-2",children:e.images.map((a,o)=>g.jsx("img",{src:a,alt:"",className:"max-w-full rounded-md max-h-64 object-contain"},o))}),e.actions&&e.actions.length>0&&g.jsx(mV,{actions:e.actions,messageId:e.id,sessionId:t}),g.jsxs("div",{className:"mt-1 flex items-center gap-1 relative",children:[g.jsx(vV,{text:e.content}),g.jsx(SV,{messageId:e.id})]})]})]})}function kV({onSend:e,onCancel:t,isStreaming:r=!1,placeholder:a="Type a message...",enableFileUpload:o=!1}){const[s,u]=S.useState(""),[d,p]=S.useState([]),f=S.useRef(null),h=S.useRef(null),b=S.useCallback(()=>{const A=s.trim();(A||d.length>0)&&!r&&(e(A,d.length>0?d:void 0),u(""),p([]),f.current&&(f.current.style.height="auto"))},[s,d,r,e]),v=S.useCallback(A=>{const _=Array.from(A.target.files||[]);p(R=>[...R,..._]),h.current&&(h.current.value="")},[]),E=S.useCallback(()=>{h.current?.click()},[]),w=S.useCallback(A=>{p(_=>_.filter((R,L)=>L!==A))},[]),T=S.useCallback(A=>{A.key==="Enter"&&!A.shiftKey&&(A.preventDefault(),b())},[b]),k=S.useCallback(()=>{const A=f.current;A&&(A.style.height="auto",A.style.height=`${Math.min(A.scrollHeight,200)}px`)},[]);return g.jsx("div",{className:"p-4 pb-6",children:g.jsxs("div",{className:"max-w-3xl mx-auto",children:[g.jsxs("div",{className:"rounded-2xl border bg-background/80 backdrop-blur-sm shadow-lg",children:[d.length>0&&g.jsx("div",{className:"flex flex-wrap gap-1 px-4 pt-3",children:d.map((A,_)=>g.jsxs("span",{className:"inline-flex items-center gap-1 px-2 py-1 bg-muted rounded-lg text-xs",children:[g.jsx("span",{className:"truncate max-w-[100px]",children:A.name}),g.jsx("button",{type:"button",onClick:()=>w(_),className:"hover:text-destructive",children:"×"})]},`${A.name}-${_}`))}),g.jsxs("div",{className:"flex items-end gap-2 p-2",children:[o&&g.jsxs(g.Fragment,{children:[g.jsx("input",{ref:h,type:"file",multiple:!0,onChange:v,className:"hidden",accept:"image/*,audio/*,video/*,.pdf,.doc,.docx,.txt,.csv,.json"}),g.jsx("button",{type:"button",onClick:E,className:"p-2 rounded-xl hover:bg-accent text-muted-foreground transition-colors",title:"Attach file",children:g.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:g.jsx("path",{d:"m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"})})})]}),g.jsx("textarea",{ref:f,value:s,onChange:A=>u(A.target.value),onKeyDown:T,onInput:k,placeholder:a,disabled:r,rows:1,className:"flex-1 resize-none bg-transparent px-3 py-2.5 text-sm focus:outline-none disabled:opacity-50 placeholder:text-muted-foreground/60",style:{maxHeight:"200px"}}),r?g.jsx("button",{type:"button",onClick:t,className:"p-2 rounded-xl bg-destructive text-destructive-foreground hover:bg-destructive/90 transition-colors",title:"Stop",children:g.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",width:"18",height:"18",viewBox:"0 0 24 24",fill:"currentColor",children:g.jsx("rect",{x:"6",y:"6",width:"12",height:"12",rx:"2"})})}):g.jsx("button",{type:"button",onClick:b,disabled:!s.trim(),className:"p-2 rounded-xl bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-30 disabled:cursor-not-allowed transition-colors",title:"Send",children:g.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:g.jsx("path",{d:"M5 12h14M12 5l7 7-7 7"})})})]})]}),g.jsx("p",{className:"text-[10px] text-center text-muted-foreground/50 mt-2",children:"Press Enter to send, Shift+Enter for new line"
Line truncated
|
||||
`),b=S.useCallback(async()=>{!r.trim()||s||p(r)},[r,s,p]),v=S.useCallback(()=>{a("")},[]);return g.jsxs("div",{className:"min-h-screen bg-background text-foreground flex flex-col",children:[g.jsxs("header",{className:"border-b px-6 py-4 flex items-center justify-between",children:[g.jsx("h1",{className:"text-xl font-semibold",children:t||"Playground"}),g.jsx("div",{className:"flex items-center gap-2",children:g.jsx("button",{onClick:v,className:"px-3 py-1.5 text-sm rounded-md border hover:bg-accent",children:"Clear"})})]}),g.jsxs("div",{className:"flex-1 flex",children:[g.jsxs("div",{className:"flex-1 flex flex-col border-r",children:[g.jsx("div",{className:"px-4 py-2 border-b bg-muted/30",children:g.jsx("span",{className:"text-sm font-medium text-muted-foreground",children:"Input"})}),g.jsx("div",{className:"flex-1 p-4",children:g.jsx("textarea",{value:r,onChange:E=>a(E.target.value),placeholder:"Enter your prompt here...",className:"w-full h-full resize-none bg-transparent border rounded-md p-3 focus:outline-none focus:ring-2 focus:ring-primary"})}),g.jsx("div",{className:"px-4 py-3 border-t flex justify-end gap-2",children:s?g.jsx("button",{onClick:f,className:"px-4 py-2 bg-destructive text-destructive-foreground rounded-md hover:bg-destructive/90",children:"Stop"}):g.jsx("button",{onClick:b,disabled:!r.trim(),className:"px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed",children:"Submit"})})]}),g.jsxs("div",{className:"flex-1 flex flex-col",children:[g.jsxs("div",{className:"px-4 py-2 border-b bg-muted/30 flex items-center justify-between",children:[g.jsx("span",{className:"text-sm font-medium text-muted-foreground",children:"Output"}),s&&g.jsx("span",{className:"text-xs text-muted-foreground animate-pulse",children:"Processing..."})]}),g.jsx("div",{className:"flex-1 p-4 overflow-auto",children:h?g.jsx("div",{className:"prose prose-sm dark:prose-invert max-w-none whitespace-pre-wrap",children:h}):g.jsx("div",{className:"h-full flex items-center justify-center text-muted-foreground text-sm",children:"Output will appear here"})})]})]}),e?.features&&g.jsxs("div",{className:"border-t px-6 py-3 bg-muted/20 flex items-center gap-4 text-sm text-muted-foreground",children:[e.features.streaming&&g.jsx("span",{children:"✓ Streaming"}),e.features.reasoning&&g.jsx("span",{children:"✓ Reasoning"}),e.features.tools&&g.jsx("span",{children:"✓ Tools"})]})]})}function _u({enabled:e}){return e?g.jsx("a",{href:"#main-content",className:"sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 z-50 bg-primary text-primary-foreground px-4 py-2 rounded-md text-sm font-medium",children:"Skip to main content"}):null}function jV(){const[e,t]=S.useState({}),[r,a]=S.useState({}),[o,s]=S.useState({}),[u,d]=S.useState(!0),[p,f]=S.useState(null),[h,b]=S.useState(null),[v,E]=S.useState("");S.useEffect(()=>{async function L(){try{const[j,D,z]=await Promise.all([fetch("/ui-config.json"),fetch("/docs-nav.json"),fetch("/route-manifest.json")]);if(!j.ok||!D.ok||!z.ok)throw new Error("Failed to load manifests");const F=await j.json(),V=await D.json();t(F),a(V),s(await z.json());const M=F.site?.theme;K1(M?.preset||"zinc",M?.darkMode!==!1,M?.radius||"md");const H=window.location.pathname;if(H&&H!=="/"){const K=Z=>{for(const te of Z){if(te.path===H||te.path===H.replace(/^\//,""))return te;if(te.children){const Q=K(te.children);if(Q)return Q}}return null};if(V.items){for(const Z of V.items)if(Z.children){const te=K(Z.children);if(te){b(te),E(te.path||te.title);break}}}}}catch(j){f(j instanceof Error?j.message:"Unknown error")}finally{d(!1)}}L()},[]),S.useEffect(()=>{const L=()=>{const j=window.location.pathname,D=F=>{for(const V of F){if(V.path===j||V.path===j.replace(/^\//,""))return V;if(V.children){const M=D(V.children);if(M)return M}}return null};let z=null;if(r.items){for(const F of r.items)if(F.children&&(z=D(F.children),z))break}if(z){b(z),E(z.path||z.title);const F=e.site?.title||"Documentation",V=e.seo?.titleTemplate||"%s | %s";if(V.includes("%s")){let M=V.replace("%s",z.title);M.includes("%s")&&(M=M.replace("%s",F)),document.title=M}else document.title=`${z.title} | ${F}`}else j==="/"&&(b(null),E(""),document.title=e.site?.title||"Documentation")};return window.addEventListener("popstate",L),()=>window.removeEventListener("popstate",L)},[r,e]);const w=(L,j,D)=>{const z=e.seo?.titleTemplate||"%s | %s",F=e.site?.title||"Documentation";if(z.includes("%s")){let Z=z.replace("%s",L);Z.includes("%s")&&(Z=Z.replace("%s",F)),document.title=Z}else document.title=`${L} | ${F}`;let V=document.querySelector('link[rel="canonical"]');V||(V=document.createElement("link"),V.rel="canonical",document.head.appendChild(V)),V.href=window.location.origin+j;let M=document.querySelector('meta[name="description"]');M||(M=document.createElement("meta"),M.name="description",document.head.appendChild(M)),M.content=`${L} - ${e.site?.description||""}`;let H=document.querySele
Line truncated
|
||||
`)}function Cq({language:e,children:t}){const[r,a]=S.useState(!1),o=S.useCallback(async()=>{await navigator.clipboard.writeText(t),a(!0),setTimeout(()=>a(!1),2e3)},[t]);return g.jsxs("div",{className:"relative group/code my-3 rounded-lg overflow-hidden",children:[g.jsxs("div",{className:"flex items-center justify-between px-4 py-1.5 bg-[#1e1e2e] text-xs text-gray-400",children:[g.jsx("span",{children:e||"code"}),g.jsx("button",{onClick:o,className:"flex items-center gap-1 hover:text-white transition-colors",children:r?"✓ Copied":"Copy"})]}),g.jsx(rC,{style:N4,language:e||"text",PreTag:"div",customStyle:{margin:0,borderRadius:0,fontSize:"13px"},children:t})]})}function Iq(e){const t=e.split(`
|
||||
`);let r=!1,a=!1,o="",s=0;for(let u=0;u<t.length;u++){const c=t[u];if(/^[ \t]*```/.test(c)){r=!r;continue}if(r)continue;const p=c.match(/^([ \t]*)(\d+)\.[ \t]/);if(p){const f=p[1];!a||f!==o?(a=!0,o=f,s=1):s++,t[u]=c.replace(/^([ \t]*)\d+\./,`${f}${s}.`)}else c.trim()===""&&a||a&&/^[ \t]+\S/.test(c)||a&&(a=!1,s=0)}return t.join(`
|
||||
`)}function nO({content:e}){const t=Iq(e);return g.jsx("div",{className:"prose prose-sm dark:prose-invert max-w-none prose-p:leading-relaxed prose-pre:p-0 prose-pre:bg-transparent",children:g.jsx(mR,{remarkPlugins:[BR],components:{ul({children:r,ordered:a,start:o}){if(a){const s=o!=null?Number(o):1;return g.jsx("ol",{start:s,style:{listStyleType:"decimal",paddingLeft:"1.5rem",margin:"0.5rem 0"},children:r})}return g.jsx("ul",{style:{listStyleType:"disc",paddingLeft:"1.5rem",margin:"0.5rem 0"},children:r})},li({children:r}){return g.jsx("li",{style:{display:"list-item",marginBottom:"0.25rem"},children:r})},code({className:r,children:a,...o}){const s=/language-(\w+)/.exec(r||""),u=Rq(String(a));return!s&&!u.includes(`
|
||||
`)?g.jsx("code",{className:"px-1.5 py-0.5 rounded bg-muted text-sm font-mono",...o,children:a}):g.jsx(Cq,{language:s?.[1],children:u})},a({href:r,children:a}){return g.jsx("a",{href:r,target:"_blank",rel:"noopener noreferrer",className:"text-primary hover:underline",children:a})},table({children:r}){return g.jsx("div",{className:"overflow-x-auto my-3",children:g.jsx("table",{className:"min-w-full text-sm",children:r})})}},children:t})})}function Oq({message:e,sessionId:t}){return e.role==="user"?g.jsx("div",{className:"flex justify-end",children:g.jsxs("div",{className:"flex gap-3 max-w-[80%]",children:[g.jsx("div",{className:"rounded-2xl px-4 py-2.5 bg-primary text-primary-foreground",children:g.jsx("p",{className:"text-sm whitespace-pre-wrap",children:e.content})}),g.jsx(rc,{role:"user"})]})}):g.jsxs("div",{className:"flex gap-3 group",children:[g.jsx(rc,{role:"assistant"}),g.jsxs("div",{className:"flex-1 min-w-0",children:[e.toolCalls&&e.toolCalls.length>0&&g.jsx("div",{className:"mb-2 space-y-1",children:e.toolCalls.map((a,o)=>g.jsx(tO,{toolCall:a},o))}),g.jsx(nO,{content:e.content}),e.elements&&e.elements.length>0&&g.jsx("div",{className:"mt-3 space-y-3",children:e.elements.map((a,o)=>g.jsx(QE,{element:a,className:"w-full"},o))}),e.images&&e.images.length>0&&g.jsx("div",{className:"mt-2 flex flex-wrap gap-2",children:e.images.map((a,o)=>g.jsx("img",{src:a,alt:"",className:"max-w-full rounded-md max-h-64 object-contain"},o))}),e.actions&&e.actions.length>0&&g.jsx(xq,{actions:e.actions,messageId:e.id,sessionId:t}),g.jsxs("div",{className:"mt-1 flex items-center gap-1 relative",children:[g.jsx(_q,{text:e.content}),g.jsx(Nq,{messageId:e.id})]})]})]})}function Lq({onSend:e,onCancel:t,isStreaming:r=!1,placeholder:a="Type a message...",enableFileUpload:o=!1}){const[s,u]=S.useState(""),[c,p]=S.useState([]),f=S.useRef(null),h=S.useRef(null),m=S.useCallback(()=>{const A=s.trim();(A||c.length>0)&&!r&&(e(A,c.length>0?c:void 0),u(""),p([]),f.current&&(f.current.style.height="auto"))},[s,c,r,e]),v=S.useCallback(A=>{const _=Array.from(A.target.files||[]);p(N=>[...N,..._]),h.current&&(h.current.value="")},[]),E=S.useCallback(()=>{h.current?.click()},[]),x=S.useCallback(A=>{p(_=>_.filter((N,D)=>D!==A))},[]),w=S.useCallback(A=>{A.key==="Enter"&&!A.shiftKey&&(A.preventDefault(),m())},[m]),k=S.useCallback(()=>{const A=f.current;A&&(A.style.height="auto",A.style.height=`${Math.min(A.scrollHeight,200)}px`)},[]);return g.jsx("div",{className:"p-4 pb-6",children:g.jsxs("div",{className:"max-w-3xl mx-auto",children:[g.jsxs("div",{className:"rounded-2xl border bg-background/80 backdrop-blur-sm shadow-lg",children:[c.length>0&&g.jsx("div",{className:"flex flex-wrap gap-1 px-4 pt-3",children:c.map((A,_)=>g.jsxs("span",{className:"inline-flex items-center gap-1 px-2 py-1 bg-muted rounded-lg text-xs",children:[g.jsx("span",{className:"truncate max-w-[100px]",children:A.name}),g.jsx("button",{type:"button",onClick:()=>x(_),className:"hover:text-destructive",children:"×"})]},`${A.name}-${_}`))}),g.jsxs("div",{className:"flex items-end gap-2 p-2",children:[o&&g.jsxs(g.Fragment,{children:[g.jsx("input",{ref:h,type:"file",multiple:!0,onChange:v,className:"hidden",accept:"image/*,audio/*,video/*,.pdf,.doc,.docx,.txt,.csv,.json"}),g.jsx("button",{type:"button",onClick:E,className:"p-2 rounded-xl hover:bg-accent text-muted-foreground transition-colors",title:"Attach file",children:g.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:g.jsx("path",{d:"m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"})})})]}),g.jsx("textarea",{ref:f,value:s,onChange:A=>u(A.target.value),onKeyDown:w,onInput:k,placeholder:a,disabled:r,rows:1,className:"flex-1 resize-none bg-transparent px-3 py-2.5 text-sm focus:outline-none disabled:opacity-50 placeholder:text-muted-foreground/60",style:{maxHeight:"200px"}}),r?g.jsx("button",{type:"button",onClick:t,className:"p-2 rounded-xl bg-destructive text-destructive-foreground hover:bg-destructive/90 transition-colors",title:"Stop",children:g.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",width:"18",height:"18",viewBox:"0 0 24 24",fill:"currentColor",children:g.jsx("rect",{x:"6",y:"6",width:"12",height:"12",rx:"2"})})}):g.jsx("button",{type:"button",onClick:m,disabled:!s.trim(),className:"p-2 rounded-xl bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-30 disabled:cursor-not-allowed transition-colors",title:"Send",children:g.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:g.jsx("path",{d:"M5 12h14M12 5l7 7-7 7"})})})]})]}),g.jsx("p",{className:"text-[10px] text-center text-muted-foreground/50 mt-2",children:"Press Enter to send, Shift+Enter for new line"
Line truncated
|
||||
`),m=S.useCallback(async()=>{!r.trim()||s||p(r)},[r,s,p]),v=S.useCallback(()=>{a("")},[]);return g.jsxs("div",{className:"min-h-screen bg-background text-foreground flex flex-col",children:[g.jsxs("header",{className:"border-b px-6 py-4 flex items-center justify-between",children:[g.jsx("h1",{className:"text-xl font-semibold",children:t||"Playground"}),g.jsx("div",{className:"flex items-center gap-2",children:g.jsx("button",{onClick:v,className:"px-3 py-1.5 text-sm rounded-md border hover:bg-accent",children:"Clear"})})]}),g.jsxs("div",{className:"flex-1 flex",children:[g.jsxs("div",{className:"flex-1 flex flex-col border-r",children:[g.jsx("div",{className:"px-4 py-2 border-b bg-muted/30",children:g.jsx("span",{className:"text-sm font-medium text-muted-foreground",children:"Input"})}),g.jsx("div",{className:"flex-1 p-4",children:g.jsx("textarea",{value:r,onChange:E=>a(E.target.value),placeholder:"Enter your prompt here...",className:"w-full h-full resize-none bg-transparent border rounded-md p-3 focus:outline-none focus:ring-2 focus:ring-primary"})}),g.jsx("div",{className:"px-4 py-3 border-t flex justify-end gap-2",children:s?g.jsx("button",{onClick:f,className:"px-4 py-2 bg-destructive text-destructive-foreground rounded-md hover:bg-destructive/90",children:"Stop"}):g.jsx("button",{onClick:m,disabled:!r.trim(),className:"px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed",children:"Submit"})})]}),g.jsxs("div",{className:"flex-1 flex flex-col",children:[g.jsxs("div",{className:"px-4 py-2 border-b bg-muted/30 flex items-center justify-between",children:[g.jsx("span",{className:"text-sm font-medium text-muted-foreground",children:"Output"}),s&&g.jsx("span",{className:"text-xs text-muted-foreground animate-pulse",children:"Processing..."})]}),g.jsx("div",{className:"flex-1 p-4 overflow-auto",children:h?g.jsx("div",{className:"prose prose-sm dark:prose-invert max-w-none whitespace-pre-wrap",children:h}):g.jsx("div",{className:"h-full flex items-center justify-center text-muted-foreground text-sm",children:"Output will appear here"})})]})]}),e?.features&&g.jsxs("div",{className:"border-t px-6 py-3 bg-muted/20 flex items-center gap-4 text-sm text-muted-foreground",children:[e.features.streaming&&g.jsx("span",{children:"✓ Streaming"}),e.features.reasoning&&g.jsx("span",{children:"✓ Reasoning"}),e.features.tools&&g.jsx("span",{children:"✓ Tools"})]})]})}function v9(e,t){const r=e.replace(/^\/+|\/+$/g,""),o=t.replace(/^\/+|\/+$/g,"").replace(/\*\*/g,"{{GLOB}}").replace(/\*/g,"[^/]+").replace(/\{\{GLOB\}\}/g,".*");return new RegExp(`^${o}$`).test(r)}function E9(e,t,r){const o=[...t.routes??[]].sort((s,u)=>(u.priority??0)-(s.priority??0));for(const s of o){if(!v9(e,s.pattern))continue;const u=r[s.template];return u?{template:s.template,layout:u.layout??"ThreeColumnLayout",slots:u.slots??{},slotOverrides:s.slotOverrides}:(console.warn(`Template "${s.template}" not found`),null)}return null}function S9(e,t){if(t)return t.slotOverrides&&e in t.slotOverrides?t.slotOverrides[e]:t.slots[e]}function x9(e){return S9("right",e)!==null}function Lu({enabled:e}){return e?g.jsx("a",{href:"#main-content",className:"sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 z-50 bg-primary text-primary-foreground px-4 py-2 rounded-md text-sm font-medium",children:"Skip to main content"}):null}function w9(){const[e,t]=S.useState({}),[r,a]=S.useState({}),[o,s]=S.useState({}),[u,c]=S.useState(!0),[p,f]=S.useState(null),[h,m]=S.useState(null),[v,E]=S.useState(""),[x,w]=S.useState(()=>window.location.pathname.replace(/\/$/,"")||"/"),[k,A]=S.useState(!1),_=(M,H)=>{const Q=H.replace(/\/$/,"")||"/",de=I=>{for(const U of I){const B=(U.path??"").replace(/\/$/,"");if(B===Q||B===Q.replace(/^\//,""))return U;if(U.children){const C=de(U.children);if(C)return C}}return null};return M.items?de(M.items):null},N=(M,H,Q,de)=>{const I=de??e,U=I.seo?.titleTemplate||"%s | %s",B=I.site?.title||"Documentation";if(U.includes("%s")){let ie=U.replace("%s",M);ie.includes("%s")&&(ie=ie.replace("%s",B)),document.title=ie}else document.title=`${M} | ${B}`;let C=document.querySelector('link[rel="canonical"]');C||(C=document.createElement("link"),C.rel="canonical",document.head.appendChild(C)),C.href=window.location.origin+H;let J=document.querySelector('meta[name="description"]');J||(J=document.createElement("meta"),J.name="description",document.head.appendChild(J)),J.content=`${M} - ${I.site?.description||""}`;let ae=document.querySelector('meta[property="og:title"]');ae||(ae=document.createElement("meta"),ae.setAttribute("property","og:title"),document.head.appendChild(ae)),ae.content=M;let ue=document.querySelector('meta[property="og:url"]');if(ue||(ue=document.createElement("meta"),ue.setAttribute("property","og:url"),document.head.appendChild(ue)),ue.content=window.location.origin+H,I.seo?.twitter&&Object.entries(I.seo.twitter).forEach(([ie,he])=>{let fe=document.querySel
Line truncated
|
||||
@@ -475,6 +475,7 @@
|
||||
}
|
||||
|
||||
/* ── Light Theme (Gap 19) ─────────────────────────────────────────*/
|
||||
:root:not(.dark) .chat-container,
|
||||
[data-theme="light"] .chat-container,
|
||||
.chat-container.light {
|
||||
--chat-bg: #fff;
|
||||
|
||||
@@ -525,6 +525,7 @@ export function createInteractiveBoard(root, opts = {}) {
|
||||
selected,
|
||||
onSelect: opts.interactive !== false ? onSelect : null,
|
||||
onOpen: (card) => {
|
||||
if (typeof opts.onOpen === 'function') { opts.onOpen(card); return; }
|
||||
if (openDrawer && openDrawer.isOpen()) openDrawer.close();
|
||||
openDrawer = openTaskDrawer(card, {
|
||||
apiBase: drawerApiBase,
|
||||
|
||||
@@ -82,6 +82,7 @@ function addCopyButtons(root) {
|
||||
if (pre.dataset.copyProcessed) return;
|
||||
if (pre.closest('.mermaid-diagram')) return;
|
||||
if (pre.querySelector('.mermaid')) return;
|
||||
if (pre.querySelector('code.language-mermaid')) return;
|
||||
|
||||
pre.dataset.copyProcessed = 'true';
|
||||
|
||||
|
||||
@@ -122,6 +122,8 @@ async function loadContent(root) {
|
||||
|
||||
// Update the "On This Page" ToC if toc plugin is active
|
||||
updateTocSidebar(article);
|
||||
|
||||
window.dispatchEvent(new CustomEvent('aiui:content-loaded', { detail: { root: article } }));
|
||||
|
||||
console.debug('[AIUI:content-loader] Loaded content from', mdPath);
|
||||
} catch (err) {
|
||||
@@ -163,15 +165,73 @@ function updateTocSidebar(article) {
|
||||
|
||||
/* ───────── Markdown → HTML Converter ───────── */
|
||||
|
||||
function convertMkdocsTabs(md) {
|
||||
const lines = md.split('\n');
|
||||
const out = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < lines.length) {
|
||||
const tabMatch = lines[i].match(/^===\s+"([^"]+)"\s*$/);
|
||||
if (!tabMatch) {
|
||||
out.push(lines[i]);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const title = tabMatch[1];
|
||||
i++;
|
||||
while (i < lines.length && lines[i].trim() === '') i++;
|
||||
|
||||
const block = [];
|
||||
while (i < lines.length) {
|
||||
if (/^===\s+"[^"]+"\s*$/.test(lines[i])) break;
|
||||
if (lines[i].trim() === '' && i + 1 < lines.length && /^===\s+"[^"]+"\s*$/.test(lines[i + 1])) break;
|
||||
|
||||
if (lines[i].startsWith(' ') || lines[i].trimStart().startsWith('```')) {
|
||||
block.push(lines[i].replace(/^ /, ''));
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (block.length > 0 && lines[i].trim() === '') {
|
||||
block.push('');
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
out.push(`### \u200Btab:${title}`);
|
||||
out.push('');
|
||||
out.push(...block);
|
||||
out.push('');
|
||||
}
|
||||
|
||||
return out.join('\n');
|
||||
}
|
||||
|
||||
function markdownToHtml(md) {
|
||||
// Clean up MkDocs-specific syntax
|
||||
// Clean up MkDocs-specific syntax (mirrors src/frontend/src/markdown/mkdocsPreprocess.ts)
|
||||
let html = md
|
||||
.replace(/<div[^>]*markdown[^>]*>/gi, '')
|
||||
.replace(/<\/div>/gi, '')
|
||||
.replace(/===\s+"([^"]+)"/g, '**$1**')
|
||||
.replace(/<div[^>]*markdown[^>]*>\s*/gi, '')
|
||||
.replace(/<\/div>\s*/gi, '');
|
||||
|
||||
const iconMap = {
|
||||
':material-file-document:': '📄',
|
||||
':material-puzzle:': '🧩',
|
||||
':material-palette:': '🎨',
|
||||
':material-rocket-launch:': '🚀',
|
||||
};
|
||||
for (const [shortcode, emoji] of Object.entries(iconMap)) {
|
||||
html = html.replaceAll(shortcode, emoji);
|
||||
}
|
||||
html = html
|
||||
.replace(/:material-[\w-]+:/g, '•')
|
||||
.replace(/:octicons-[\w-]+(?:-\d+)?:/g, '•');
|
||||
|
||||
html = convertMkdocsTabs(html);
|
||||
|
||||
const lines = html.split('\n');
|
||||
const result = [];
|
||||
let inCodeBlock = false, codeContent = '', codeLang = '';
|
||||
@@ -207,7 +267,7 @@ function markdownToHtml(md) {
|
||||
// Code blocks
|
||||
if (line.trimStart().startsWith('```')) {
|
||||
if (inCodeBlock) {
|
||||
result.push(`<pre class="bg-gray-900 rounded-lg p-4 my-4 overflow-x-auto"><code class="language-${codeLang}">${escapeHtml(codeContent.trim())}</code></pre>`);
|
||||
result.push(`<pre class="bg-muted text-foreground border border-border rounded-lg p-4 my-4 overflow-x-auto"><code class="language-${codeLang}">${escapeHtml(codeContent.trim())}</code></pre>`);
|
||||
inCodeBlock = false; codeContent = '';
|
||||
} else {
|
||||
flushList(); flushTable(); inCodeBlock = true;
|
||||
@@ -292,8 +352,8 @@ function inlineMarkdown(text) {
|
||||
return text
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||
.replace(/`([^`]+)`/g, '<code class="bg-gray-800 px-1.5 py-0.5 rounded text-sm">$1</code>')
|
||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" class="text-blue-400 hover:text-blue-300 underline">$1</a>');
|
||||
.replace(/`([^`]+)`/g, '<code class="bg-primary/10 text-primary px-1.5 py-0.5 rounded text-sm font-mono">$1</code>')
|
||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" class="text-primary hover:underline">$1</a>');
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
@@ -376,6 +436,8 @@ async function navigateToContent(targetPath) {
|
||||
// Update the "On This Page" ToC
|
||||
updateTocSidebar(article);
|
||||
|
||||
window.dispatchEvent(new CustomEvent('aiui:content-loaded', { detail: { root: article } }));
|
||||
|
||||
// Remove anti-flicker CSS (critical: ensures content is visible)
|
||||
const af = document.getElementById('aiui-anti-flicker');
|
||||
if (af) af.remove();
|
||||
|
||||
@@ -156,6 +156,7 @@ const BUILTIN_VIEWS = {
|
||||
approvals: '/plugins/views/approvals.js',
|
||||
usage: '/plugins/views/usage.js',
|
||||
channels: '/plugins/views/channels.js',
|
||||
inbox: '/plugins/views/inbox.js',
|
||||
skills: '/plugins/views/skills.js',
|
||||
tools: '/plugins/views/skills.js',
|
||||
nodes: '/plugins/views/nodes.js',
|
||||
@@ -171,10 +172,13 @@ const BUILTIN_VIEWS = {
|
||||
feedback: '/plugins/views/feedback.js',
|
||||
jobs: '/plugins/views/jobs.js',
|
||||
kanban: '/plugins/views/kanban.js',
|
||||
work: '/plugins/views/work-hub.js',
|
||||
'jobs-board': '/plugins/views/jobs-board.js',
|
||||
auth: '/plugins/views/auth.js',
|
||||
api: '/plugins/views/api.js',
|
||||
mcp: '/plugins/views/mcp.js',
|
||||
marketplace: '/plugins/views/marketplace.js',
|
||||
'code-studio': '/plugins/views/code-studio.js',
|
||||
};
|
||||
|
||||
const SLOT_REGISTRY = {};
|
||||
@@ -1278,7 +1282,374 @@ function initSessionSearch() {
|
||||
window.aiui = window.aiui || {};
|
||||
window.aiui.openSessionSearch = openSessionSearch;
|
||||
|
||||
// ── First-run Onboarding Gate ────────────────────────────────────────
|
||||
// Shown on first visit when running in gateway mode and unauthenticated.
|
||||
// Replaces the raw HTTP 401 experience with a guided token flow.
|
||||
const ONBOARD_SKIP_KEY = 'aiui_onboard_skip';
|
||||
const ONBOARD_TOKEN_KEY = 'aiui_gateway_token';
|
||||
|
||||
function getStoredToken() {
|
||||
try {
|
||||
return window.sessionStorage.getItem(ONBOARD_TOKEN_KEY) || '';
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// Attach the stored gateway token to same-origin requests so that protected
|
||||
// dashboard APIs (/api/pages, /ui-config.json, view modules, …) authenticate
|
||||
// after onboarding. Wrapping window.fetch once keeps every existing caller
|
||||
// unchanged and is a no-op when no token is stored or in standalone mode.
|
||||
let _authFetchInstalled = false;
|
||||
function installAuthFetch() {
|
||||
if (_authFetchInstalled || typeof window.fetch !== 'function') return;
|
||||
_authFetchInstalled = true;
|
||||
const nativeFetch = window.fetch.bind(window);
|
||||
|
||||
function isSameOrigin(url) {
|
||||
try {
|
||||
return new URL(url, window.location.href).origin === window.location.origin;
|
||||
} catch (e) {
|
||||
return true; // relative/opaque URLs are same-origin
|
||||
}
|
||||
}
|
||||
|
||||
window.fetch = function (input, init) {
|
||||
const token = getStoredToken();
|
||||
if (!token) return nativeFetch(input, init);
|
||||
|
||||
const url = typeof input === 'string' ? input : (input && input.url) || '';
|
||||
if (!isSameOrigin(url)) return nativeFetch(input, init);
|
||||
|
||||
const opts = { ...(init || {}) };
|
||||
const headers = new Headers(
|
||||
opts.headers || (typeof input !== 'string' && input && input.headers) || undefined
|
||||
);
|
||||
if (!headers.has('Authorization')) headers.set('Authorization', `Bearer ${token}`);
|
||||
opts.headers = headers;
|
||||
if (!opts.credentials) opts.credentials = 'same-origin';
|
||||
return nativeFetch(input, opts);
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchAuthStatus() {
|
||||
try {
|
||||
const headers = {};
|
||||
const token = getStoredToken();
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
const res = await fetch('/api/auth/status', { headers, credentials: 'same-origin' });
|
||||
if (!res.ok) return { authenticated: false, mode: 'gateway', auth_required: true };
|
||||
return await res.json();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function consumeToken(token) {
|
||||
if (!token) return false;
|
||||
try {
|
||||
const res = await fetch('/api/auth/status', {
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
if (!res.ok) return false;
|
||||
const data = await res.json();
|
||||
if (data && data.authenticated) {
|
||||
try { window.sessionStorage.setItem(ONBOARD_TOKEN_KEY, token); } catch (e) { /* ignore */ }
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function probeGatewayReachable(urlString) {
|
||||
let base;
|
||||
try {
|
||||
base = new URL(urlString);
|
||||
} catch (e) {
|
||||
return { ok: false, reason: 'invalid_url' };
|
||||
}
|
||||
if (base.protocol !== 'http:' && base.protocol !== 'https:') {
|
||||
return { ok: false, reason: 'invalid_url' };
|
||||
}
|
||||
|
||||
const probe = new URL('/api/auth/status', base);
|
||||
const sameOrigin = base.origin === window.location.origin;
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 5000);
|
||||
|
||||
try {
|
||||
const res = await fetch(probe.toString(), {
|
||||
method: 'GET',
|
||||
credentials: 'omit',
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
if (res.type === 'opaque' || res.status === 0) {
|
||||
return { ok: false, reason: 'opaque' };
|
||||
}
|
||||
return { ok: true, status: res.status };
|
||||
} catch (e) {
|
||||
clearTimeout(timeout);
|
||||
if (e.name === 'AbortError') {
|
||||
return { ok: false, reason: 'timeout' };
|
||||
}
|
||||
// A rejected fetch on a cross-origin URL is ambiguous: the gateway may be
|
||||
// up but blocking CORS, or genuinely unreachable. The browser cannot tell
|
||||
// these apart, so surface the "Continue anyway" opt-in rather than a
|
||||
// definitive unreachable error. Same-origin rejections are genuine.
|
||||
return { ok: false, reason: sameOrigin ? 'network' : 'opaque' };
|
||||
}
|
||||
}
|
||||
|
||||
async function maybeShowOnboarding() {
|
||||
let params;
|
||||
try {
|
||||
params = new URLSearchParams(window.location.search);
|
||||
} catch (e) {
|
||||
params = null;
|
||||
}
|
||||
|
||||
// Magic link: ?token= auto-consume, then strip from history.
|
||||
const urlToken = params ? params.get('token') : null;
|
||||
if (urlToken) {
|
||||
const ok = await consumeToken(urlToken);
|
||||
try {
|
||||
params.delete('token');
|
||||
const qs = params.toString();
|
||||
const clean = window.location.pathname + (qs ? `?${qs}` : '') + window.location.hash;
|
||||
window.history.replaceState({}, document.title, clean);
|
||||
} catch (e) { /* ignore */ }
|
||||
if (ok) return;
|
||||
}
|
||||
|
||||
const status = await fetchAuthStatus();
|
||||
if (!status) return;
|
||||
|
||||
// Standalone / no-auth / already authenticated → straight to dashboard.
|
||||
if (status.authenticated) return;
|
||||
if (status.mode === 'none' || status.mode === 'standalone') return;
|
||||
if (status.auth_required === false) return;
|
||||
|
||||
// "Don't show again" persisted for this device.
|
||||
try {
|
||||
if (window.localStorage.getItem(ONBOARD_SKIP_KEY) === '1') return;
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
await renderOnboarding(status);
|
||||
}
|
||||
|
||||
function renderOnboarding(status) {
|
||||
return new Promise((resolve) => {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'aiui-onboarding';
|
||||
overlay.setAttribute('role', 'dialog');
|
||||
overlay.setAttribute('aria-modal', 'true');
|
||||
overlay.setAttribute('aria-label', 'Welcome to PraisonAIUI');
|
||||
overlay.style.cssText = 'position:fixed;inset:0;z-index:20000;display:flex;align-items:center;justify-content:center;background:var(--db-bg,#0b0e14);padding:16px';
|
||||
|
||||
const gatewayUrl = (status && status.gateway_url) || window.location.origin;
|
||||
const state = { step: 1 };
|
||||
|
||||
function close() {
|
||||
overlay.remove();
|
||||
resolve();
|
||||
}
|
||||
|
||||
function card(inner) {
|
||||
return `<div style="background:var(--db-card-bg,#151922);border:1px solid var(--db-border,#2a2f3a);border-radius:12px;padding:28px;max-width:440px;width:100%;box-shadow:0 8px 40px rgba(0,0,0,0.4)">${inner}</div>`;
|
||||
}
|
||||
|
||||
function steps() {
|
||||
let dots = '';
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
const on = i === state.step;
|
||||
dots += `<span aria-current="${on ? 'step' : 'false'}" style="width:8px;height:8px;border-radius:50%;background:${on ? 'var(--db-accent,#4f8cff)' : 'var(--db-border,#2a2f3a)'}"></span>`;
|
||||
}
|
||||
return `<div style="display:flex;gap:8px;justify-content:center;margin-bottom:20px">${dots}</div>`;
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (state.step === 1) renderWelcome();
|
||||
else if (state.step === 2) renderAuth();
|
||||
else if (state.step === 3) renderProvider();
|
||||
else renderReady();
|
||||
}
|
||||
|
||||
function renderWelcome() {
|
||||
overlay.innerHTML = card(`
|
||||
${steps()}
|
||||
<h1 style="margin:0 0 8px;font-size:22px;font-weight:600;color:var(--db-text,#e6e9ef);text-align:center">Welcome to PraisonAIUI</h1>
|
||||
<p style="margin:0 0 20px;color:var(--db-text-muted,#9aa4b2);text-align:center;font-size:14px">Confirm your gateway to get started.</p>
|
||||
<label style="display:block;font-size:13px;color:var(--db-text-muted,#9aa4b2);margin-bottom:6px">Gateway URL</label>
|
||||
<input id="ob-url" type="text" value="${gatewayUrl}" style="width:100%;box-sizing:border-box;padding:10px 12px;border-radius:8px;border:1px solid var(--db-border,#2a2f3a);background:var(--db-bg,#0b0e14);color:var(--db-text,#e6e9ef);font-size:14px;margin-bottom:6px">
|
||||
<div id="ob-url-err" aria-live="assertive" style="min-height:18px;color:#ff6b6b;font-size:13px;margin-bottom:12px"></div>
|
||||
<button id="ob-continue" style="width:100%;padding:11px;border:none;border-radius:8px;background:var(--db-accent,#4f8cff);color:#fff;font-size:14px;font-weight:600;cursor:pointer">Continue</button>
|
||||
<button id="ob-standalone" style="width:100%;padding:8px;margin-top:10px;border:none;background:none;color:var(--db-text-muted,#9aa4b2);font-size:13px;cursor:pointer;text-decoration:underline">Use standalone mode</button>
|
||||
`);
|
||||
const continueBtn = overlay.querySelector('#ob-continue');
|
||||
continueBtn.addEventListener('click', async () => {
|
||||
const url = overlay.querySelector('#ob-url').value.trim();
|
||||
const errEl = overlay.querySelector('#ob-url-err');
|
||||
const override = overlay.querySelector('#ob-url-override');
|
||||
errEl.textContent = '';
|
||||
if (override && override.checked) {
|
||||
state.step = 2;
|
||||
render();
|
||||
return;
|
||||
}
|
||||
continueBtn.disabled = true;
|
||||
continueBtn.textContent = 'Checking…';
|
||||
const result = await probeGatewayReachable(url);
|
||||
continueBtn.disabled = false;
|
||||
continueBtn.textContent = 'Continue';
|
||||
if (result.ok) {
|
||||
state.step = 2;
|
||||
render();
|
||||
return;
|
||||
}
|
||||
if (result.reason === 'invalid_url') {
|
||||
errEl.textContent = `Enter a valid http(s) URL`;
|
||||
} else if (result.reason === 'opaque') {
|
||||
errEl.innerHTML = `Cannot verify gateway from browser. <label style="cursor:pointer"><input id="ob-url-override" type="checkbox" style="vertical-align:middle"> Continue anyway</label>`;
|
||||
} else if (result.reason === 'timeout') {
|
||||
errEl.textContent = `Gateway did not respond at ${url}`;
|
||||
} else {
|
||||
errEl.textContent = `Cannot reach gateway at ${url}`;
|
||||
}
|
||||
});
|
||||
overlay.querySelector('#ob-standalone').addEventListener('click', () => {
|
||||
try { window.localStorage.setItem(ONBOARD_SKIP_KEY, '1'); } catch (e) { /* ignore */ }
|
||||
close();
|
||||
});
|
||||
}
|
||||
|
||||
function renderAuth() {
|
||||
overlay.innerHTML = card(`
|
||||
${steps()}
|
||||
<h1 style="margin:0 0 8px;font-size:20px;font-weight:600;color:var(--db-text,#e6e9ef)">Authenticate</h1>
|
||||
<p style="margin:0 0 16px;color:var(--db-text-muted,#9aa4b2);font-size:14px">Paste your gateway token to connect.</p>
|
||||
<textarea id="ob-token" rows="3" placeholder="gw_xxxx…" style="width:100%;box-sizing:border-box;padding:10px 12px;border-radius:8px;border:1px solid var(--db-border,#2a2f3a);background:var(--db-bg,#0b0e14);color:var(--db-text,#e6e9ef);font-size:13px;font-family:monospace;resize:vertical"></textarea>
|
||||
<div id="ob-auth-err" aria-live="assertive" style="min-height:18px;color:#ff6b6b;font-size:13px;margin:6px 0 10px"></div>
|
||||
<button id="ob-connect" style="width:100%;padding:11px;border:none;border-radius:8px;background:var(--db-accent,#4f8cff);color:#fff;font-size:14px;font-weight:600;cursor:pointer">Connect</button>
|
||||
<button id="ob-copy-link" style="width:100%;padding:8px;margin-top:10px;border:1px solid var(--db-border,#2a2f3a);background:none;color:var(--db-text-muted,#9aa4b2);font-size:13px;cursor:pointer;border-radius:8px">Copy magic link</button>
|
||||
<button id="ob-back" style="width:100%;padding:8px;margin-top:8px;border:none;background:none;color:var(--db-text-muted,#9aa4b2);font-size:13px;cursor:pointer">Back</button>
|
||||
`);
|
||||
const connectBtn = overlay.querySelector('#ob-connect');
|
||||
connectBtn.addEventListener('click', async () => {
|
||||
const token = overlay.querySelector('#ob-token').value.trim();
|
||||
const errEl = overlay.querySelector('#ob-auth-err');
|
||||
errEl.textContent = '';
|
||||
if (!token) { errEl.textContent = 'Enter a token to continue'; return; }
|
||||
connectBtn.disabled = true;
|
||||
connectBtn.textContent = 'Connecting…';
|
||||
const ok = await consumeToken(token);
|
||||
connectBtn.disabled = false;
|
||||
connectBtn.textContent = 'Connect';
|
||||
if (ok) {
|
||||
state.step = 3;
|
||||
render();
|
||||
} else {
|
||||
errEl.textContent = 'Invalid token — check ~/.praisonai/.env';
|
||||
}
|
||||
});
|
||||
overlay.querySelector('#ob-copy-link').addEventListener('click', () => {
|
||||
const token = overlay.querySelector('#ob-token').value.trim();
|
||||
const link = `${window.location.origin}${window.location.pathname}?token=${encodeURIComponent(token || 'YOUR_TOKEN')}`;
|
||||
try { navigator.clipboard.writeText(link); } catch (e) { /* ignore */ }
|
||||
});
|
||||
overlay.querySelector('#ob-back').addEventListener('click', () => { state.step = 1; render(); });
|
||||
}
|
||||
|
||||
function renderProvider() {
|
||||
overlay.innerHTML = card(`
|
||||
${steps()}
|
||||
<h1 style="margin:0 0 8px;font-size:20px;font-weight:600;color:var(--db-text,#e6e9ef)">Provider setup</h1>
|
||||
<p style="margin:0 0 16px;color:var(--db-text-muted,#9aa4b2);font-size:14px">Optional — configure API keys in Settings.</p>
|
||||
<div id="ob-providers" style="margin-bottom:16px;color:var(--db-text-muted,#9aa4b2);font-size:13px">Loading provider status…</div>
|
||||
<button id="ob-provider-next" style="width:100%;padding:11px;border:none;border-radius:8px;background:var(--db-accent,#4f8cff);color:#fff;font-size:14px;font-weight:600;cursor:pointer">Continue</button>
|
||||
<button id="ob-provider-skip" style="width:100%;padding:8px;margin-top:10px;border:none;background:none;color:var(--db-text-muted,#9aa4b2);font-size:13px;cursor:pointer;text-decoration:underline">Skip</button>
|
||||
`);
|
||||
loadProviderStatus();
|
||||
overlay.querySelector('#ob-provider-next').addEventListener('click', () => { state.step = 4; render(); });
|
||||
overlay.querySelector('#ob-provider-skip').addEventListener('click', () => { state.step = 4; render(); });
|
||||
}
|
||||
|
||||
async function loadProviderStatus() {
|
||||
const el = overlay.querySelector('#ob-providers');
|
||||
if (!el) return;
|
||||
try {
|
||||
const headers = {};
|
||||
const token = getStoredToken();
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
const res = await fetch('/api/config', { headers, credentials: 'same-origin' });
|
||||
const cfg = res.ok ? await res.json() : {};
|
||||
const providers = ['OpenAI', 'Anthropic', 'Google'];
|
||||
el.innerHTML = providers.map((p) => {
|
||||
const key = `${p.toLowerCase()}`;
|
||||
const configured = cfg && (cfg[`${key}_configured`] || (cfg.providers && cfg.providers[key]));
|
||||
const label = configured ? 'configured' : 'missing';
|
||||
const color = configured ? '#4caf50' : 'var(--db-text-muted,#9aa4b2)';
|
||||
return `<div style="display:flex;justify-content:space-between;padding:6px 0"><span style="color:var(--db-text,#e6e9ef)">${p}</span><span style="color:${color}">${label}</span></div>`;
|
||||
}).join('');
|
||||
} catch (e) {
|
||||
el.textContent = 'Provider status unavailable.';
|
||||
}
|
||||
}
|
||||
|
||||
function renderReady() {
|
||||
overlay.innerHTML = card(`
|
||||
${steps()}
|
||||
<div style="text-align:center;font-size:40px;margin-bottom:8px">✅</div>
|
||||
<h1 style="margin:0 0 8px;font-size:20px;font-weight:600;color:var(--db-text,#e6e9ef);text-align:center">You're all set</h1>
|
||||
<p style="margin:0 0 20px;color:var(--db-text-muted,#9aa4b2);text-align:center;font-size:14px">Gateway connected. Ready to open your dashboard.</p>
|
||||
<button id="ob-open" style="width:100%;padding:11px;border:none;border-radius:8px;background:var(--db-accent,#4f8cff);color:#fff;font-size:14px;font-weight:600;cursor:pointer">Open Dashboard</button>
|
||||
<label style="display:flex;align-items:center;gap:8px;margin-top:14px;color:var(--db-text-muted,#9aa4b2);font-size:13px;cursor:pointer;justify-content:center">
|
||||
<input id="ob-dont-show" type="checkbox"> Don't show again on this device
|
||||
</label>
|
||||
`);
|
||||
overlay.querySelector('#ob-open').addEventListener('click', () => {
|
||||
if (overlay.querySelector('#ob-dont-show').checked) {
|
||||
try { window.localStorage.setItem(ONBOARD_SKIP_KEY, '1'); } catch (e) { /* ignore */ }
|
||||
}
|
||||
close();
|
||||
});
|
||||
}
|
||||
|
||||
// Focus trap: keep focus within the overlay; ESC disabled on auth step.
|
||||
overlay.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && state.step === 2) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Tab') {
|
||||
const focusable = overlay.querySelectorAll('button, input, textarea, a[href]');
|
||||
if (!focusable.length) return;
|
||||
const first = focusable[0];
|
||||
const last = focusable[focusable.length - 1];
|
||||
if (e.shiftKey && document.activeElement === first) {
|
||||
e.preventDefault();
|
||||
last.focus();
|
||||
} else if (!e.shiftKey && document.activeElement === last) {
|
||||
e.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
render();
|
||||
const firstInput = overlay.querySelector('input, button');
|
||||
if (firstInput) firstInput.focus();
|
||||
});
|
||||
}
|
||||
|
||||
async function init() {
|
||||
// Attach stored gateway token to same-origin requests (no-op without a token).
|
||||
installAuthFetch();
|
||||
|
||||
// Inject styles
|
||||
const style = document.createElement('style');
|
||||
style.textContent = DASHBOARD_STYLE;
|
||||
@@ -1293,6 +1664,13 @@ async function init() {
|
||||
console.warn('[AIUI] Failed to load theme config:', e);
|
||||
}
|
||||
|
||||
// First-run onboarding gate — runs before protected dashboard APIs.
|
||||
try {
|
||||
await maybeShowOnboarding();
|
||||
} catch (e) {
|
||||
console.warn('[AIUI] Onboarding gate error:', e);
|
||||
}
|
||||
|
||||
await buildDashboard();
|
||||
|
||||
// Initialize Ctrl+K / Cmd+K command palette
|
||||
|
||||
@@ -300,7 +300,13 @@ let lastUrl = '';
|
||||
|
||||
export default {
|
||||
name: 'mermaid',
|
||||
async init() { await loadMermaidLib(); },
|
||||
async init() {
|
||||
await loadMermaidLib();
|
||||
window.addEventListener('aiui:content-loaded', (event) => {
|
||||
const root = event.detail?.root || document;
|
||||
renderMermaidBlocks(root);
|
||||
});
|
||||
},
|
||||
onContentChange(root) {
|
||||
const currentUrl = location.pathname + location.hash;
|
||||
|
||||
|
||||
@@ -1,74 +1,247 @@
|
||||
/**
|
||||
* AIUI MkDocs Compatibility Plugin
|
||||
*
|
||||
*
|
||||
* Strips or transforms MkDocs Material-specific syntax that renders
|
||||
* as raw text in standard markdown renderers:
|
||||
*
|
||||
* - :material-*: icon shortcodes → emoji or removes them
|
||||
* - <div class="grid cards" markdown> → removes wrapper
|
||||
* - === "Tab Title" → transforms to heading
|
||||
* - !!! note/warning → transforms to blockquote
|
||||
* - </div> visible tags → removes them
|
||||
* as raw text in standard markdown renderers.
|
||||
*/
|
||||
|
||||
const ICON_MAP = {
|
||||
':material-file-document:': '📄',
|
||||
':material-puzzle:': '🧩',
|
||||
':material-palette:': '🎨',
|
||||
':material-rocket-launch:': '🚀',
|
||||
':material-code-tags:': '💻',
|
||||
':material-cog:': '⚙️',
|
||||
':material-lightning-bolt:': '⚡',
|
||||
':material-shield:': '🛡️',
|
||||
':material-database:': '🗄️',
|
||||
':material-web:': '🌐',
|
||||
':material-book:': '📖',
|
||||
':material-star:': '⭐',
|
||||
':material-check:': '✅',
|
||||
':material-close:': '❌',
|
||||
':material-alert:': '⚠️',
|
||||
':material-information:': 'ℹ️',
|
||||
};
|
||||
|
||||
const TAB_HEADING_PREFIX = '\u200Btab:';
|
||||
|
||||
function replaceIcons(text) {
|
||||
let result = text;
|
||||
for (const [shortcode, emoji] of Object.entries(ICON_MAP)) {
|
||||
result = result.replaceAll(shortcode, emoji);
|
||||
}
|
||||
return result
|
||||
.replace(/:material-[\w-]+:/g, '•')
|
||||
.replace(/:octicons-[\w-]+(?:-\d+)?:/g, '•');
|
||||
}
|
||||
|
||||
function convertMkdocsTabs(md) {
|
||||
const lines = md.split('\n');
|
||||
const out = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < lines.length) {
|
||||
const tabMatch = lines[i].match(/^===\s+"([^"]+)"\s*$/);
|
||||
if (!tabMatch) {
|
||||
out.push(lines[i]);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const title = tabMatch[1];
|
||||
i++;
|
||||
while (i < lines.length && lines[i].trim() === '') i++;
|
||||
|
||||
const block = [];
|
||||
while (i < lines.length) {
|
||||
if (/^===\s+"[^"]+"\s*$/.test(lines[i])) break;
|
||||
if (lines[i].trim() === '' && i + 1 < lines.length && /^===\s+"[^"]+"\s*$/.test(lines[i + 1])) break;
|
||||
|
||||
if (lines[i].startsWith(' ') || lines[i].trimStart().startsWith('```')) {
|
||||
block.push(lines[i].replace(/^ /, ''));
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (block.length > 0 && lines[i].trim() === '') {
|
||||
block.push('');
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
out.push(`### ${TAB_HEADING_PREFIX}${title}`);
|
||||
out.push('');
|
||||
out.push(...block);
|
||||
out.push('');
|
||||
}
|
||||
|
||||
return out.join('\n');
|
||||
}
|
||||
|
||||
function isTabHeading(text) {
|
||||
return text.startsWith(TAB_HEADING_PREFIX) || text.startsWith('tab:');
|
||||
}
|
||||
|
||||
function tabTitle(text) {
|
||||
return text.replace(/^\u200Btab:/, '').replace(/^tab:/, '');
|
||||
}
|
||||
|
||||
function enhanceFeatureGrid(article) {
|
||||
for (const ul of article.querySelectorAll('ul')) {
|
||||
if (ul.classList.contains('aiui-feature-grid')) continue;
|
||||
|
||||
const firstLi = ul.querySelector('li');
|
||||
const looksLikeCards = Boolean(
|
||||
firstLi?.textContent?.includes('YAML-Driven')
|
||||
|| firstLi?.textContent?.includes('Component Slots')
|
||||
|| firstLi?.querySelector('strong'),
|
||||
);
|
||||
if (!looksLikeCards) continue;
|
||||
|
||||
ul.classList.add(
|
||||
'aiui-feature-grid',
|
||||
'grid',
|
||||
'grid-cols-1',
|
||||
'md:grid-cols-2',
|
||||
'gap-4',
|
||||
'my-6',
|
||||
'list-none',
|
||||
'pl-0',
|
||||
);
|
||||
|
||||
for (const li of ul.querySelectorAll('li')) {
|
||||
li.classList.add('border', 'border-border', 'rounded-lg', 'p-4', 'bg-card');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function enhanceTabs(article) {
|
||||
if (article.querySelector('[data-aiui-tabs]')) return;
|
||||
|
||||
const tabHeadings = Array.from(article.querySelectorAll('h3')).filter((h3) =>
|
||||
isTabHeading(h3.textContent?.trim() ?? ''),
|
||||
);
|
||||
if (tabHeadings.length < 2) return;
|
||||
|
||||
const sections = tabHeadings.map((heading) => {
|
||||
const title = tabTitle(heading.textContent?.trim() ?? '');
|
||||
const nodes = [];
|
||||
let sibling = heading.nextSibling;
|
||||
|
||||
while (sibling) {
|
||||
if (sibling.nodeType === Node.ELEMENT_NODE) {
|
||||
const el = sibling;
|
||||
if (el.tagName === 'H3' && isTabHeading(el.textContent?.trim() ?? '')) break;
|
||||
if (['H1', 'H2'].includes(el.tagName)) break;
|
||||
}
|
||||
const next = sibling.nextSibling;
|
||||
nodes.push(sibling);
|
||||
sibling = next;
|
||||
}
|
||||
|
||||
return { heading, title, nodes };
|
||||
});
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'aiui-tabs my-6 border border-border rounded-lg overflow-hidden';
|
||||
wrapper.dataset.aiuiTabs = 'true';
|
||||
|
||||
const buttons = document.createElement('div');
|
||||
buttons.className = 'aiui-tab-buttons flex flex-wrap gap-0 border-b border-border bg-muted/30';
|
||||
wrapper.appendChild(buttons);
|
||||
|
||||
const panels = document.createElement('div');
|
||||
panels.className = 'aiui-tab-panels';
|
||||
wrapper.appendChild(panels);
|
||||
|
||||
sections.forEach((section, index) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = [
|
||||
'aiui-tab-button px-4 py-2 text-sm font-medium transition-colors',
|
||||
index === 0
|
||||
? 'bg-background text-foreground border-b-2 border-primary'
|
||||
: 'text-muted-foreground hover:text-foreground',
|
||||
].join(' ');
|
||||
btn.textContent = section.title;
|
||||
btn.dataset.tabIndex = String(index);
|
||||
buttons.appendChild(btn);
|
||||
|
||||
const panel = document.createElement('div');
|
||||
panel.className = index === 0 ? 'aiui-tab-panel p-4 block' : 'aiui-tab-panel p-4 hidden';
|
||||
panel.dataset.tabPanel = String(index);
|
||||
for (const node of section.nodes) {
|
||||
panel.appendChild(node);
|
||||
}
|
||||
panels.appendChild(panel);
|
||||
});
|
||||
|
||||
article.insertBefore(wrapper, sections[0].heading);
|
||||
for (const section of sections) {
|
||||
section.heading.remove();
|
||||
}
|
||||
|
||||
buttons.addEventListener('click', (event) => {
|
||||
const target = event.target.closest('.aiui-tab-button');
|
||||
if (!target) return;
|
||||
const index = target.dataset.tabIndex ?? '0';
|
||||
|
||||
for (const btn of buttons.querySelectorAll('.aiui-tab-button')) {
|
||||
const active = btn.dataset.tabIndex === index;
|
||||
btn.classList.toggle('bg-background', active);
|
||||
btn.classList.toggle('text-foreground', active);
|
||||
btn.classList.toggle('border-b-2', active);
|
||||
btn.classList.toggle('border-primary', active);
|
||||
btn.classList.toggle('text-muted-foreground', !active);
|
||||
}
|
||||
|
||||
for (const panel of panels.querySelectorAll('.aiui-tab-panel')) {
|
||||
panel.classList.toggle('hidden', panel.dataset.tabPanel !== index);
|
||||
panel.classList.toggle('block', panel.dataset.tabPanel === index);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean MkDocs-specific patterns from rendered HTML.
|
||||
*/
|
||||
function cleanMkDocsContent(root) {
|
||||
// Find all text nodes and elements in the content area
|
||||
const articles = root.querySelectorAll('article.prose, main .prose');
|
||||
if (articles.length === 0) return;
|
||||
const articles = new Set();
|
||||
if (root.matches?.('article.prose, main .prose, .prose, main.flex-1, #main-content')) {
|
||||
articles.add(root);
|
||||
}
|
||||
root.querySelectorAll('article.prose, main .prose, .prose, main.flex-1, #main-content').forEach((el) => articles.add(el));
|
||||
if (articles.size === 0) return;
|
||||
|
||||
for (const article of articles) {
|
||||
if (article.dataset.mkdocsClean) continue;
|
||||
delete article.dataset.mkdocsClean;
|
||||
|
||||
let html = article.innerHTML;
|
||||
let changed = false;
|
||||
|
||||
// 1. Remove :material-*: icon shortcodes (replace with relevant emoji or empty)
|
||||
const iconMap = {
|
||||
':material-file-document:': '📄',
|
||||
':material-puzzle:': '🧩',
|
||||
':material-palette:': '🎨',
|
||||
':material-rocket-launch:': '🚀',
|
||||
':material-code-tags:': '💻',
|
||||
':material-cog:': '⚙️',
|
||||
':material-lightning-bolt:': '⚡',
|
||||
':material-shield:': '🛡️',
|
||||
':material-database:': '🗄️',
|
||||
':material-web:': '🌐',
|
||||
':material-book:': '📖',
|
||||
':material-star:': '⭐',
|
||||
':material-check:': '✅',
|
||||
':material-close:': '❌',
|
||||
':material-alert:': '⚠️',
|
||||
':material-information:': 'ℹ️',
|
||||
};
|
||||
|
||||
// Replace known icons
|
||||
for (const [shortcode, emoji] of Object.entries(iconMap)) {
|
||||
for (const [shortcode, emoji] of Object.entries(ICON_MAP)) {
|
||||
if (html.includes(shortcode)) {
|
||||
html = html.replaceAll(shortcode, emoji);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Replace any remaining :material-*: with generic icon
|
||||
const materialPattern = /:material-[\w-]+:/g;
|
||||
if (materialPattern.test(html)) {
|
||||
html = html.replace(materialPattern, '•');
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// Replace :octicons-*: shortcodes
|
||||
const octiconsPattern = /:octicons-[\w-]+(?:-\d+)?:/g;
|
||||
if (octiconsPattern.test(html)) {
|
||||
html = html.replace(octiconsPattern, '•');
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// 2. Remove <div class="grid cards" markdown> and </div> tags
|
||||
// These render as visible text in react-markdown
|
||||
const gridCardPattern = /<div\s+class="[^"]*"\s*(?:markdown)?>/gi;
|
||||
if (gridCardPattern.test(html)) {
|
||||
html = html.replace(gridCardPattern, '');
|
||||
@@ -81,29 +254,20 @@ function cleanMkDocsContent(root) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// Also handle cases where react-markdown renders them as actual elements
|
||||
const rawDivNodes = article.querySelectorAll('p');
|
||||
for (const p of rawDivNodes) {
|
||||
if (changed) {
|
||||
article.innerHTML = html;
|
||||
}
|
||||
|
||||
for (const p of article.querySelectorAll('p')) {
|
||||
const text = p.textContent.trim();
|
||||
if (/^<\/?div[\s>]/.test(text) || text === '</div>') {
|
||||
// Clear text instead of hiding — safer for React reconciliation
|
||||
p.textContent = '';
|
||||
changed = true;
|
||||
p.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Transform === "Tab Title" content tabs to headings
|
||||
// These appear as literal text like: === "Python"
|
||||
const tabPattern = /===\s+"([^"]+)"/g;
|
||||
if (tabPattern.test(html)) {
|
||||
html = html.replace(tabPattern, '<strong>$1</strong>');
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
article.innerHTML = html;
|
||||
article.dataset.mkdocsClean = 'true';
|
||||
}
|
||||
enhanceFeatureGrid(article);
|
||||
enhanceTabs(article);
|
||||
article.dataset.mkdocsClean = 'true';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,3 +282,5 @@ export default {
|
||||
cleanMkDocsContent(root);
|
||||
},
|
||||
};
|
||||
|
||||
export { replaceIcons, convertMkdocsTabs, ICON_MAP };
|
||||
@@ -1,54 +1,73 @@
|
||||
/**
|
||||
* AIUI Syntax Highlighting Plugin
|
||||
*
|
||||
* Adds colorful syntax highlighting to code blocks using highlight.js.
|
||||
* Adds colourful syntax highlighting to code blocks using highlight.js.
|
||||
* Loads from CDN – no build step required.
|
||||
*/
|
||||
|
||||
let hljsReady = false;
|
||||
let pendingHighlight = false;
|
||||
let currentTheme = null;
|
||||
|
||||
const HLJS_VERSION = '11.9.0';
|
||||
const HLJS_THEME = 'github-dark';
|
||||
const HLJS_CDN = `https://cdnjs.cloudflare.com/ajax/libs/highlight.js/${HLJS_VERSION}`;
|
||||
|
||||
function isDarkMode() {
|
||||
return document.documentElement.classList.contains('dark');
|
||||
}
|
||||
|
||||
function getHljsTheme() {
|
||||
return isDarkMode() ? 'github-dark' : 'github';
|
||||
}
|
||||
|
||||
function applyPreStyles() {
|
||||
const dark = isDarkMode();
|
||||
let override = document.getElementById('aiui-hljs-overrides');
|
||||
if (!override) {
|
||||
override = document.createElement('style');
|
||||
override.id = 'aiui-hljs-overrides';
|
||||
document.head.appendChild(override);
|
||||
}
|
||||
|
||||
override.textContent = `
|
||||
pre code.hljs {
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
color: inherit !important;
|
||||
}
|
||||
pre {
|
||||
background: ${dark ? 'rgba(15, 23, 42, 0.6)' : 'var(--muted, #f4f4f5)'} !important;
|
||||
color: ${dark ? 'inherit' : 'var(--foreground, #18181b)'} !important;
|
||||
border: 1px solid ${dark ? 'rgba(148, 163, 184, 0.1)' : 'var(--border, #e4e4e7)'};
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem !important;
|
||||
overflow-x: auto;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load highlight.js CSS theme and core script from CDN.
|
||||
*/
|
||||
function loadHljs() {
|
||||
return new Promise((resolve) => {
|
||||
// Load theme CSS
|
||||
const theme = getHljsTheme();
|
||||
currentTheme = theme;
|
||||
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = `${HLJS_CDN}/styles/${HLJS_THEME}.min.css`;
|
||||
link.id = 'aiui-hljs-theme';
|
||||
link.href = `${HLJS_CDN}/styles/${theme}.min.css`;
|
||||
document.head.appendChild(link);
|
||||
|
||||
// Override styles for better integration
|
||||
const override = document.createElement('style');
|
||||
override.id = 'aiui-hljs-overrides';
|
||||
override.textContent = `
|
||||
pre code.hljs {
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
pre {
|
||||
background: rgba(15, 23, 42, 0.6) !important;
|
||||
border: 1px solid rgba(148, 163, 184, 0.1);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem !important;
|
||||
overflow-x: auto;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(override);
|
||||
applyPreStyles();
|
||||
|
||||
// Load highlight.js core
|
||||
const script = document.createElement('script');
|
||||
script.src = `${HLJS_CDN}/highlight.min.js`;
|
||||
script.onload = () => {
|
||||
hljsReady = true;
|
||||
console.debug('[AIUI:syntax] highlight.js core loaded');
|
||||
|
||||
// Load additional language packs
|
||||
|
||||
const langs = ['python', 'yaml', 'bash', 'javascript', 'typescript', 'json', 'css', 'html', 'xml'];
|
||||
let loaded = 0;
|
||||
for (const lang of langs) {
|
||||
@@ -58,7 +77,6 @@ function loadHljs() {
|
||||
loaded++;
|
||||
if (loaded >= langs.length) {
|
||||
console.debug('[AIUI:syntax] All languages loaded');
|
||||
// Highlight any pending content
|
||||
if (pendingHighlight) {
|
||||
pendingHighlight = false;
|
||||
highlightCodeBlocks();
|
||||
@@ -68,7 +86,6 @@ function loadHljs() {
|
||||
document.head.appendChild(ls);
|
||||
}
|
||||
|
||||
// Also highlight immediately with core languages
|
||||
setTimeout(() => highlightCodeBlocks(), 200);
|
||||
resolve();
|
||||
};
|
||||
@@ -80,6 +97,28 @@ function loadHljs() {
|
||||
});
|
||||
}
|
||||
|
||||
function swapThemeIfNeeded() {
|
||||
const theme = getHljsTheme();
|
||||
if (theme === currentTheme) {
|
||||
applyPreStyles();
|
||||
return;
|
||||
}
|
||||
|
||||
currentTheme = theme;
|
||||
const link = document.getElementById('aiui-hljs-theme');
|
||||
if (link) {
|
||||
link.href = `${HLJS_CDN}/styles/${theme}.min.css`;
|
||||
}
|
||||
applyPreStyles();
|
||||
|
||||
for (const block of document.querySelectorAll('pre code[data-hljs-highlighted]')) {
|
||||
delete block.dataset.hljsHighlighted;
|
||||
block.removeAttribute('data-highlighted');
|
||||
block.classList.remove('hljs');
|
||||
}
|
||||
highlightCodeBlocks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight all unprocessed code blocks.
|
||||
*/
|
||||
@@ -89,9 +128,10 @@ function highlightCodeBlocks() {
|
||||
return;
|
||||
}
|
||||
|
||||
swapThemeIfNeeded();
|
||||
|
||||
const blocks = document.querySelectorAll('pre code:not([data-hljs-highlighted])');
|
||||
for (const block of blocks) {
|
||||
// Skip mermaid blocks
|
||||
if (block.classList.contains('language-mermaid') ||
|
||||
block.closest('.mermaid') ||
|
||||
block.closest('[data-aiui-plugin="mermaid"]')) {
|
||||
@@ -111,10 +151,13 @@ export default {
|
||||
name: 'syntax-highlight',
|
||||
async init() {
|
||||
await loadHljs();
|
||||
|
||||
const observer = new MutationObserver(() => swapThemeIfNeeded());
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
||||
|
||||
console.debug('[AIUI:syntax] Plugin loaded.');
|
||||
},
|
||||
onContentChange() {
|
||||
// Try immediately and with delay (for dynamically inserted content)
|
||||
highlightCodeBlocks();
|
||||
setTimeout(highlightCodeBlocks, 300);
|
||||
setTimeout(highlightCodeBlocks, 800);
|
||||
|
||||
@@ -57,9 +57,489 @@ export function searchInput(placeholder = 'Search…', id = 'db-search-input') {
|
||||
return `<input id="${id}" type="search" placeholder="${placeholder}" style="width:100%;max-width:320px;padding:8px 12px;background:var(--db-card-bg);border:1px solid var(--db-border);border-radius:8px;color:var(--db-text);font-size:13px;box-sizing:border-box" />`;
|
||||
}
|
||||
|
||||
/** Escape a string for safe interpolation into template literals. */
|
||||
export function esc(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s == null ? '' : String(s);
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline SVG sparkline (no external library).
|
||||
*
|
||||
* @param {number[]} values - Series values (empty renders a flat baseline).
|
||||
* @param {Object} [opts]
|
||||
* @param {number} [opts.width=96]
|
||||
* @param {number} [opts.height=32]
|
||||
* @param {string} [opts.color] - Stroke colour (defaults to accent token).
|
||||
* @returns {string} SVG markup.
|
||||
*/
|
||||
export function sparklineSVG(values, { width = 96, height = 32, color = 'var(--db-accent,#6366f1)' } = {}) {
|
||||
const data = Array.isArray(values) ? values.filter((v) => typeof v === 'number' && !isNaN(v)) : [];
|
||||
const pad = 2;
|
||||
const w = width - pad * 2;
|
||||
const h = height - pad * 2;
|
||||
if (data.length < 2) {
|
||||
const y = height / 2;
|
||||
return `<svg class="db-sparkline" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" aria-hidden="true"><line x1="${pad}" y1="${y}" x2="${width - pad}" y2="${y}" stroke="var(--db-border,#3f3f46)" stroke-width="1.5" /></svg>`;
|
||||
}
|
||||
const min = Math.min(...data);
|
||||
const max = Math.max(...data);
|
||||
const range = max - min || 1;
|
||||
const pts = data.map((v, i) => {
|
||||
const x = pad + (i / (data.length - 1)) * w;
|
||||
const yy = pad + h - ((v - min) / range) * h;
|
||||
return `${x.toFixed(1)},${yy.toFixed(1)}`;
|
||||
});
|
||||
const areaPts = `${pad},${(pad + h).toFixed(1)} ${pts.join(' ')} ${(pad + w).toFixed(1)},${(pad + h).toFixed(1)}`;
|
||||
return `<svg class="db-sparkline" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" preserveAspectRatio="none" aria-hidden="true">
|
||||
<polygon points="${areaPts}" fill="${color}" opacity="0.12" />
|
||||
<polyline points="${pts.join(' ')}" fill="none" stroke="${color}" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round" />
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metric card with value, subtitle, optional delta and sparkline.
|
||||
* Renders a clickable card that navigates via data-nav (handled by caller).
|
||||
*
|
||||
* @param {Object} opts
|
||||
* @param {string} opts.title - Card label.
|
||||
* @param {string|number} opts.value - Primary value (already formatted).
|
||||
* @param {string} [opts.subtitle] - Secondary label.
|
||||
* @param {number} [opts.delta] - Signed percentage delta.
|
||||
* @param {number[]} [opts.spark] - Sparkline series.
|
||||
* @param {string} [opts.nav] - Page id to navigate to on click.
|
||||
* @param {string} [opts.accent] - Value colour override.
|
||||
* @returns {string} HTML string.
|
||||
*/
|
||||
export function metricCard({ title, value, subtitle = '', delta, spark, nav, accent }) {
|
||||
let deltaHtml = '';
|
||||
if (typeof delta === 'number' && !isNaN(delta) && delta !== 0) {
|
||||
const up = delta > 0;
|
||||
const col = up ? '#22c55e' : '#ef4444';
|
||||
const arrow = up ? '▲' : '▼';
|
||||
deltaHtml = `<span style="font-size:11px;color:${col};margin-left:6px">${arrow} ${Math.abs(delta).toFixed(0)}%</span>`;
|
||||
}
|
||||
const sparkHtml = Array.isArray(spark) ? `<div style="margin-top:8px">${sparklineSVG(spark)}</div>` : '';
|
||||
const clickable = nav ? `data-nav="${esc(nav)}" role="button" tabindex="0" style="cursor:pointer"` : '';
|
||||
return `<div class="db-card db-metric-card" ${clickable}>
|
||||
<div class="db-card-title">${esc(title)}</div>
|
||||
<div class="db-card-value"${accent ? ` style="color:${accent}"` : ''}>${esc(value)}${deltaHtml}</div>
|
||||
<div class="db-card-footer">${esc(subtitle)}</div>
|
||||
${sparkHtml}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bucket evaluation records into per-day average score series.
|
||||
*
|
||||
* @param {Array} evaluations - Records with `timestamp` (epoch seconds) and `score`.
|
||||
* @param {number} [days=7] - Number of trailing days to include.
|
||||
* @param {string|null} [agentId=null] - Optional agent filter.
|
||||
* @returns {Array<{date: string, avg: number, count: number}>} Ascending by date.
|
||||
*/
|
||||
export function bucketByDay(evaluations, days = 7, agentId = null) {
|
||||
const list = Array.isArray(evaluations) ? evaluations : [];
|
||||
const cutoff = Date.now() / 1000 - days * 86400;
|
||||
const acc = {};
|
||||
for (const ev of list) {
|
||||
if (!ev || typeof ev.timestamp !== 'number') continue;
|
||||
if (ev.timestamp < cutoff) continue;
|
||||
if (agentId && ev.agent_id !== agentId) continue;
|
||||
if (ev.score == null || isNaN(ev.score)) continue;
|
||||
const date = new Date(ev.timestamp * 1000).toISOString().slice(0, 10);
|
||||
if (!acc[date]) acc[date] = { date, sum: 0, count: 0 };
|
||||
acc[date].sum += Number(ev.score);
|
||||
acc[date].count += 1;
|
||||
}
|
||||
return Object.values(acc)
|
||||
.sort((a, b) => (a.date < b.date ? -1 : 1))
|
||||
.map((b) => ({ date: b.date, avg: b.sum / b.count, count: b.count }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression check comparing a current value against a pinned baseline.
|
||||
*
|
||||
* @param {number} current
|
||||
* @param {number} baseline
|
||||
* @param {number} [threshold=0.10] - Relative drop that counts as a regression.
|
||||
* @returns {{isRegression: boolean, delta: number, label: string}}
|
||||
*/
|
||||
export function detectRegression(current, baseline, threshold = 0.1) {
|
||||
const c = Number(current);
|
||||
const b = Number(baseline);
|
||||
if (!isFinite(c) || !isFinite(b) || b === 0) {
|
||||
return { isRegression: false, delta: 0, label: '' };
|
||||
}
|
||||
const delta = (c - b) / b;
|
||||
const isRegression = delta <= -threshold;
|
||||
const pct = Math.round(delta * 100);
|
||||
return { isRegression, delta, label: `${pct >= 0 ? '+' : ''}${pct}%` };
|
||||
}
|
||||
|
||||
/**
|
||||
* Coloured trend arrow comparing current vs previous value.
|
||||
*
|
||||
* @param {number} current
|
||||
* @param {number} previous
|
||||
* @returns {string} HTML span (empty string when no meaningful change).
|
||||
*/
|
||||
export function trendArrow(current, previous) {
|
||||
const c = Number(current);
|
||||
const p = Number(previous);
|
||||
if (!isFinite(c) || !isFinite(p) || p === 0) return '';
|
||||
const delta = ((c - p) / p) * 100;
|
||||
if (Math.abs(delta) < 0.5) {
|
||||
return '<span style="font-size:11px;color:var(--db-text-dim,#a1a1aa)">→ 0%</span>';
|
||||
}
|
||||
const up = delta > 0;
|
||||
const col = up ? '#22c55e' : '#ef4444';
|
||||
const arrow = up ? '↑' : '↓';
|
||||
return `<span style="font-size:11px;color:${col}">${arrow} ${Math.abs(delta).toFixed(1)}%</span>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression pill badge, colour-graded by drop severity.
|
||||
*
|
||||
* @param {number} delta - Signed relative delta (e.g. -0.12 for -12%).
|
||||
* @param {string} [suffix='vs baseline']
|
||||
* @returns {string} HTML string (empty when delta is not a regression).
|
||||
*/
|
||||
export function regressionBadge(delta, suffix = 'vs baseline') {
|
||||
const d = Number(delta);
|
||||
if (!isFinite(d) || d > -0.05) return '';
|
||||
const pct = Math.round(d * 100);
|
||||
const red = d <= -0.1;
|
||||
const col = red ? '#ef4444' : '#f59e0b';
|
||||
const bg = red ? 'rgba(239,68,68,.12)' : 'rgba(245,158,11,.12)';
|
||||
return `<span style="display:inline-block;padding:2px 8px;border-radius:999px;font-size:.7rem;font-weight:600;color:${col};background:${bg}">${pct}% ${esc(suffix)}</span>`;
|
||||
}
|
||||
|
||||
/** Relative "time ago" label from an ISO string or epoch (ms or s). */
|
||||
export function timeAgo(input) {
|
||||
if (input == null) return '';
|
||||
let ms;
|
||||
if (typeof input === 'number') {
|
||||
ms = input < 1e12 ? input * 1000 : input;
|
||||
} else {
|
||||
const t = Date.parse(input);
|
||||
if (isNaN(t)) return '';
|
||||
ms = t;
|
||||
}
|
||||
const diff = Date.now() - ms;
|
||||
if (diff < 0) return 'now';
|
||||
const s = Math.floor(diff / 1000);
|
||||
if (s < 60) return `${s}s ago`;
|
||||
const m = Math.floor(s / 60);
|
||||
if (m < 60) return `${m}m ago`;
|
||||
const h = Math.floor(m / 60);
|
||||
if (h < 24) return `${h}h ago`;
|
||||
return `${Math.floor(h / 24)}d ago`;
|
||||
}
|
||||
|
||||
/** Format a token count into a compact string (mirrors usage.js). */
|
||||
export function formatTokens(tokens) {
|
||||
const n = Number(tokens) || 0;
|
||||
if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M`;
|
||||
if (n >= 1000) return `${(n / 1000).toFixed(1)}K`;
|
||||
return String(Math.round(n));
|
||||
}
|
||||
|
||||
/** Format a USD cost into a string (mirrors usage.js). */
|
||||
export function formatCost(cost) {
|
||||
const n = Number(cost) || 0;
|
||||
if (n < 0.01) return `$${n.toFixed(4)}`;
|
||||
if (n < 1) return `$${n.toFixed(3)}`;
|
||||
return `$${n.toFixed(2)}`;
|
||||
}
|
||||
|
||||
/** Strip a provider prefix from a model id (anthropic/claude-3 → claude-3). */
|
||||
export function shortModelName(model) {
|
||||
const s = model == null ? '' : String(model);
|
||||
const idx = s.lastIndexOf('/');
|
||||
return idx >= 0 ? s.slice(idx + 1) : s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sum tokens from timeseries buckets belonging to the current local day.
|
||||
* Falls back to summing all buckets when no bucket carries a hour_key.
|
||||
*/
|
||||
export function sumTodayTokens(timeseries) {
|
||||
const arr = Array.isArray(timeseries) ? timeseries : [];
|
||||
const todayKey = new Date().toISOString().slice(0, 10);
|
||||
const dated = arr.filter((p) => typeof p.hour_key === 'string');
|
||||
const scope = dated.length ? dated.filter((p) => p.hour_key.startsWith(todayKey)) : arr;
|
||||
return scope.reduce((sum, p) => sum + (Number(p.tokens) || 0), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Percent of daily budget consumed, capped at 100.
|
||||
* Returns null when the budget is unset or non-positive (E3/E4).
|
||||
*/
|
||||
export function computeBudgetPct(today, budget) {
|
||||
const b = Number(budget);
|
||||
if (!b || b <= 0) return null;
|
||||
const pct = (Number(today) || 0) / b * 100;
|
||||
if (!isFinite(pct) || pct < 0) return 0;
|
||||
return Math.min(pct, 100);
|
||||
}
|
||||
|
||||
/** Banner level from a budget percentage and thresholds. */
|
||||
export function budgetLevelFor(pct, warnPct = 80, criticalPct = 95) {
|
||||
if (pct == null || isNaN(pct)) return 'none';
|
||||
if (pct >= criticalPct) return 'critical';
|
||||
if (pct >= warnPct) return 'warn';
|
||||
return 'none';
|
||||
}
|
||||
|
||||
/** Gauge fill colour by budget percentage. */
|
||||
export function budgetGaugeColor(pct) {
|
||||
if (pct == null) return 'var(--db-accent,#6366f1)';
|
||||
if (pct >= 95) return '#ef4444';
|
||||
if (pct >= 80) return '#f97316';
|
||||
if (pct >= 60) return '#eab308';
|
||||
return '#22c55e';
|
||||
}
|
||||
|
||||
const DONUT_PALETTE = ['#3b82f6', '#8b5cf6', '#22c55e', '#64748b'];
|
||||
|
||||
/**
|
||||
* Pure SVG donut (no chart library).
|
||||
*
|
||||
* @param {Array<{label: string, value: number, color?: string}>} slices
|
||||
* @param {number} [size=40]
|
||||
* @returns {string} SVG markup
|
||||
*/
|
||||
export function MiniDonutSVG(slices, size = 40) {
|
||||
const data = (Array.isArray(slices) ? slices : [])
|
||||
.filter((s) => s && (Number(s.value) || 0) > 0);
|
||||
const total = data.reduce((sum, s) => sum + (Number(s.value) || 0), 0);
|
||||
if (!data.length || total <= 0) {
|
||||
const r = size / 2 - 3;
|
||||
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" role="img" aria-label="No model data"><circle cx="${size / 2}" cy="${size / 2}" r="${r}" fill="none" stroke="var(--db-border,#3f3f46)" stroke-width="4" stroke-dasharray="3 3" /></svg>`;
|
||||
}
|
||||
const cx = size / 2;
|
||||
const cy = size / 2;
|
||||
const r = size / 2 - 3;
|
||||
const inner = r * 0.55;
|
||||
let angle = -Math.PI / 2;
|
||||
const parts = [];
|
||||
const labels = [];
|
||||
data.forEach((s, i) => {
|
||||
const frac = (Number(s.value) || 0) / total;
|
||||
const end = angle + frac * Math.PI * 2;
|
||||
const color = s.color || DONUT_PALETTE[i % DONUT_PALETTE.length];
|
||||
const large = end - angle > Math.PI ? 1 : 0;
|
||||
const x1 = cx + r * Math.cos(angle);
|
||||
const y1 = cy + r * Math.sin(angle);
|
||||
const x2 = cx + r * Math.cos(end);
|
||||
const y2 = cy + r * Math.sin(end);
|
||||
const ix2 = cx + inner * Math.cos(end);
|
||||
const iy2 = cy + inner * Math.sin(end);
|
||||
const ix1 = cx + inner * Math.cos(angle);
|
||||
const iy1 = cy + inner * Math.sin(angle);
|
||||
parts.push(`<path d="M${x1.toFixed(2)},${y1.toFixed(2)} A${r},${r} 0 ${large} 1 ${x2.toFixed(2)},${y2.toFixed(2)} L${ix2.toFixed(2)},${iy2.toFixed(2)} A${inner},${inner} 0 ${large} 0 ${ix1.toFixed(2)},${iy1.toFixed(2)} Z" fill="${color}" />`);
|
||||
labels.push(`${esc(s.label)} ${Math.round(frac * 100)}%`);
|
||||
angle = end;
|
||||
});
|
||||
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" role="img" aria-label="${esc(labels.join(', '))}">${parts.join('')}</svg>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce per-model usage rows to donut slices: top 3 by cost + aggregated Other.
|
||||
*
|
||||
* @param {Array<{model?: string, cost_usd?: number, total_tokens?: number}>} models
|
||||
* @returns {Array<{label: string, value: number, color: string}>}
|
||||
*/
|
||||
export function modelDonutSlices(models) {
|
||||
const rows = (Array.isArray(models) ? models : [])
|
||||
.map((m) => ({ label: shortModelName(m.model), value: Number(m.cost_usd) || Number(m.total_tokens) || 0 }))
|
||||
.filter((m) => m.value > 0)
|
||||
.sort((a, b) => b.value - a.value);
|
||||
const top = rows.slice(0, 3).map((m, i) => ({ ...m, color: DONUT_PALETTE[i] }));
|
||||
const rest = rows.slice(3).reduce((sum, m) => sum + m.value, 0);
|
||||
if (rest > 0) top.push({ label: 'Other', value: rest, color: DONUT_PALETTE[3] });
|
||||
return top;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the Overview Cost FinOps strip markup.
|
||||
*
|
||||
* @param {Object} opts
|
||||
* @param {number} opts.todayTokens
|
||||
* @param {number} [opts.todayCost]
|
||||
* @param {number|null} [opts.budget]
|
||||
* @param {number} [opts.warnPct]
|
||||
* @param {number} [opts.criticalPct]
|
||||
* @param {Array} [opts.models]
|
||||
* @param {number} [opts.delta] - signed pct vs yesterday
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
export function costStripHTML({ todayTokens, todayCost, budget = null, warnPct = 80, criticalPct = 95, models = [], delta } = {}) {
|
||||
const pct = computeBudgetPct(todayTokens, budget);
|
||||
const level = budgetLevelFor(pct, warnPct, criticalPct);
|
||||
const gaugeColor = budgetGaugeColor(pct);
|
||||
const slices = modelDonutSlices(models);
|
||||
const topModel = slices.length ? slices[0].label : '';
|
||||
const budgetText = pct == null
|
||||
? `${formatTokens(todayTokens)} tokens`
|
||||
: `${formatTokens(todayTokens)} / ${formatTokens(budget)} tokens (${Math.round(pct)}%)`;
|
||||
const gauge = pct == null ? '' : `<div class="db-finops-gauge" role="progressbar" aria-valuenow="${Math.round(pct)}" aria-valuemin="0" aria-valuemax="100" style="height:8px;border-radius:6px;background:var(--db-border,#3f3f46);overflow:hidden;margin-top:6px">
|
||||
<div class="db-finops-gauge-fill" style="width:${Math.round(pct)}%;height:100%;background:${gaugeColor};transition:width 400ms ease"></div>
|
||||
</div>`;
|
||||
const pill = level === 'none' ? '' : `<span class="db-finops-pill" style="font-size:11px;padding:2px 8px;border-radius:10px;margin-left:8px;background:${level === 'critical' ? 'rgba(239,68,68,.15)' : 'rgba(234,179,8,.15)'};color:${level === 'critical' ? '#ef4444' : '#eab308'}">${Math.round(pct)}%</span>`;
|
||||
const costText = todayCost != null ? `Est. ${formatCost(todayCost)} today` : '';
|
||||
let deltaHtml = '';
|
||||
if (typeof delta === 'number' && !isNaN(delta) && delta !== 0) {
|
||||
const up = delta > 0;
|
||||
deltaHtml = `<span style="font-size:11px;color:${up ? '#ef4444' : '#22c55e'};margin-left:8px">${up ? '↗' : '↘'} ${Math.abs(delta).toFixed(0)}% vs yesterday</span>`;
|
||||
}
|
||||
return `<div id="ov-finops-strip" class="db-card db-finops-strip" role="region" aria-label="Cost and usage summary" tabindex="0" data-nav="usage"
|
||||
style="cursor:pointer;display:flex;align-items:center;gap:20px;padding:14px 18px;margin-top:16px;margin-bottom:8px;flex-wrap:wrap">
|
||||
<div style="display:flex;align-items:center;gap:8px;min-width:130px">
|
||||
<span aria-hidden="true">💰</span>
|
||||
<span style="font-size:13px;font-weight:600;color:var(--db-text)">Cost FinOps</span>${pill}
|
||||
</div>
|
||||
<div style="flex:1;min-width:200px">
|
||||
<div style="font-size:20px;font-weight:700;color:var(--db-text)">${esc(budgetText)}${deltaHtml}</div>
|
||||
<div style="font-size:12px;color:var(--db-text-dim,#a1a1aa)">${esc(costText)}</div>
|
||||
${gauge}
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
${MiniDonutSVG(slices, 40)}
|
||||
<span style="font-size:12px;color:var(--db-text-dim,#a1a1aa)">${esc(topModel)}</span>
|
||||
</div>
|
||||
<span style="font-size:12px;color:var(--db-accent,#6366f1)">View usage →</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a FinOps budget banner (Overview or Chat).
|
||||
*
|
||||
* @param {string} level - 'none' | 'warn' | 'critical'
|
||||
* @param {number} pct
|
||||
* @returns {string} HTML string ('' when level is none)
|
||||
*/
|
||||
export function budgetBannerHTML(level, pct) {
|
||||
if (level === 'none') return '';
|
||||
const critical = level === 'critical';
|
||||
const color = critical ? '#ef4444' : '#eab308';
|
||||
const bg = critical ? 'rgba(239,68,68,.1)' : 'rgba(234,179,8,.1)';
|
||||
const copy = critical
|
||||
? `Daily token budget at ${Math.round(pct)}% — review usage`
|
||||
: `Daily token budget at ${Math.round(pct)}% — consider lighter models`;
|
||||
return `<div class="db-finops-banner" role="${critical ? 'alert' : 'status'}"
|
||||
style="display:flex;align-items:center;gap:8px;padding:8px 14px;border:1px solid ${color};border-radius:10px;background:${bg};color:${color};font-size:12px;font-weight:500;margin-bottom:8px">
|
||||
<span aria-hidden="true">💰</span>
|
||||
<span style="flex:1">${esc(copy)}</span>
|
||||
<button type="button" class="db-finops-banner-usage" style="background:none;border:none;color:inherit;cursor:pointer;font-size:12px;text-decoration:underline">Open usage</button>
|
||||
<button type="button" class="db-finops-banner-dismiss" title="Dismiss" aria-label="Dismiss" style="background:none;border:none;color:inherit;cursor:pointer;font-size:13px;padding:0 4px">✕</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/** Modal shell for view-local dialogs. */
|
||||
export function modalShell(id, innerHtml) {
|
||||
return `<div id="${id}" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:100;align-items:center;justify-content:center;padding:20px">
|
||||
<div style="background:var(--db-sidebar-bg);border:1px solid var(--db-border);border-radius:12px;max-width:520px;width:100%;max-height:90vh;overflow:auto;padding:20px">${innerHtml}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Line-level diff between two text blocks using an LCS table.
|
||||
* Normalises CRLF/CR to LF before comparing. Bounded for large inputs.
|
||||
*
|
||||
* @param {string} before
|
||||
* @param {string} after
|
||||
* @param {number} [maxLines=2000] - Truncate each side to this many lines.
|
||||
* @returns {{type:('add'|'del'|'ctx'), text:string}[]}
|
||||
*/
|
||||
export function diffLines(before, after, maxLines = 2000) {
|
||||
const norm = (s) => String(s == null ? '' : s).replace(/\r\n?/g, '\n');
|
||||
const a = norm(before).split('\n').slice(0, maxLines);
|
||||
const b = norm(after).split('\n').slice(0, maxLines);
|
||||
const n = a.length;
|
||||
const m = b.length;
|
||||
const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
||||
for (let i = n - 1; i >= 0; i--) {
|
||||
for (let j = m - 1; j >= 0; j--) {
|
||||
lcs[i][j] = a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
|
||||
}
|
||||
}
|
||||
const out = [];
|
||||
let i = 0;
|
||||
let j = 0;
|
||||
while (i < n && j < m) {
|
||||
if (a[i] === b[j]) {
|
||||
out.push({ type: 'ctx', text: a[i] });
|
||||
i++;
|
||||
j++;
|
||||
} else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
|
||||
out.push({ type: 'del', text: a[i] });
|
||||
i++;
|
||||
} else {
|
||||
out.push({ type: 'add', text: b[j] });
|
||||
j++;
|
||||
}
|
||||
}
|
||||
while (i < n) { out.push({ type: 'del', text: a[i] }); i++; }
|
||||
while (j < m) { out.push({ type: 'add', text: b[j] }); j++; }
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal, XSS-safe markdown preview. All content is escaped first; only a
|
||||
* small, fixed set of inline/block constructs are re-enabled. No raw HTML,
|
||||
* scripts, or attributes from source ever reach the DOM.
|
||||
*
|
||||
* @param {string} md
|
||||
* @returns {string} sanitised HTML string
|
||||
*/
|
||||
export function renderMarkdownPreview(md) {
|
||||
const lines = String(md == null ? '' : md).replace(/\r\n?/g, '\n').split('\n');
|
||||
const inline = (s) => esc(s)
|
||||
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
||||
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
||||
const html = [];
|
||||
let inList = false;
|
||||
const closeList = () => { if (inList) { html.push('</ul>'); inList = false; } };
|
||||
for (const raw of lines) {
|
||||
const line = raw.replace(/\t/g, ' ');
|
||||
const h = line.match(/^(#{1,6})\s+(.*)$/);
|
||||
const li = line.match(/^\s*[-*]\s+(.*)$/);
|
||||
if (h) {
|
||||
closeList();
|
||||
const lvl = h[1].length;
|
||||
html.push(`<h${lvl}>${inline(h[2])}</h${lvl}>`);
|
||||
} else if (li) {
|
||||
if (!inList) { html.push('<ul>'); inList = true; }
|
||||
html.push(`<li>${inline(li[1])}</li>`);
|
||||
} else if (line.trim() === '') {
|
||||
closeList();
|
||||
} else {
|
||||
closeList();
|
||||
html.push(`<p>${inline(line)}</p>`);
|
||||
}
|
||||
}
|
||||
closeList();
|
||||
return html.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* True when a pending approval represents a skill write (rules R1-R5).
|
||||
*
|
||||
* @param {Object} a - approval record
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isSkillApproval(a) {
|
||||
if (!a || typeof a !== 'object') return false;
|
||||
const NAMES = ['write_skill', 'skill_write', 'save_skill', 'create_skill'];
|
||||
const tool = String(a.tool_name || '').toLowerCase();
|
||||
const action = String(a.action || '').toLowerCase();
|
||||
if (NAMES.includes(tool) || NAMES.includes(action)) return true;
|
||||
if (String(a.type || '').toLowerCase() === 'skill_write') return true;
|
||||
if (a.metadata && String(a.metadata.kind || '').toLowerCase() === 'skill') return true;
|
||||
const path = String((a.arguments && a.arguments.path) || '');
|
||||
if (path.endsWith('SKILL.md') || path.includes('.praisonai/skills/')) return true;
|
||||
return false;
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
* Approvals View — execution approval queue
|
||||
* API: /api/approvals
|
||||
*/
|
||||
import { isSkillApproval } from '/plugins/views/_helpers.js';
|
||||
|
||||
export async function render(container) {
|
||||
container.innerHTML = '<div class="db-loading"><div class="db-spinner"></div></div>';
|
||||
|
||||
@@ -33,11 +35,13 @@ export async function render(container) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'db-card';
|
||||
card.style.cssText = 'margin-bottom:10px;padding:16px 20px;display:flex;align-items:center;justify-content:space-between';
|
||||
const isSkill = isSkillApproval(a);
|
||||
card.innerHTML = `
|
||||
<div style="flex:1">
|
||||
<div style="font-weight:500;font-size:14px">${riskIcons[a.risk_level] || '❓'} ${a.tool_name || a.action || 'Unknown'}</div>
|
||||
<div style="font-size:12px;color:var(--db-text-dim);margin-top:4px">Agent: ${a.agent_name || '—'} · ${a.reason || ''}</div>
|
||||
<div style="font-size:11px;color:var(--db-text-dim)">${a.created_at ? new Date(a.created_at * 1000).toLocaleString() : ''}</div>
|
||||
${isSkill ? `<a href="/skills?tab=pending&approval_id=${encodeURIComponent(a.id || a.approval_id || '')}" data-skill-studio="1" style="font-size:11px;color:var(--db-accent);text-decoration:none">Skill write? Open Skills Studio →</a>` : ''}
|
||||
</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
<button class="approv-yes" data-id="${a.id || a.approval_id}" style="padding:6px 16px;background:rgba(34,197,94,0.15);color:#22c55e;border:1px solid rgba(34,197,94,0.3);border-radius:6px;cursor:pointer;font-size:12px">✓ Approve</button>
|
||||
|
||||
@@ -90,7 +90,10 @@ export async function render(container) {
|
||||
<h2 style="margin:0;font-size:1.5rem">Messaging Channels</h2>
|
||||
<div style="margin-top:4px;font-size:13px;color:var(--db-text-dim)">${online} Online · ${channels.length} Total</div>
|
||||
</div>
|
||||
<button id="ch-add" style="padding:8px 18px;background:var(--db-accent,#6366f1);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600;font-size:.85rem">+ Add Channel</button>
|
||||
<div style="display:flex;align-items:center;gap:12px">
|
||||
<a href="/inbox" style="font-size:.85rem;color:var(--db-accent,#6366f1);text-decoration:none">Open Inbox →</a>
|
||||
<button id="ch-add" style="padding:8px 18px;background:var(--db-accent,#6366f1);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600;font-size:.85rem">+ Add Channel</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ch-grid" class="db-columns" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:16px"></div>
|
||||
<div id="ch-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:100;align-items:center;justify-content:center"></div>
|
||||
|
||||
@@ -22,6 +22,7 @@ let agents = [];
|
||||
let messageQueue = [];
|
||||
let focusMode = false;
|
||||
let sessionSelectHandler = null;
|
||||
let prefillHandler = null;
|
||||
let containerRef = null;
|
||||
let currentDeltaEl = null;
|
||||
let currentDeltaBodyEl = null;
|
||||
@@ -29,6 +30,58 @@ let currentDeltaText = '';
|
||||
let pendingMediaElements = [];
|
||||
let pendingAttachments = []; // { id, filename, content_type, preview_url }
|
||||
|
||||
// ── Context Ring State ───────────────────────────────────────────
|
||||
const RING_CIRCUMFERENCE = 2 * Math.PI * 20; // r=20 → ~125.66
|
||||
let contextPollTimer = null;
|
||||
let contextRefreshDebounce = null;
|
||||
let lastContextPct = 0;
|
||||
let finopsConfig = null;
|
||||
let sessionCostState = { tokens: 0, model: '', estimate: false };
|
||||
let lastUserMessage = ''; // last sent user text, drives per-turn memory search
|
||||
|
||||
// ── i18n State ───────────────────────────────────────────────────
|
||||
let i18nStrings = {};
|
||||
const I18N_FALLBACK = {
|
||||
'chat.context.healthy': 'Context healthy',
|
||||
'chat.context.warning': 'Approaching context limit',
|
||||
'chat.context.critical': 'Context nearly full',
|
||||
'chat.context.estimate': 'Estimate',
|
||||
'chat.compaction.banner': 'Context nearly full — older messages may be summarized on next send',
|
||||
'chat.compaction.compacted': 'Context compacted',
|
||||
'chat.memory.chip': '{count} memories',
|
||||
'chat.memory.empty_turn': 'No memories for this turn',
|
||||
};
|
||||
|
||||
function t(key, params) {
|
||||
let text = i18nStrings[key] != null ? i18nStrings[key] : (I18N_FALLBACK[key] != null ? I18N_FALLBACK[key] : key);
|
||||
if (params) {
|
||||
for (const k in params) {
|
||||
text = text.split('{' + k + '}').join(String(params[k]));
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
async function loadI18nStrings() {
|
||||
let locale = 'en';
|
||||
try {
|
||||
const stored = (typeof localStorage !== 'undefined') ? localStorage.getItem('aiui-locale') : null;
|
||||
if (stored) {
|
||||
locale = stored;
|
||||
} else {
|
||||
const lr = await fetch('/api/i18n/locale');
|
||||
if (lr.ok) { const ld = await lr.json(); if (ld && ld.locale) locale = ld.locale; }
|
||||
}
|
||||
} catch (e) { /* fall back to English */ }
|
||||
try {
|
||||
const resp = await fetch('/api/i18n/strings/' + encodeURIComponent(locale));
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
i18nStrings = (data && data.strings) || {};
|
||||
}
|
||||
} catch (e) { /* keep English fallback */ }
|
||||
}
|
||||
|
||||
// ── Floating Chat State ──────────────────────────────────────────
|
||||
let chatModeConfig = { mode: 'fullpage' };
|
||||
let isMinimized = false;
|
||||
@@ -277,6 +330,72 @@ function injectStyles() {
|
||||
.chat-metrics { display:flex; gap:12px; padding:4px 12px; font-size:11px; color:var(--db-text-dim); }
|
||||
.chat-metrics span { display:flex; align-items:center; gap:4px; }
|
||||
|
||||
/* ── Context Token Ring ─────────────────────────────────────── */
|
||||
.chat-ring-wrap { position:relative; width:48px; height:48px; min-width:48px; flex-shrink:0; }
|
||||
.chat-ring-wrap svg { position:absolute; inset:0; width:48px; height:48px; transform:rotate(-90deg); }
|
||||
.chat-ring-track { fill:none; stroke:var(--db-border,#333); stroke-width:4; }
|
||||
.chat-ring-arc { fill:none; stroke-width:4; stroke-linecap:round; transition:stroke-dashoffset .2s ease, stroke .2s ease; }
|
||||
.chat-ring-healthy .chat-ring-arc { stroke:#22c55e; }
|
||||
.chat-ring-warning .chat-ring-arc { stroke:#eab308; }
|
||||
.chat-ring-critical .chat-ring-arc { stroke:#eab308; }
|
||||
.chat-ring-over .chat-ring-arc { stroke:#ef4444; }
|
||||
.chat-ring-estimate .chat-ring-arc { stroke-dasharray:4 3 !important; }
|
||||
.chat-ring-over .chat-ring-arc { animation:ring-pulse 1.1s ease-in-out infinite; }
|
||||
@keyframes ring-pulse { 50% { opacity:.4; } }
|
||||
.chat-ring-send { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:32px; height:32px; border-radius:50%; border:none; background:var(--db-accent); color:#fff; cursor:pointer; font-size:15px; display:flex; align-items:center; justify-content:center; padding:0; transition:filter .15s; }
|
||||
.chat-ring-send:hover { filter:brightness(1.1); }
|
||||
.chat-ring-send:disabled { opacity:.5; cursor:not-allowed; }
|
||||
.chat-ring-tooltip { position:absolute; bottom:56px; right:0; z-index:20; display:none; min-width:180px; padding:10px 12px; background:var(--db-card-bg,#27272a); border:1px solid var(--db-border); border-radius:8px; box-shadow:0 8px 24px rgba(0,0,0,.4); font-size:11px; color:var(--db-text); }
|
||||
.chat-ring-wrap:hover .chat-ring-tooltip, .chat-ring-wrap:focus-within .chat-ring-tooltip { display:block; }
|
||||
.chat-ring-tooltip h5 { margin:0 0 6px; font-size:11px; font-weight:600; }
|
||||
.chat-ring-tt-row { display:flex; align-items:center; gap:6px; margin:3px 0; }
|
||||
.chat-ring-tt-row .lbl { width:64px; color:var(--db-text-dim); }
|
||||
.chat-ring-tt-bar { flex:1; height:6px; border-radius:3px; background:rgba(var(--db-accent-rgb,100,100,255),.15); overflow:hidden; }
|
||||
.chat-ring-tt-bar > i { display:block; height:100%; background:var(--db-accent); }
|
||||
.chat-ring-tt-val { width:36px; text-align:right; color:var(--db-text-dim); }
|
||||
.chat-ring-tt-total { margin-top:6px; padding-top:6px; border-top:1px solid var(--db-border); font-weight:600; }
|
||||
|
||||
/* ── Compaction Banner ──────────────────────────────────────── */
|
||||
.chat-compaction-banner { display:none; align-items:center; gap:8px; padding:8px 16px; border-bottom:1px solid rgba(234,179,8,.3); background:rgba(234,179,8,.1); color:#eab308; font-size:12.5px; }
|
||||
.chat-compaction-banner.open { display:flex; }
|
||||
.chat-compaction-banner.critical { border-bottom-color:rgba(239,68,68,.35); background:rgba(239,68,68,.1); color:#ef4444; }
|
||||
.chat-compaction-banner .cb-text { flex:1; }
|
||||
.chat-compaction-banner .cb-dismiss { background:none; border:none; cursor:pointer; color:inherit; font-size:14px; padding:0 4px; line-height:1; }
|
||||
|
||||
/* ── Memory Chip ────────────────────────────────────────────── */
|
||||
.chat-compose-footer { display:flex; align-items:center; gap:12px; padding:6px 2px 0; }
|
||||
.chat-memory-chip { display:none; align-items:center; gap:5px; padding:3px 10px; border-radius:14px; border:1px solid rgba(147,51,234,.25); background:rgba(147,51,234,.08); color:rgba(147,51,234,.9); font-size:12px; cursor:pointer; transition:all .15s; }
|
||||
.chat-memory-chip.visible { display:inline-flex; }
|
||||
.chat-memory-chip:hover { background:rgba(147,51,234,.16); }
|
||||
.chat-memory-chip.loading { opacity:.6; }
|
||||
|
||||
/* ── Session Cost Chip (FinOps) ─────────────────────────────── */
|
||||
.chat-session-cost-chip { display:none; align-items:center; gap:4px; font-size:11px; font-weight:500; padding:4px 10px; border-radius:20px; color:var(--db-text-dim); background:rgba(99,102,241,.08); border:1px solid rgba(99,102,241,.18); white-space:nowrap; cursor:default; }
|
||||
.chat-session-cost-chip.visible { display:inline-flex; }
|
||||
.chat-session-cost-chip.estimate { font-style:italic; }
|
||||
#chat-finops-banner:empty { display:none; }
|
||||
@media (max-width:480px) {
|
||||
.chat-session-cost-chip .cost-chip-model { display:none; }
|
||||
}
|
||||
.chat-floating-container .chat-session-cost-chip { display:none !important; }
|
||||
|
||||
/* ── Enhanced memory cards ──────────────────────────────────── */
|
||||
.chat-mem-item .mem-head { display:flex; align-items:center; gap:6px; margin-bottom:4px; }
|
||||
.chat-mem-item .mem-badge { font-size:10px; padding:1px 6px; border-radius:6px; background:rgba(147,51,234,.12); color:rgba(147,51,234,.85); font-weight:600; text-transform:uppercase; }
|
||||
.chat-mem-item .mem-time { font-size:10px; color:var(--db-text-dim); margin-left:auto; }
|
||||
.chat-mem-item .mem-relevance { height:5px; border-radius:3px; background:rgba(147,51,234,.12); overflow:hidden; margin-top:5px; }
|
||||
.chat-mem-item .mem-relevance > i { display:block; height:100%; background:rgba(147,51,234,.7); }
|
||||
.chat-mem-item .mem-snippet { display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
|
||||
|
||||
@media (max-width:520px) {
|
||||
.chat-ring-wrap, .chat-ring-wrap svg { width:36px; height:36px; min-width:36px; }
|
||||
.chat-ring-send { width:24px; height:24px; font-size:12px; }
|
||||
}
|
||||
@media (prefers-reduced-motion:reduce) {
|
||||
.chat-ring-arc { transition:none; }
|
||||
.chat-ring-over .chat-ring-arc { animation:none; }
|
||||
}
|
||||
|
||||
.chat-memory-panel { display:none; border-bottom:1px solid var(--db-border); background:var(--db-sidebar-bg,var(--db-card-bg)); max-height:300px; overflow-y:auto; padding:10px 14px; font-size:13px; }
|
||||
.chat-memory-panel.open { display:block; }
|
||||
.chat-memory-panel-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:8px; }
|
||||
@@ -621,11 +740,34 @@ function setupResizeHandles(wrapper) {
|
||||
});
|
||||
}
|
||||
|
||||
// ── Prefill Composer Listener (singleton) ───────────────────────
|
||||
// Code Studio's "Send to Agent" dispatches a window-level
|
||||
// 'aiui:prefill-composer' event. This SPA calls render() on every
|
||||
// visit to /chat, so the listener is installed once at module scope
|
||||
// and guarded to avoid stacking duplicate handlers across navigations.
|
||||
function ensurePrefillListener() {
|
||||
if (prefillHandler) return;
|
||||
prefillHandler = (e) => {
|
||||
const text = e.detail && e.detail.text;
|
||||
if (!text) return;
|
||||
const el = document.getElementById('chat-input');
|
||||
if (!el) return;
|
||||
el.value = text;
|
||||
el.style.height = 'auto';
|
||||
el.style.height = Math.min(el.scrollHeight, 150) + 'px';
|
||||
el.focus();
|
||||
};
|
||||
window.addEventListener('aiui:prefill-composer', prefillHandler);
|
||||
}
|
||||
|
||||
// ── Main render (ES module entry point) ─────────────────────────
|
||||
export async function render(container) {
|
||||
injectStyles();
|
||||
containerRef = container;
|
||||
|
||||
|
||||
// Load translations for ring/banner/chip (English fallback on failure)
|
||||
await loadI18nStrings();
|
||||
|
||||
// Fetch chat mode config
|
||||
try {
|
||||
const configRes = await fetch('/ui-config.json');
|
||||
@@ -685,6 +827,11 @@ export async function render(container) {
|
||||
<button class="chat-icon-btn danger" id="chat-abort-btn" style="display:none" title="Stop">⬛ Stop</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-compaction-banner" id="chat-compaction-banner">
|
||||
<span>⚠</span>
|
||||
<span class="cb-text" id="chat-compaction-text"></span>
|
||||
<button class="cb-dismiss" id="chat-compaction-dismiss" title="Dismiss" aria-label="Dismiss">✕</button>
|
||||
</div>
|
||||
<div class="chat-memory-panel" id="chat-memory-panel">
|
||||
<div class="chat-memory-panel-header">
|
||||
<h4>🧠 Agent Memories</h4>
|
||||
@@ -703,15 +850,31 @@ export async function render(container) {
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-compose">
|
||||
<div id="chat-finops-banner"></div>
|
||||
<div class="chat-attach-strip" id="chat-attach-strip"></div>
|
||||
<div class="chat-compose-row">
|
||||
<button class="chat-attach-btn" id="chat-attach-btn" title="Attach file (PDF, image)">📎</button>
|
||||
<input type="file" id="chat-file-input" accept=".pdf,.png,.jpg,.jpeg,.gif,.webp,image/*,application/pdf" multiple style="display:none" />
|
||||
<textarea id="chat-input" placeholder="Type a message…" rows="1"></textarea>
|
||||
<div class="chat-compose-actions">
|
||||
<button class="chat-send-btn" id="chat-send-btn">Send ↵</button>
|
||||
<div class="chat-session-cost-chip" id="chat-session-cost-chip" tabindex="0"></div>
|
||||
<div class="chat-ring-wrap chat-ring-healthy" id="chat-ring-wrap"
|
||||
role="progressbar" aria-label="Context usage"
|
||||
aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
|
||||
<svg viewBox="0 0 48 48" aria-hidden="true">
|
||||
<circle class="chat-ring-track" cx="24" cy="24" r="20"></circle>
|
||||
<circle class="chat-ring-arc" id="chat-ring-arc" cx="24" cy="24" r="20"
|
||||
stroke-dasharray="125.66" stroke-dashoffset="125.66"></circle>
|
||||
</svg>
|
||||
<button class="chat-ring-send" id="chat-send-btn" title="Send"
|
||||
type="button" aria-label="Send message">➤</button>
|
||||
<div class="chat-ring-tooltip" id="chat-ring-tooltip"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-compose-footer">
|
||||
<button class="chat-memory-chip" id="chat-memory-chip" aria-expanded="false"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -738,6 +901,8 @@ export async function render(container) {
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
|
||||
});
|
||||
|
||||
ensurePrefillListener();
|
||||
input.addEventListener('input', () => {
|
||||
input.style.height = 'auto';
|
||||
input.style.height = Math.min(input.scrollHeight, 150) + 'px';
|
||||
@@ -766,6 +931,16 @@ export async function render(container) {
|
||||
if (panel) panel.classList.remove('open');
|
||||
});
|
||||
|
||||
const memChip = document.getElementById('chat-memory-chip');
|
||||
if (memChip) memChip.addEventListener('click', () => toggleMemoryPanel());
|
||||
const compactionDismiss = document.getElementById('chat-compaction-dismiss');
|
||||
if (compactionDismiss) compactionDismiss.addEventListener('click', dismissCompactionBanner);
|
||||
|
||||
// Initial context ring render (estimate for empty/new session)
|
||||
refreshContextStats();
|
||||
startContextPoll();
|
||||
loadFinopsConfig();
|
||||
|
||||
await loadAgents();
|
||||
|
||||
// Pre-select agent if passed via URL query param (from Agents page "Run" button)
|
||||
@@ -798,10 +973,16 @@ export async function render(container) {
|
||||
|
||||
export function cleanup() {
|
||||
if (ws) { try { ws.close(); } catch(e) {} ws = null; }
|
||||
stopContextPoll();
|
||||
if (contextRefreshDebounce) { clearTimeout(contextRefreshDebounce); contextRefreshDebounce = null; }
|
||||
if (sessionSelectHandler) {
|
||||
window.removeEventListener('aiui:session-select', sessionSelectHandler);
|
||||
sessionSelectHandler = null;
|
||||
}
|
||||
if (prefillHandler) {
|
||||
window.removeEventListener('aiui:prefill-composer', prefillHandler);
|
||||
prefillHandler = null;
|
||||
}
|
||||
currentSessionId = null;
|
||||
currentAgentName = null;
|
||||
isStreaming = false;
|
||||
@@ -902,6 +1083,9 @@ async function loadSessions() {
|
||||
async function loadSession(sessionId) {
|
||||
currentSessionId = sessionId;
|
||||
refreshStreamingUI();
|
||||
// E13: reset context ring + memory chip for the new session
|
||||
refreshContextStats();
|
||||
refreshMemoryChip();
|
||||
const messagesEl = document.getElementById('chat-messages');
|
||||
const welcome = document.getElementById('chat-welcome');
|
||||
if (welcome) welcome.style.display = 'none';
|
||||
@@ -1087,6 +1271,25 @@ function handleWsMessage(data) {
|
||||
setStatus('connected');
|
||||
setStreaming(false);
|
||||
drainQueue();
|
||||
// Refresh context ring + memory chip after turn completes
|
||||
scheduleContextRefresh();
|
||||
refreshMemoryChip();
|
||||
updateFinopsBanner();
|
||||
break;
|
||||
|
||||
case 'context_update':
|
||||
updateContextRing({
|
||||
used: data.used,
|
||||
limit: data.limit,
|
||||
usage_pct: data.usage_pct,
|
||||
breakdown: data.breakdown,
|
||||
estimate: data.estimate,
|
||||
});
|
||||
break;
|
||||
|
||||
case 'context_compacted':
|
||||
flashCompactionCompacted(data);
|
||||
scheduleContextRefresh();
|
||||
break;
|
||||
|
||||
case 'run_started':
|
||||
@@ -1171,6 +1374,8 @@ function handleWsMessage(data) {
|
||||
setStatus('connected');
|
||||
setStreaming(false);
|
||||
drainQueue();
|
||||
scheduleContextRefresh();
|
||||
refreshMemoryChip();
|
||||
break;
|
||||
|
||||
case 'team_run_error':
|
||||
@@ -1712,6 +1917,203 @@ function completeMemoryIndicator() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Context Token Ring ──────────────────────────────────────────
|
||||
function ringStateFor(pct) {
|
||||
if (pct >= 95) return 'over';
|
||||
if (pct >= 85) return 'critical';
|
||||
if (pct >= 60) return 'warning';
|
||||
return 'healthy';
|
||||
}
|
||||
|
||||
function updateContextRing(stats) {
|
||||
const wrap = document.getElementById('chat-ring-wrap');
|
||||
const arc = document.getElementById('chat-ring-arc');
|
||||
if (!wrap || !arc) return;
|
||||
|
||||
const used = Number(stats.used) || 0;
|
||||
const limit = Number(stats.limit) || 0;
|
||||
const estimate = !!stats.estimate;
|
||||
|
||||
// E2: unknown/zero limit — hide ring, keep send button usable
|
||||
if (!limit || limit <= 0) {
|
||||
wrap.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
wrap.style.display = '';
|
||||
|
||||
let pct = stats.usage_pct != null ? Number(stats.usage_pct) : (used / limit) * 100;
|
||||
if (!isFinite(pct) || pct < 0) pct = 0;
|
||||
const clamped = Math.min(pct, 100);
|
||||
lastContextPct = pct;
|
||||
|
||||
const state = ringStateFor(pct);
|
||||
wrap.className = 'chat-ring-wrap chat-ring-' + state + (estimate ? ' chat-ring-estimate' : '');
|
||||
arc.setAttribute('stroke-dasharray', RING_CIRCUMFERENCE.toFixed(2));
|
||||
arc.setAttribute('stroke-dashoffset', (RING_CIRCUMFERENCE * (1 - clamped / 100)).toFixed(2));
|
||||
|
||||
wrap.setAttribute('aria-valuenow', String(Math.round(pct)));
|
||||
wrap.setAttribute('aria-valuemax', '100');
|
||||
|
||||
updateRingTooltip(stats, pct);
|
||||
updateCompactionBanner(pct, estimate);
|
||||
updateSessionCostChip(stats);
|
||||
}
|
||||
|
||||
function updateRingTooltip(stats, pct) {
|
||||
const tip = document.getElementById('chat-ring-tooltip');
|
||||
if (!tip) return;
|
||||
const b = stats.breakdown || {};
|
||||
const used = Number(stats.used) || 0;
|
||||
const limit = Number(stats.limit) || 0;
|
||||
const fmt = (n) => n >= 1000 ? (n / 1000).toFixed(n >= 10000 ? 0 : 1) + 'k' : String(n);
|
||||
const rows = [
|
||||
['System', b.system || 0],
|
||||
['Tools', b.tools || 0],
|
||||
['Messages', b.messages || 0],
|
||||
['Memory', b.memory || 0],
|
||||
];
|
||||
const maxVal = Math.max(1, ...rows.map((r) => r[1]));
|
||||
const statusKey = pct >= 85 ? 'chat.context.critical' : (pct >= 60 ? 'chat.context.warning' : 'chat.context.healthy');
|
||||
const title = stats.estimate ? escapeHtml(t('chat.context.estimate')) : escapeHtml(t(statusKey));
|
||||
tip.innerHTML =
|
||||
'<h5>' + title + '</h5>' +
|
||||
'<div class="chat-ring-tt-status">' + escapeHtml(t(statusKey)) + '</div>' +
|
||||
rows.map(([lbl, val]) =>
|
||||
'<div class="chat-ring-tt-row">' +
|
||||
'<span class="lbl">' + lbl + '</span>' +
|
||||
'<span class="chat-ring-tt-bar"><i style="width:' + Math.round((val / maxVal) * 100) + '%"></i></span>' +
|
||||
'<span class="chat-ring-tt-val">' + fmt(val) + '</span>' +
|
||||
'</div>'
|
||||
).join('') +
|
||||
'<div class="chat-ring-tt-total">Total: ' + fmt(used) + ' / ' + fmt(limit) + ' (' + Math.round(pct) + '%)</div>';
|
||||
}
|
||||
|
||||
function updateCompactionBanner(pct, estimate) {
|
||||
const banner = document.getElementById('chat-compaction-banner');
|
||||
const textEl = document.getElementById('chat-compaction-text');
|
||||
if (!banner || !textEl) return;
|
||||
|
||||
if (pct < 85) {
|
||||
banner.classList.remove('open', 'critical');
|
||||
return;
|
||||
}
|
||||
// E4: respect per-session dismissal until usage drops below 85 then rises
|
||||
if (sessionStorage.getItem('aiui-compaction-dismissed') === (currentSessionId || 'default')) {
|
||||
if (pct < 85) sessionStorage.removeItem('aiui-compaction-dismissed');
|
||||
else return;
|
||||
}
|
||||
banner.classList.add('open');
|
||||
banner.classList.toggle('critical', pct >= 95);
|
||||
banner.setAttribute('role', pct >= 95 ? 'alert' : 'status');
|
||||
textEl.textContent = t('chat.compaction.banner');
|
||||
}
|
||||
|
||||
function dismissCompactionBanner() {
|
||||
const banner = document.getElementById('chat-compaction-banner');
|
||||
if (banner) banner.classList.remove('open', 'critical');
|
||||
sessionStorage.setItem('aiui-compaction-dismissed', currentSessionId || 'default');
|
||||
}
|
||||
|
||||
// E21: flash a transient "Context compacted" banner after WS event
|
||||
function flashCompactionCompacted(data) {
|
||||
const banner = document.getElementById('chat-compaction-banner');
|
||||
const textEl = document.getElementById('chat-compaction-text');
|
||||
if (!banner || !textEl) return;
|
||||
banner.classList.add('open');
|
||||
banner.classList.remove('critical');
|
||||
banner.setAttribute('role', 'status');
|
||||
const removed = data && data.removed_tokens ? ' (' + data.removed_tokens + ' tokens freed)' : '';
|
||||
textEl.textContent = t('chat.compaction.compacted') + removed;
|
||||
sessionStorage.removeItem('aiui-compaction-dismissed');
|
||||
setTimeout(() => {
|
||||
if (lastContextPct < 85) banner.classList.remove('open');
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
async function refreshContextStats() {
|
||||
try {
|
||||
const qs = currentSessionId ? '?session_id=' + encodeURIComponent(currentSessionId) : '';
|
||||
const resp = await fetch('/api/chat/context-stats' + qs);
|
||||
if (!resp.ok) return; // E1: keep last estimate on error
|
||||
const stats = await resp.json();
|
||||
updateContextRing(stats);
|
||||
} catch (e) {
|
||||
// E1/E11: network error — ring keeps prior state
|
||||
}
|
||||
}
|
||||
|
||||
// E10: debounce rapid refreshes (e.g. multiple completions)
|
||||
function scheduleContextRefresh() {
|
||||
if (contextRefreshDebounce) clearTimeout(contextRefreshDebounce);
|
||||
contextRefreshDebounce = setTimeout(refreshContextStats, 100);
|
||||
}
|
||||
|
||||
// E11: poll fallback when WS context_update isn't emitted by backend
|
||||
function startContextPoll() {
|
||||
if (contextPollTimer) clearInterval(contextPollTimer);
|
||||
contextPollTimer = setInterval(refreshContextStats, 5000);
|
||||
}
|
||||
|
||||
function stopContextPoll() {
|
||||
if (contextPollTimer) { clearInterval(contextPollTimer); contextPollTimer = null; }
|
||||
}
|
||||
|
||||
// ── Memory Chip ─────────────────────────────────────────────────
|
||||
function updateMemoryChip(count, loading) {
|
||||
const chip = document.getElementById('chat-memory-chip');
|
||||
if (!chip) return;
|
||||
if (loading) {
|
||||
chip.classList.add('visible', 'loading');
|
||||
chip.innerHTML = '🧠 …';
|
||||
return;
|
||||
}
|
||||
chip.classList.remove('loading');
|
||||
const n = Number(count) || 0;
|
||||
if (n <= 0) {
|
||||
// E5: no memories retrieved this turn — show muted empty-turn label
|
||||
chip.classList.add('visible');
|
||||
chip.title = t('chat.memory.empty_turn');
|
||||
chip.innerHTML = '🧠 0';
|
||||
return;
|
||||
}
|
||||
chip.classList.add('visible');
|
||||
chip.title = '';
|
||||
chip.innerHTML = '🧠 ' + escapeHtml(t('chat.memory.chip', { count: n }));
|
||||
}
|
||||
|
||||
// Per-turn memory search: count hits for the last user message, not total store.
|
||||
async function refreshMemoryChip() {
|
||||
const chip = document.getElementById('chat-memory-chip');
|
||||
if (!chip) return;
|
||||
const query = (lastUserMessage || '').trim();
|
||||
if (!query) {
|
||||
updateMemoryChip(0, false);
|
||||
return;
|
||||
}
|
||||
updateMemoryChip(0, true);
|
||||
try {
|
||||
const resp = await fetch('/api/memory/search', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
limit: 10,
|
||||
session_id: currentSessionId || undefined,
|
||||
}),
|
||||
});
|
||||
if (!resp.ok) { chip.classList.remove('loading'); chip.classList.add('visible'); chip.title = ''; chip.innerHTML = '🧠 —'; return; }
|
||||
const data = await resp.json();
|
||||
const results = data.results || [];
|
||||
updateMemoryChip(results.length, false);
|
||||
} catch (e) {
|
||||
// E6: memory search failed — muted dash
|
||||
chip.classList.remove('loading');
|
||||
chip.classList.add('visible');
|
||||
chip.title = '';
|
||||
chip.innerHTML = '🧠 —';
|
||||
}
|
||||
}
|
||||
|
||||
function showStreamMetrics(metrics) {
|
||||
let metricsEl = document.getElementById('chat-stream-metrics');
|
||||
if (!metricsEl) {
|
||||
@@ -1729,11 +2131,132 @@ function showStreamMetrics(metrics) {
|
||||
metricsEl.innerHTML = parts.join('') || '';
|
||||
}
|
||||
|
||||
// ── Session Cost Chip (FinOps / STITCH-013) ─────────────────────
|
||||
function finopsChipEnabled() {
|
||||
if (!finopsConfig) return true; // default on until config loads
|
||||
if (finopsConfig.enabled === false) return false;
|
||||
return finopsConfig.show_session_chip !== false;
|
||||
}
|
||||
|
||||
function fmtChipTokens(n) {
|
||||
const v = Number(n) || 0;
|
||||
if (v >= 1000000) return (v / 1000000).toFixed(1) + 'M';
|
||||
if (v >= 1000) return (v / 1000).toFixed(1) + 'k';
|
||||
return String(Math.round(v));
|
||||
}
|
||||
|
||||
function shortModel(model) {
|
||||
const s = model == null ? '' : String(model);
|
||||
const idx = s.lastIndexOf('/');
|
||||
return idx >= 0 ? s.slice(idx + 1) : s;
|
||||
}
|
||||
|
||||
function updateSessionCostChip(stats) {
|
||||
const chip = document.getElementById('chat-session-cost-chip');
|
||||
if (!chip) return;
|
||||
// E9/E2: hide when no session, feature disabled, or chip toggled off
|
||||
if (!currentSessionId || !finopsChipEnabled()) {
|
||||
chip.classList.remove('visible');
|
||||
chip.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
const tokens = Number(stats && stats.used) || sessionCostState.tokens || 0;
|
||||
const model = shortModel((stats && stats.model) || sessionCostState.model || currentAgentName || '');
|
||||
const estimate = !!(stats && stats.estimate);
|
||||
sessionCostState = { tokens, model, estimate };
|
||||
if (tokens <= 0) {
|
||||
chip.classList.remove('visible');
|
||||
chip.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
chip.classList.add('visible');
|
||||
chip.classList.toggle('estimate', estimate);
|
||||
const prefix = estimate ? '~' : '';
|
||||
const modelHtml = model
|
||||
? ` · <span class="cost-chip-model">${escapeChip(model)}</span>`
|
||||
: '';
|
||||
chip.innerHTML = prefix + fmtChipTokens(tokens) + modelHtml;
|
||||
chip.setAttribute('aria-label', 'Session token usage ' + Math.round(tokens) + ' tokens, model ' + (model || 'unknown'));
|
||||
chip.title = 'Session spend\nTokens: ' + Math.round(tokens).toLocaleString() + '\nModel: ' + (model || 'unknown');
|
||||
}
|
||||
|
||||
function escapeChip(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s == null ? '' : String(s);
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
async function loadFinopsConfig() {
|
||||
try {
|
||||
const res = await fetch('/api/config/runtime');
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
finopsConfig = (data && data.config && data.config.finops) || finopsConfig;
|
||||
} catch (e) {
|
||||
// graceful: keep default-on behaviour
|
||||
}
|
||||
updateFinopsBanner();
|
||||
}
|
||||
|
||||
function finopsBannerDismissed() {
|
||||
return sessionStorage.getItem('finops-banner-dismissed') === '1';
|
||||
}
|
||||
|
||||
async function updateFinopsBanner() {
|
||||
const host = document.getElementById('chat-finops-banner');
|
||||
if (!host) return;
|
||||
if (!finopsConfig || finopsConfig.enabled === false) { host.innerHTML = ''; return; }
|
||||
const budget = finopsConfig.daily_token_budget != null ? Number(finopsConfig.daily_token_budget) : null;
|
||||
if (!budget || budget <= 0) { host.innerHTML = ''; return; }
|
||||
if (finopsBannerDismissed()) { host.innerHTML = ''; return; }
|
||||
let today = 0;
|
||||
try {
|
||||
const res = await fetch('/api/usage/timeseries?hours=24');
|
||||
if (!res.ok) { host.innerHTML = ''; return; } // E1: hide, no toast
|
||||
const data = await res.json();
|
||||
const ts = Array.isArray(data.timeseries) ? data.timeseries : [];
|
||||
const todayKey = new Date().toISOString().slice(0, 10);
|
||||
const dated = ts.filter((p) => typeof p.hour_key === 'string');
|
||||
const scope = dated.length ? dated.filter((p) => p.hour_key.startsWith(todayKey)) : ts;
|
||||
today = scope.reduce((sum, p) => sum + (Number(p.tokens) || 0), 0);
|
||||
} catch (e) {
|
||||
host.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
const pct = Math.min((today / budget) * 100, 100);
|
||||
const warnPct = Number(finopsConfig.warn_pct) || 80;
|
||||
const criticalPct = Number(finopsConfig.critical_pct) || 95;
|
||||
if (pct < warnPct) { host.innerHTML = ''; return; }
|
||||
const critical = pct >= criticalPct;
|
||||
const color = critical ? '#ef4444' : '#eab308';
|
||||
const bg = critical ? 'rgba(239,68,68,.1)' : 'rgba(234,179,8,.1)';
|
||||
const copy = critical
|
||||
? 'Daily token budget at ' + Math.round(pct) + '% — review usage'
|
||||
: 'Daily token budget at ' + Math.round(pct) + '% — consider lighter models';
|
||||
host.innerHTML =
|
||||
'<div class="chat-finops-banner-inner" role="' + (critical ? 'alert' : 'status') + '"' +
|
||||
' style="display:flex;align-items:center;gap:8px;padding:8px 14px;margin-bottom:8px;border:1px solid ' + color +
|
||||
';border-radius:10px;background:' + bg + ';color:' + color + ';font-size:12px;font-weight:500">' +
|
||||
'<span aria-hidden="true">💰</span>' +
|
||||
'<span style="flex:1">' + escapeChip(copy) + '</span>' +
|
||||
'<button type="button" class="finops-banner-dismiss" title="Dismiss" aria-label="Dismiss"' +
|
||||
' style="background:none;border:none;color:inherit;cursor:pointer;font-size:13px;padding:0 4px">✕</button>' +
|
||||
'</div>';
|
||||
const dismiss = host.querySelector('.finops-banner-dismiss');
|
||||
if (dismiss) dismiss.addEventListener('click', () => {
|
||||
sessionStorage.setItem('finops-banner-dismissed', '1');
|
||||
host.innerHTML = '';
|
||||
});
|
||||
}
|
||||
|
||||
// ── Memory Panel ────────────────────────────────────────────────
|
||||
function toggleMemoryPanel() {
|
||||
const panel = document.getElementById('chat-memory-panel');
|
||||
if (!panel) return;
|
||||
const isOpen = panel.classList.toggle('open');
|
||||
// E17: brain button + chip open the same panel; keep chip aria in sync
|
||||
const chip = document.getElementById('chat-memory-chip');
|
||||
if (chip) chip.setAttribute('aria-expanded', String(isOpen));
|
||||
if (isOpen) loadMemories();
|
||||
}
|
||||
|
||||
@@ -1763,12 +2286,26 @@ async function loadMemories() {
|
||||
return;
|
||||
}
|
||||
|
||||
listEl.innerHTML = items.map(m => `
|
||||
listEl.innerHTML = items.map(m => {
|
||||
const type = m.memory_type || m.type || 'long';
|
||||
const snippet = m.text || m.value || m.content || '';
|
||||
const relevance = Number(m.relevance != null ? m.relevance : m.score);
|
||||
const relPct = isFinite(relevance) ? Math.round(Math.max(0, Math.min(1, relevance)) * 100) : null;
|
||||
const ts = m.created_at || m.timestamp;
|
||||
const relBar = relPct != null
|
||||
? `<div class="mem-relevance" title="Relevance ${relPct}%"><i style="width:${relPct}%"></i></div>`
|
||||
: '';
|
||||
const timeStr = ts ? timeAgo(ts) : '';
|
||||
return `
|
||||
<div class="chat-mem-item">
|
||||
<div class="mem-type">${m.memory_type || 'long'}</div>
|
||||
<div class="mem-text">${escapeHtml(m.text || '')}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
<div class="mem-head">
|
||||
<span class="mem-badge">${escapeHtml(type)}</span>
|
||||
${timeStr ? `<span class="mem-time">${escapeHtml(timeStr)}</span>` : ''}
|
||||
</div>
|
||||
<div class="mem-text mem-snippet">${escapeHtml(snippet)}</div>
|
||||
${relBar}
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch (err) {
|
||||
listEl.innerHTML = `<span class="chat-mem-empty">Error: ${err.message}</span>`;
|
||||
}
|
||||
@@ -1873,6 +2410,9 @@ function sendMessage() {
|
||||
const content = input.value.trim();
|
||||
if (!content && pendingAttachments.length === 0) return;
|
||||
|
||||
// Track last user text so the memory chip searches this turn's query
|
||||
if (content) lastUserMessage = content;
|
||||
|
||||
// Generate session ID if needed
|
||||
if (!currentSessionId) {
|
||||
currentSessionId = crypto.randomUUID ? crypto.randomUUID() :
|
||||
@@ -2016,7 +2556,7 @@ function refreshStreamingUI() {
|
||||
const abortBtn = document.getElementById('chat-abort-btn');
|
||||
const sendBtn = document.getElementById('chat-send-btn');
|
||||
if (abortBtn) abortBtn.style.display = inStreamingSession ? '' : 'none';
|
||||
if (sendBtn) sendBtn.textContent = inStreamingSession ? 'Queue ↵' : 'Send ↵';
|
||||
if (sendBtn) sendBtn.title = inStreamingSession ? 'Queue message' : 'Send message';
|
||||
// Update status badge too
|
||||
if (!inStreamingSession && isStreaming) {
|
||||
setStatus('connected'); // other session streaming, but we're not in it
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
/**
|
||||
* Code Studio View — VS Code-lite in-dashboard IDE.
|
||||
* Write code, run in sandbox, view output, and send snippets to chat.
|
||||
* API: GET /api/code/languages, POST /api/code/execute
|
||||
*/
|
||||
import { helpBanner } from '/plugins/views/_helpers.js';
|
||||
|
||||
const MONACO_CDN = 'https://cdn.jsdelivr.net/npm/[email protected]/min/vs';
|
||||
const HISTORY_MAX = 10;
|
||||
const SNIPPET_KEY = 'aiui_snippets';
|
||||
const OUTPUT_MAX = 200000;
|
||||
|
||||
const DEFAULT_CODE = {
|
||||
python: "print('hello from Code Studio')\n",
|
||||
javascript: "console.log('hello from Code Studio');\n",
|
||||
bash: "echo 'hello from Code Studio'\n",
|
||||
};
|
||||
|
||||
const MONACO_LANG = { python: 'python', javascript: 'javascript', bash: 'shell' };
|
||||
const FILE_EXT = { python: 'py', javascript: 'js', bash: 'sh' };
|
||||
|
||||
let editor = null;
|
||||
let monacoNs = null;
|
||||
let running = false;
|
||||
let timerId = null;
|
||||
const history = [];
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s == null ? '' : s)
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function currentCode() {
|
||||
if (editor && typeof editor.getValue === 'function') return editor.getValue();
|
||||
const ta = document.getElementById('cs-fallback');
|
||||
return ta ? ta.value : '';
|
||||
}
|
||||
|
||||
function setCode(code) {
|
||||
if (editor && typeof editor.setValue === 'function') editor.setValue(code);
|
||||
else { const ta = document.getElementById('cs-fallback'); if (ta) ta.value = code; }
|
||||
}
|
||||
|
||||
function setEditorLanguage(lang) {
|
||||
if (monacoNs && editor) {
|
||||
const model = editor.getModel();
|
||||
if (model) monacoNs.editor.setModelLanguage(model, MONACO_LANG[lang] || 'plaintext');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMonaco(host, lang) {
|
||||
if (window.monaco) monacoNs = window.monaco;
|
||||
if (!monacoNs) {
|
||||
await new Promise((resolve, reject) => {
|
||||
const s = document.createElement('script');
|
||||
s.src = MONACO_CDN + '/loader.js';
|
||||
s.onload = resolve;
|
||||
s.onerror = reject;
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
if (!window.require) throw new Error('Monaco loader unavailable');
|
||||
window.require.config({ paths: { vs: MONACO_CDN } });
|
||||
monacoNs = await new Promise((resolve, reject) => {
|
||||
try { window.require(['vs/editor/editor.main'], () => resolve(window.monaco)); }
|
||||
catch (e) { reject(e); }
|
||||
});
|
||||
}
|
||||
editor = monacoNs.editor.create(host, {
|
||||
value: DEFAULT_CODE[lang] || '',
|
||||
language: MONACO_LANG[lang] || 'plaintext',
|
||||
theme: 'vs-dark',
|
||||
automaticLayout: true,
|
||||
fontSize: 13,
|
||||
fontFamily: "'JetBrains Mono','SF Mono',Monaco,Consolas,monospace",
|
||||
lineNumbers: 'on',
|
||||
minimap: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
});
|
||||
}
|
||||
|
||||
function mountFallback(host, lang) {
|
||||
host.innerHTML = `<textarea id="cs-fallback" spellcheck="false"
|
||||
style="width:100%;height:100%;box-sizing:border-box;resize:none;border:0;outline:none;padding:12px;background:#1e1e1e;color:#d4d4d4;font-family:'JetBrains Mono','SF Mono',Monaco,Consolas,monospace;font-size:13px;line-height:1.5">${escapeHtml(DEFAULT_CODE[lang] || '')}</textarea>`;
|
||||
}
|
||||
|
||||
function pickTab(container, tab) {
|
||||
container.querySelectorAll('.cs-tab').forEach((b) => {
|
||||
const on = b.dataset.tab === tab;
|
||||
b.style.color = on ? 'var(--db-text,#e4e4e7)' : 'var(--db-text-dim,#a1a1aa)';
|
||||
b.style.borderBottom = on ? '2px solid var(--db-accent,#6366f1)' : '2px solid transparent';
|
||||
});
|
||||
container.querySelectorAll('.cs-tab-panel').forEach((p) => {
|
||||
p.style.display = p.dataset.panel === tab ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function truncate(text) {
|
||||
const s = String(text == null ? '' : text);
|
||||
if (s.length <= OUTPUT_MAX) return { text: s, truncated: false };
|
||||
return { text: s.slice(0, OUTPUT_MAX), truncated: true };
|
||||
}
|
||||
|
||||
function renderOutput(container, result) {
|
||||
const out = result.output != null ? result.output : (result.stdout || '');
|
||||
const err = result.error != null ? result.error : (result.stderr || '');
|
||||
const artifacts = Array.isArray(result.artifacts) ? result.artifacts : [];
|
||||
const failed = ['error', 'failed', 'degraded'].includes(result.status);
|
||||
|
||||
const banner = container.querySelector('#cs-banner');
|
||||
if (failed) {
|
||||
banner.style.display = 'flex';
|
||||
const code = result.exit_code != null ? ` (exit ${result.exit_code})` : '';
|
||||
banner.querySelector('#cs-banner-text').textContent = `Execution ${result.status}${code}`;
|
||||
} else {
|
||||
banner.style.display = 'none';
|
||||
}
|
||||
|
||||
const o = truncate(out);
|
||||
const e = truncate(err);
|
||||
container.querySelector('#cs-out-body').innerHTML =
|
||||
`<pre style="margin:0;white-space:pre-wrap;word-break:break-word;color:#22c55e">${escapeHtml(o.text) || '<span style="color:#71717a">(no output)</span>'}</pre>` +
|
||||
(o.truncated ? '<div style="color:#71717a;font-size:11px;margin-top:6px">Output truncated</div>' : '');
|
||||
container.querySelector('#cs-err-body').innerHTML =
|
||||
`<pre style="margin:0;white-space:pre-wrap;word-break:break-word;color:#ef4444">${escapeHtml(e.text) || '<span style="color:#71717a">(no errors)</span>'}</pre>` +
|
||||
(e.truncated ? '<div style="color:#71717a;font-size:11px;margin-top:6px">Output truncated</div>' : '');
|
||||
container.querySelector('#cs-art-body').innerHTML = artifacts.length
|
||||
? `<pre style="margin:0;white-space:pre-wrap">${escapeHtml(JSON.stringify(artifacts, null, 2))}</pre>`
|
||||
: '<span style="color:#71717a">(no artifacts)</span>';
|
||||
|
||||
pickTab(container, failed && err ? 'errors' : 'output');
|
||||
}
|
||||
|
||||
function renderHistory(container) {
|
||||
const list = container.querySelector('#cs-history');
|
||||
if (!history.length) {
|
||||
list.innerHTML = '<div style="color:#71717a;font-size:12px;padding:8px">No runs yet</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = history.map((h, i) => {
|
||||
const ok = !['error', 'failed', 'degraded'].includes(h.result.status);
|
||||
const icon = ok ? '<span style="color:#22c55e">●</span>' : '<span style="color:#ef4444">●</span>';
|
||||
const dur = h.duration != null ? `${(h.duration / 1000).toFixed(1)}s` : '';
|
||||
return `<div class="cs-history-row" data-idx="${i}" style="display:flex;align-items:center;gap:8px;padding:8px;border-radius:6px;cursor:pointer;font-size:12px" onmouseover="this.style.background='rgba(255,255,255,.05)'" onmouseout="this.style.background='transparent'">
|
||||
${icon}<span style="flex:1">${escapeHtml(h.language)}</span><span style="color:#71717a">${dur}</span></div>`;
|
||||
}).join('');
|
||||
list.querySelectorAll('.cs-history-row').forEach((row) => {
|
||||
row.addEventListener('click', () => {
|
||||
const h = history[Number(row.dataset.idx)];
|
||||
if (!h) return;
|
||||
const sel = container.querySelector('#cs-language');
|
||||
if (sel) { sel.value = h.language; setEditorLanguage(h.language); }
|
||||
setCode(h.code);
|
||||
renderOutput(container, h.result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function pushHistory(container, entry) {
|
||||
history.unshift(entry);
|
||||
if (history.length > HISTORY_MAX) history.length = HISTORY_MAX;
|
||||
renderHistory(container);
|
||||
}
|
||||
|
||||
async function runCode(container) {
|
||||
if (running) return;
|
||||
const code = currentCode();
|
||||
if (!code.trim()) {
|
||||
alert('Enter code to run');
|
||||
return;
|
||||
}
|
||||
const lang = container.querySelector('#cs-language').value;
|
||||
const runBtn = container.querySelector('#cs-run');
|
||||
running = true;
|
||||
runBtn.disabled = true;
|
||||
runBtn.textContent = 'Running…';
|
||||
const start = Date.now();
|
||||
timerId = setInterval(() => {
|
||||
runBtn.textContent = `Executing… ${((Date.now() - start) / 1000).toFixed(1)}s`;
|
||||
}, 100);
|
||||
|
||||
let result;
|
||||
try {
|
||||
const res = await fetch('/api/code/execute', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ language: lang, code, timeout: 30 }),
|
||||
});
|
||||
result = await res.json();
|
||||
} catch (e) {
|
||||
result = { status: 'failed', error: String(e), language: lang };
|
||||
} finally {
|
||||
running = false;
|
||||
clearInterval(timerId);
|
||||
timerId = null;
|
||||
runBtn.disabled = false;
|
||||
runBtn.textContent = '▶ Run';
|
||||
}
|
||||
|
||||
const duration = result.duration_ms != null ? result.duration_ms : (Date.now() - start);
|
||||
renderOutput(container, result);
|
||||
pushHistory(container, { language: lang, code, result, duration });
|
||||
}
|
||||
|
||||
function saveSnippet(container) {
|
||||
const code = currentCode();
|
||||
const lang = container.querySelector('#cs-language').value;
|
||||
try {
|
||||
const raw = localStorage.getItem(SNIPPET_KEY);
|
||||
const snippets = raw ? JSON.parse(raw) : [];
|
||||
snippets.push({ language: lang, code, ts: Date.now() });
|
||||
localStorage.setItem(SNIPPET_KEY, JSON.stringify(snippets));
|
||||
const btn = container.querySelector('#cs-save');
|
||||
btn.textContent = 'Saved!';
|
||||
setTimeout(() => { btn.textContent = 'Save'; }, 1500);
|
||||
} catch (e) {
|
||||
alert('Could not save snippet (storage full)');
|
||||
}
|
||||
}
|
||||
|
||||
function sendToAgent(container) {
|
||||
const code = currentCode();
|
||||
const lang = container.querySelector('#cs-language').value;
|
||||
const text = '```' + lang + '\n' + code + '\n```\nPlease review this code.';
|
||||
if (window.aiui && typeof window.aiui.selectPage === 'function') {
|
||||
Promise.resolve(window.aiui.selectPage('chat')).then(() => {
|
||||
window.dispatchEvent(new CustomEvent('aiui:prefill-composer', { detail: { text } }));
|
||||
});
|
||||
} else {
|
||||
window.dispatchEvent(new CustomEvent('aiui:prefill-composer', { detail: { text } }));
|
||||
}
|
||||
}
|
||||
|
||||
export async function render(container) {
|
||||
container.setAttribute('data-page', 'code-studio');
|
||||
|
||||
let languages = [];
|
||||
try {
|
||||
const r = await fetch('/api/code/languages');
|
||||
const d = await r.json();
|
||||
languages = d.languages || [];
|
||||
} catch (e) {}
|
||||
if (!languages.length) {
|
||||
languages = [{ id: 'python', name: 'Python' }, { id: 'javascript', name: 'JavaScript' }, { id: 'bash', name: 'Bash' }];
|
||||
}
|
||||
const firstLang = languages[0].id;
|
||||
|
||||
const langOptions = languages
|
||||
.map((l) => `<option value="${escapeHtml(l.id)}">${escapeHtml(l.name || l.id)}</option>`)
|
||||
.join('');
|
||||
|
||||
container.innerHTML = helpBanner({
|
||||
title: 'Code Studio',
|
||||
what: 'Write code, run it in a sandbox, inspect output, and hand snippets to your agent.',
|
||||
howToUse: 'Pick a language, edit the code, then click Run. Use "Send to Agent" to continue in chat.',
|
||||
tip: 'Code runs server-side in a sandbox — never in your browser.',
|
||||
collapsed: true,
|
||||
}) + `
|
||||
<div class="cs-root" style="display:flex;gap:12px;height:calc(100vh - 220px);min-height:480px">
|
||||
<div style="flex:1;display:flex;flex-direction:column;border:1px solid var(--db-border);border-radius:10px;overflow:hidden;background:#1e1e1e">
|
||||
<div style="display:flex;align-items:center;gap:8px;padding:8px;background:#18181b;border-bottom:1px solid var(--db-border);flex-wrap:wrap">
|
||||
<select id="cs-language" style="padding:6px 10px;background:var(--db-card-bg);color:var(--db-text);border:1px solid var(--db-border);border-radius:6px;font-size:13px">${langOptions}</select>
|
||||
<input id="cs-filename" type="text" value="script.${FILE_EXT[firstLang] || 'txt'}" style="padding:6px 10px;background:var(--db-card-bg);color:var(--db-text);border:1px solid var(--db-border);border-radius:6px;font-size:13px;width:140px" />
|
||||
<button id="cs-run" style="padding:6px 16px;background:#22c55e;color:#fff;border:0;border-radius:6px;cursor:pointer;font-size:13px;font-weight:600">▶ Run</button>
|
||||
<button id="cs-save" style="padding:6px 14px;background:transparent;color:var(--db-text);border:1px solid var(--db-border);border-radius:6px;cursor:pointer;font-size:13px">Save</button>
|
||||
</div>
|
||||
<div id="cs-editor" style="flex:3;min-height:180px"></div>
|
||||
<div style="flex:2;display:flex;flex-direction:column;border-top:1px solid var(--db-border);background:#0d1117;min-height:120px">
|
||||
<div id="cs-banner" style="display:none;align-items:center;gap:10px;padding:8px 12px;background:rgba(239,68,68,.12);border-bottom:1px solid rgba(239,68,68,.3);font-size:12px">
|
||||
<span id="cs-banner-text" style="color:#ef4444;flex:1"></span>
|
||||
<button id="cs-fix" style="padding:4px 12px;background:transparent;color:#ef4444;border:1px solid rgba(239,68,68,.4);border-radius:6px;cursor:pointer;font-size:12px">Ask agent to fix</button>
|
||||
</div>
|
||||
<div style="display:flex;gap:4px;padding:0 8px;border-bottom:1px solid var(--db-border)">
|
||||
<button class="cs-tab" data-tab="output" style="padding:8px 12px;background:transparent;border:0;border-bottom:2px solid transparent;color:var(--db-text-dim,#a1a1aa);cursor:pointer;font-size:12px">Output</button>
|
||||
<button class="cs-tab" data-tab="errors" style="padding:8px 12px;background:transparent;border:0;border-bottom:2px solid transparent;color:var(--db-text-dim,#a1a1aa);cursor:pointer;font-size:12px">Errors</button>
|
||||
<button class="cs-tab" data-tab="artifacts" style="padding:8px 12px;background:transparent;border:0;border-bottom:2px solid transparent;color:var(--db-text-dim,#a1a1aa);cursor:pointer;font-size:12px">Artifacts</button>
|
||||
</div>
|
||||
<div style="flex:1;overflow:auto;padding:12px;font-family:'JetBrains Mono','SF Mono',Monaco,Consolas,monospace;font-size:12px">
|
||||
<div class="cs-tab-panel" data-panel="output" id="cs-out-body"><span style="color:#71717a">Run code to see output</span></div>
|
||||
<div class="cs-tab-panel" data-panel="errors" id="cs-err-body" style="display:none"></div>
|
||||
<div class="cs-tab-panel" data-panel="artifacts" id="cs-art-body" style="display:none"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width:240px;display:flex;flex-direction:column;gap:12px">
|
||||
<div style="border:1px solid var(--db-border);border-radius:10px;padding:12px;background:var(--db-card-bg)">
|
||||
<div style="font-size:12px;font-weight:600;margin-bottom:8px">History</div>
|
||||
<div id="cs-history"></div>
|
||||
</div>
|
||||
<div style="border:1px solid var(--db-border);border-radius:10px;padding:12px;background:var(--db-card-bg);font-size:12px;color:var(--db-text-dim)">
|
||||
<div style="font-weight:600;color:var(--db-text);margin-bottom:6px">Environment</div>
|
||||
<div>Timeout: 30s</div>
|
||||
<div>Sandbox: server-side</div>
|
||||
</div>
|
||||
<button id="cs-send-agent" style="padding:10px;background:var(--db-accent,#6366f1);color:#fff;border:0;border-radius:8px;cursor:pointer;font-size:13px;font-weight:600">→ Send to Agent</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const editorHost = container.querySelector('#cs-editor');
|
||||
try {
|
||||
await loadMonaco(editorHost, firstLang);
|
||||
} catch (e) {
|
||||
console.warn('[code-studio] Monaco load failed, using fallback textarea:', e);
|
||||
mountFallback(editorHost, firstLang);
|
||||
}
|
||||
|
||||
renderHistory(container);
|
||||
pickTab(container, 'output');
|
||||
|
||||
const langSel = container.querySelector('#cs-language');
|
||||
langSel.addEventListener('change', () => {
|
||||
const lang = langSel.value;
|
||||
setEditorLanguage(lang);
|
||||
const fn = container.querySelector('#cs-filename');
|
||||
if (fn) fn.value = `script.${FILE_EXT[lang] || 'txt'}`;
|
||||
});
|
||||
|
||||
container.querySelector('#cs-run').addEventListener('click', () => runCode(container));
|
||||
container.querySelector('#cs-save').addEventListener('click', () => saveSnippet(container));
|
||||
container.querySelector('#cs-send-agent').addEventListener('click', () => sendToAgent(container));
|
||||
container.querySelector('#cs-fix').addEventListener('click', () => sendToAgent(container));
|
||||
|
||||
container.querySelectorAll('.cs-tab').forEach((b) => {
|
||||
b.addEventListener('click', () => pickTab(container, b.dataset.tab));
|
||||
});
|
||||
}
|
||||
|
||||
export function cleanup() {
|
||||
if (timerId) { clearInterval(timerId); timerId = null; }
|
||||
if (editor && typeof editor.dispose === 'function') { try { editor.dispose(); } catch (e) {} }
|
||||
editor = null;
|
||||
running = false;
|
||||
}
|
||||
@@ -1,102 +1,413 @@
|
||||
/**
|
||||
* Eval View — Agent evaluation and accuracy monitoring.
|
||||
* Shows evaluation scores, judge results, quality metrics.
|
||||
* API: /api/eval, /api/eval/status, /api/eval/scores, /api/eval/judges
|
||||
* Eval Quality Cockpit (STITCH-009) — regression dashboard for agent quality.
|
||||
*
|
||||
* Upgrades the read-only Evaluation table into a quality cockpit that answers:
|
||||
* are agents getting better/worse, which suite regressed, what failed, and can
|
||||
* I trigger a run — all client-derived from existing endpoints (no new backend).
|
||||
*
|
||||
* Data sources (parallel, fault-tolerant via Promise.allSettled):
|
||||
* GET /api/eval/status /api/eval/scores /api/eval?limit=200 /api/eval/judges
|
||||
* POST /api/eval/run
|
||||
*
|
||||
* Failed rows deep-link to Run Flight Recorder (/runs?session=) when a
|
||||
* session_id is present, otherwise fall back to /traces (STITCH-007 soft dep).
|
||||
*/
|
||||
import { showToast } from '../toast.js';
|
||||
import {
|
||||
esc, timeAgo, sparklineSVG, bucketByDay, detectRegression, trendArrow, regressionBadge,
|
||||
} from '/plugins/views/_helpers.js';
|
||||
|
||||
const HISTORY_LIMIT = 200;
|
||||
const BASELINE_ID_KEY = 'aiui_eval_baseline_id';
|
||||
const BASELINE_SNAP_KEY = 'aiui_eval_baseline_snapshot';
|
||||
|
||||
function navigate(pageId) {
|
||||
if (window.aiui?.selectPage) window.aiui.selectPage(pageId);
|
||||
}
|
||||
|
||||
function loadBaseline() {
|
||||
try {
|
||||
const raw = localStorage.getItem(BASELINE_SNAP_KEY);
|
||||
return raw ? JSON.parse(raw) : null;
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
function saveBaseline(snapshot) {
|
||||
try {
|
||||
localStorage.setItem(BASELINE_ID_KEY, snapshot.id);
|
||||
localStorage.setItem(BASELINE_SNAP_KEY, JSON.stringify(snapshot));
|
||||
} catch (e) { /* storage disabled */ }
|
||||
}
|
||||
|
||||
function clearBaseline() {
|
||||
try {
|
||||
localStorage.removeItem(BASELINE_ID_KEY);
|
||||
localStorage.removeItem(BASELINE_SNAP_KEY);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function passRateFor(s) {
|
||||
const denom = (s.passed || 0) + (s.failed || 0);
|
||||
return denom > 0 ? (s.passed || 0) / denom : null;
|
||||
}
|
||||
|
||||
function statusDot(rate) {
|
||||
if (rate == null) return '<span style="color:#71717a">●</span>';
|
||||
if (rate >= 0.9) return '<span style="color:#22c55e">●</span>';
|
||||
if (rate >= 0.7) return '<span style="color:#f59e0b">●</span>';
|
||||
return '<span style="color:#ef4444">●</span>';
|
||||
}
|
||||
|
||||
function statusLabel(rate) {
|
||||
if (rate == null) return 'No data';
|
||||
if (rate >= 0.9) return 'Healthy';
|
||||
if (rate >= 0.7) return 'Watch';
|
||||
return 'Regressed';
|
||||
}
|
||||
|
||||
function heroColor(rate) {
|
||||
if (rate == null) return undefined;
|
||||
if (rate >= 0.9) return '#22c55e';
|
||||
if (rate >= 0.7) return '#f59e0b';
|
||||
return '#ef4444';
|
||||
}
|
||||
|
||||
export async function render(container) {
|
||||
container.setAttribute('data-page', 'eval');
|
||||
container.innerHTML = '<div class="db-loading"><div class="db-spinner"></div></div>';
|
||||
|
||||
let statusData = {}, scores = [], evaluations = [], judges = [];
|
||||
let partialError = false;
|
||||
try {
|
||||
const [sRes, scRes, eRes, jRes] = await Promise.all([
|
||||
fetch('/api/eval/status'), fetch('/api/eval/scores'), fetch('/api/eval?limit=20'), fetch('/api/eval/judges')
|
||||
const [sRes, scRes, eRes, jRes] = await Promise.allSettled([
|
||||
fetch('/api/eval/status'),
|
||||
fetch('/api/eval/scores'),
|
||||
fetch(`/api/eval?limit=${HISTORY_LIMIT}`),
|
||||
fetch('/api/eval/judges'),
|
||||
]);
|
||||
statusData = await sRes.json();
|
||||
scores = (await scRes.json()).scores || [];
|
||||
evaluations = (await eRes.json()).evaluations || [];
|
||||
judges = (await jRes.json()).judges || [];
|
||||
} catch(e) { statusData = { status: 'error' }; }
|
||||
if (sRes.status === 'fulfilled') statusData = await sRes.value.json(); else partialError = true;
|
||||
if (scRes.status === 'fulfilled') scores = (await scRes.value.json()).scores || []; else partialError = true;
|
||||
if (eRes.status === 'fulfilled') evaluations = (await eRes.value.json()).evaluations || []; else partialError = true;
|
||||
if (jRes.status === 'fulfilled') judges = (await jRes.value.json()).judges || []; else partialError = true;
|
||||
} catch (e) {
|
||||
statusData = { status: 'error' };
|
||||
partialError = true;
|
||||
}
|
||||
|
||||
const gwBadge = statusData.gateway_connected
|
||||
const sdkAvailable = statusData.sdk_available === true;
|
||||
const gatewayConnected = statusData.gateway_connected === true;
|
||||
const baseline = loadBaseline();
|
||||
|
||||
// ── Hero aggregates ────────────────────────────────────────────
|
||||
let totalPassed = 0, totalFailed = 0, totalCases = 0, weightedScore = 0, scoredCases = 0;
|
||||
for (const s of scores) {
|
||||
totalPassed += s.passed || 0;
|
||||
totalFailed += s.failed || 0;
|
||||
totalCases += s.total || 0;
|
||||
if (s.avg_score != null && s.scored) {
|
||||
weightedScore += s.avg_score * s.scored;
|
||||
scoredCases += s.scored;
|
||||
}
|
||||
}
|
||||
const passDenom = totalPassed + totalFailed;
|
||||
const passRate = passDenom > 0 ? (totalPassed / passDenom) : null;
|
||||
const avgScore = scoredCases > 0 ? (weightedScore / scoredCases) : null;
|
||||
|
||||
// ── Regression detection (per agent vs baseline snapshot) ──────
|
||||
let regressionCount = 0;
|
||||
const agentDelta = {};
|
||||
if (baseline?.agents) {
|
||||
for (const s of scores) {
|
||||
const base = baseline.agents[s.agent_id];
|
||||
if (base != null && s.avg_score != null) {
|
||||
const r = detectRegression(s.avg_score, base);
|
||||
agentDelta[s.agent_id] = r.delta;
|
||||
if (r.isRegression) regressionCount += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const lastEval = evaluations.length ? evaluations[evaluations.length - 1] : null;
|
||||
const lastRun = lastEval ? timeAgo(lastEval.timestamp) : '—';
|
||||
|
||||
const heroCard = (title, value, subtitle, color) => `
|
||||
<div class="db-card" style="padding:16px;border-radius:12px">
|
||||
<div style="font-size:.8rem;opacity:.6">${esc(title)}</div>
|
||||
<div style="font-size:1.8rem;font-weight:700${color ? `;color:${color}` : ''}">${value}</div>
|
||||
<div style="font-size:.75rem;opacity:.5;margin-top:2px">${esc(subtitle)}</div>
|
||||
</div>`;
|
||||
|
||||
// ── Suite cards ────────────────────────────────────────────────
|
||||
const suiteCards = scores.map((s) => {
|
||||
const rate = passRateFor(s);
|
||||
const buckets = bucketByDay(evaluations, 7, s.agent_id);
|
||||
const series = buckets.map((b) => b.avg);
|
||||
const half = Math.max(1, Math.floor(series.length / 2));
|
||||
const recent = series.slice(-half).reduce((a, b) => a + b, 0) / half;
|
||||
const prior = series.slice(0, half).reduce((a, b) => a + b, 0) / half;
|
||||
const arrow = series.length >= 2 ? trendArrow(recent, prior) : '';
|
||||
const badge = agentDelta[s.agent_id] != null ? regressionBadge(agentDelta[s.agent_id]) : '';
|
||||
const isReg = agentDelta[s.agent_id] != null && agentDelta[s.agent_id] <= -0.1;
|
||||
const sparkColor = isReg ? '#ef4444' : 'var(--db-accent,#a855f7)';
|
||||
return `
|
||||
<div class="db-card db-suite-card" style="padding:14px;border-radius:12px">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px">
|
||||
<span style="font-weight:600;font-size:.95rem">${esc(s.agent_id)}</span>
|
||||
<span style="font-size:.75rem;opacity:.7">${statusDot(rate)} ${statusLabel(rate)}</span>
|
||||
</div>
|
||||
<div style="font-size:.8rem;opacity:.75;margin:6px 0">Pass ${rate != null ? Math.round(rate * 100) + '%' : '—'} · Avg ${s.avg_score != null ? s.avg_score.toFixed(2) : '—'} · ${s.total || 0} cases</div>
|
||||
<div aria-label="7-day score trend for ${esc(s.agent_id)}">${sparklineSVG(series, { width: 220, height: 34, color: sparkColor })}</div>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:8px">
|
||||
<span>${arrow || badge || '<span style="font-size:11px;opacity:.4">no trend</span>'}</span>
|
||||
<button class="ev-filter-btn" data-agent="${esc(s.agent_id)}" style="font-size:11px;padding:3px 10px;border:1px solid var(--db-border,#333);background:transparent;color:var(--db-text,#e0e0e0);border-radius:6px;cursor:pointer">Filter →</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
// ── Baseline compare panel ─────────────────────────────────────
|
||||
let compareHtml;
|
||||
if (!baseline) {
|
||||
compareHtml = '<div style="padding:20px;text-align:center;opacity:.5;font-size:.85rem">No baseline pinned. Use "Pin baseline" on a history row to compare.</div>';
|
||||
} else {
|
||||
const rows = scores.map((s) => {
|
||||
const base = baseline.agents ? baseline.agents[s.agent_id] : null;
|
||||
const cur = s.avg_score;
|
||||
let delta = '—';
|
||||
if (base != null && cur != null) {
|
||||
const d = cur - base;
|
||||
const col = d >= 0 ? '#22c55e' : '#ef4444';
|
||||
delta = `<span style="color:${col}">${d >= 0 ? '+' : ''}${d.toFixed(2)}</span>`;
|
||||
}
|
||||
return `<tr style="border-bottom:1px solid rgba(128,128,128,.1)">
|
||||
<td style="padding:8px 12px;font-size:.85rem">${esc(s.agent_id)}</td>
|
||||
<td style="padding:8px 12px;text-align:center;font-size:.85rem">${base != null ? base.toFixed(2) : '—'}</td>
|
||||
<td style="padding:8px 12px;text-align:center;font-size:.85rem">${cur != null ? cur.toFixed(2) : '—'}</td>
|
||||
<td style="padding:8px 12px;text-align:center;font-weight:600">${delta}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
compareHtml = `
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 12px;font-size:.75rem;opacity:.6">
|
||||
<span>Baseline: ${esc(baseline.id || '')} · ${timeAgo(baseline.timestamp)}</span>
|
||||
<button id="ev-clear-baseline" style="font-size:11px;padding:3px 10px;border:1px solid var(--db-border,#333);background:transparent;color:var(--db-text-dim,#888);border-radius:6px;cursor:pointer">Clear baseline</button>
|
||||
</div>
|
||||
<table style="width:100%;border-collapse:collapse">
|
||||
<thead><tr style="border-bottom:1px solid rgba(128,128,128,.2)">
|
||||
<th style="padding:8px 12px;text-align:left;font-size:.75rem;opacity:.6">Agent</th>
|
||||
<th style="padding:8px 12px;text-align:center;font-size:.75rem;opacity:.6">Baseline</th>
|
||||
<th style="padding:8px 12px;text-align:center;font-size:.75rem;opacity:.6">Latest</th>
|
||||
<th style="padding:8px 12px;text-align:center;font-size:.75rem;opacity:.6">Delta</th>
|
||||
</tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>`;
|
||||
}
|
||||
|
||||
// ── Run action bar ─────────────────────────────────────────────
|
||||
const agentOptions = scores.map((s) => `<option value="${esc(s.agent_id)}">${esc(s.agent_id)}</option>`).join('');
|
||||
const judgeOptions = judges.map((j) => `<option value="${esc(j.type || j.name)}">${esc(j.name)}${j.type ? ` (${esc(j.type)})` : ''}</option>`).join('');
|
||||
const runDisabled = !sdkAvailable;
|
||||
|
||||
// ── History table rows (newest first) ──────────────────────────
|
||||
const ordered = [...evaluations].reverse();
|
||||
const historyRows = ordered.map((e, i) => {
|
||||
const scoreColor = e.score == null ? 'inherit' : (e.score >= 0.8 ? '#22c55e' : e.score >= 0.5 ? '#f59e0b' : '#ef4444');
|
||||
const passCell = e.passed === true ? '✅' : e.passed === false ? '❌' : '—';
|
||||
const rowBorder = e.passed === false ? 'border-left:3px solid #ef4444;' : '';
|
||||
let debugCta = '';
|
||||
if (e.passed === false) {
|
||||
debugCta = e.session_id
|
||||
? `<button class="ev-debug-btn" data-session="${esc(e.session_id)}" style="font-size:11px;padding:3px 8px;border:1px solid var(--db-border,#333);background:transparent;color:var(--db-accent,#a855f7);border-radius:6px;cursor:pointer">Debug run →</button>`
|
||||
: '<span title="No session linked" style="font-size:11px;opacity:.4">Find traces</span>';
|
||||
}
|
||||
return `
|
||||
<tr class="ev-history-row" data-idx="${i}" style="border-bottom:1px solid rgba(128,128,128,.1);cursor:pointer;${rowBorder}">
|
||||
<td style="padding:8px 10px;width:28px;opacity:.5">▸</td>
|
||||
<td style="padding:8px 10px;font-size:.75rem;opacity:.7" title="${esc(new Date((e.timestamp || 0) * 1000).toLocaleString())}">${timeAgo(e.timestamp)}</td>
|
||||
<td style="padding:8px 10px;font-size:.85rem">${esc(e.agent_id)}</td>
|
||||
<td style="padding:8px 10px;font-size:.8rem;font-family:monospace">${esc(e.evaluator)}</td>
|
||||
<td style="padding:8px 10px;text-align:center;font-weight:600;color:${scoreColor}">${e.score != null ? e.score : '—'}</td>
|
||||
<td style="padding:8px 10px;text-align:center">${passCell}</td>
|
||||
<td style="padding:8px 10px;text-align:right">${debugCta}<button class="ev-pin-btn" data-idx="${i}" style="font-size:11px;padding:3px 8px;margin-left:6px;border:1px solid var(--db-border,#333);background:transparent;color:var(--db-text-dim,#888);border-radius:6px;cursor:pointer">Pin</button></td>
|
||||
</tr>
|
||||
<tr class="ev-detail-row" data-detail="${i}" style="display:none">
|
||||
<td colspan="7" style="padding:0 10px 12px">
|
||||
<div style="background:rgba(128,128,128,.06);border-radius:8px;padding:12px;font-size:.82rem;line-height:1.7">
|
||||
<div><strong>Input:</strong> ${esc((e.input || '').slice(0, 500))}${(e.input || '').length > 500 ? '…' : ''}</div>
|
||||
<div><strong>Expected:</strong> ${esc((e.expected || '').slice(0, 500))}${(e.expected || '').length > 500 ? '…' : ''}</div>
|
||||
<div><strong>Output:</strong> ${esc((e.output || '').slice(0, 500))}${(e.output || '').length > 500 ? '…' : ''}</div>
|
||||
${e.feedback ? `<div><strong>Feedback:</strong> ${esc(e.feedback)}</div>` : ''}
|
||||
</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
const gwBadge = gatewayConnected
|
||||
? '<span style="color:#22c55e">● Connected</span>'
|
||||
: '<span style="color:#ef4444">● Not Connected</span>';
|
||||
|
||||
container.innerHTML = `
|
||||
<div style="padding:24px;max-width:1200px;margin:0 auto">
|
||||
<h2 style="margin:0 0 20px;font-size:1.5rem">📊 Evaluation</h2>
|
||||
<h2 style="margin:0 0 16px;font-size:1.5rem">📊 Eval Quality Cockpit</h2>
|
||||
|
||||
${partialError ? '<div style="margin-bottom:16px;padding:10px 14px;border-radius:8px;background:rgba(245,158,11,.1);color:#f59e0b;font-size:.85rem">⚠ Some data failed to load — showing partial results.</div>' : ''}
|
||||
${!sdkAvailable ? '<div style="margin-bottom:16px;padding:10px 14px;border-radius:8px;background:rgba(239,68,68,.08);color:#ef4444;font-size:.85rem">Eval SDK not available — run trigger disabled. Install <code>praisonaiagents[eval]</code> to enable in-UI runs.</div>' : ''}
|
||||
|
||||
<div style="display:flex;flex-wrap:wrap;gap:16px;align-items:center;margin-bottom:20px;padding:10px 14px;border:1px solid var(--db-border,#333);border-radius:10px;font-size:.82rem">
|
||||
<span>Eval SDK ${sdkAvailable ? '✅' : '❌'}</span>
|
||||
<span>Gateway ${gwBadge}</span>
|
||||
<span>${judges.length} judge${judges.length === 1 ? '' : 's'}</span>
|
||||
<span style="opacity:.7">Last run ${esc(lastRun)}</span>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;margin-bottom:24px">
|
||||
<div class="db-card" style="padding:16px;border-radius:12px">
|
||||
<div style="font-size:.8rem;opacity:.6">Total Evaluations</div>
|
||||
<div style="font-size:1.8rem;font-weight:700">${statusData.total_evaluations || 0}</div>
|
||||
${heroCard('Pass Rate', passRate != null ? Math.round(passRate * 100) + '%' : '—', 'of scored cases', heroColor(passRate))}
|
||||
${heroCard('Avg Score', avgScore != null ? avgScore.toFixed(2) : '—', 'weighted by cases')}
|
||||
${heroCard('Regressions', String(regressionCount), 'vs baseline', regressionCount > 0 ? '#ef4444' : undefined)}
|
||||
${heroCard('Total Cases', String(totalCases), 'all time')}
|
||||
</div>
|
||||
|
||||
${scores.length === 0 ? `
|
||||
<div class="db-card" style="border-radius:12px;padding:40px;text-align:center">
|
||||
<div style="font-size:2rem;margin-bottom:8px">🧪</div>
|
||||
<div style="font-size:1.1rem;font-weight:600;margin-bottom:6px">No evaluations recorded yet</div>
|
||||
<div style="opacity:.6;font-size:.85rem;margin-bottom:16px">Run your first eval below or via the API to populate the cockpit.</div>
|
||||
</div>` : `
|
||||
<h3 style="margin:0 0 12px;font-size:1.1rem">Suite Health</h3>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:16px;margin-bottom:24px">
|
||||
${suiteCards}
|
||||
</div>`}
|
||||
|
||||
<div style="display:grid;grid-template-columns:minmax(0,3fr) minmax(0,2fr);gap:16px;margin-bottom:24px" class="ev-mid-grid">
|
||||
<div>
|
||||
<h3 style="margin:0 0 12px;font-size:1.1rem">Baseline Compare</h3>
|
||||
<div class="db-card" style="border-radius:12px;overflow:hidden">${compareHtml}</div>
|
||||
</div>
|
||||
<div class="db-card" style="padding:16px;border-radius:12px">
|
||||
<div style="font-size:.8rem;opacity:.6">Active Judges</div>
|
||||
<div style="font-size:1.8rem;font-weight:700">${statusData.active_judges || 0}</div>
|
||||
</div>
|
||||
<div class="db-card" style="padding:16px;border-radius:12px">
|
||||
<div style="font-size:.8rem;opacity:.6">Eval SDK</div>
|
||||
<div style="font-size:1rem;margin-top:4px">${statusData.eval_available ? '✅ Available' : '❌ Not Installed'}</div>
|
||||
</div>
|
||||
<div class="db-card" style="padding:16px;border-radius:12px">
|
||||
<div style="font-size:.8rem;opacity:.6">Gateway</div>
|
||||
<div style="font-size:1rem;margin-top:4px">${gwBadge}</div>
|
||||
<div>
|
||||
<h3 style="margin:0 0 12px;font-size:1.1rem">Run Eval</h3>
|
||||
<div class="db-card" style="border-radius:12px;padding:14px">
|
||||
<select id="ev-run-agent" style="width:100%;padding:8px;margin-bottom:8px;background:var(--db-input-bg,#0d0d1a);color:var(--db-text,#e0e0e0);border:1px solid var(--db-border,#333);border-radius:6px">
|
||||
<option value="">Agent…</option>${agentOptions}
|
||||
</select>
|
||||
<select id="ev-run-judge" style="width:100%;padding:8px;margin-bottom:8px;background:var(--db-input-bg,#0d0d1a);color:var(--db-text,#e0e0e0);border:1px solid var(--db-border,#333);border-radius:6px">
|
||||
<option value="">Judge…</option>${judgeOptions}
|
||||
</select>
|
||||
<textarea id="ev-run-input" placeholder="Input prompt" rows="2" style="width:100%;padding:8px;margin-bottom:8px;box-sizing:border-box;background:var(--db-input-bg,#0d0d1a);color:var(--db-text,#e0e0e0);border:1px solid var(--db-border,#333);border-radius:6px"></textarea>
|
||||
<textarea id="ev-run-output" placeholder="Agent output" rows="2" style="width:100%;padding:8px;margin-bottom:8px;box-sizing:border-box;background:var(--db-input-bg,#0d0d1a);color:var(--db-text,#e0e0e0);border:1px solid var(--db-border,#333);border-radius:6px"></textarea>
|
||||
<textarea id="ev-run-expected" placeholder="Expected answer" rows="2" style="width:100%;padding:8px;margin-bottom:8px;box-sizing:border-box;background:var(--db-input-bg,#0d0d1a);color:var(--db-text,#e0e0e0);border:1px solid var(--db-border,#333);border-radius:6px"></textarea>
|
||||
<div id="ev-run-error" style="display:none;color:#ef4444;font-size:.8rem;margin-bottom:8px"></div>
|
||||
<button id="ev-run-submit" ${runDisabled ? 'disabled' : ''} style="width:100%;padding:9px;background:${runDisabled ? 'rgba(168,85,247,.3)' : 'var(--db-accent,#a855f7)'};color:#fff;border:none;border-radius:8px;cursor:${runDisabled ? 'not-allowed' : 'pointer'};font-weight:600">Run eval</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="margin:0 0 12px;font-size:1.1rem">Agent Scores</h3>
|
||||
<div class="db-card" style="border-radius:12px;overflow:hidden;margin-bottom:24px">
|
||||
${scores.length === 0
|
||||
? '<div style="padding:24px;text-align:center;opacity:.5">No evaluation scores yet. Run evaluations via API or gateway hooks.</div>'
|
||||
: `<table style="width:100%;border-collapse:collapse">
|
||||
<thead><tr style="border-bottom:1px solid rgba(128,128,128,.2)">
|
||||
<th style="padding:10px 14px;text-align:left;font-size:.8rem;opacity:.6">Agent</th>
|
||||
<th style="padding:10px 14px;text-align:center;font-size:.8rem;opacity:.6">Total</th>
|
||||
<th style="padding:10px 14px;text-align:center;font-size:.8rem;opacity:.6">Avg Score</th>
|
||||
<th style="padding:10px 14px;text-align:center;font-size:.8rem;opacity:.6">Passed</th>
|
||||
<th style="padding:10px 14px;text-align:center;font-size:.8rem;opacity:.6">Failed</th>
|
||||
</tr></thead>
|
||||
<tbody>${scores.map(s => `
|
||||
<tr style="border-bottom:1px solid rgba(128,128,128,.1)">
|
||||
<td style="padding:10px 14px;font-size:.85rem">${s.agent_id}</td>
|
||||
<td style="padding:10px 14px;text-align:center">${s.total}</td>
|
||||
<td style="padding:10px 14px;text-align:center;font-weight:600">${s.avg_score !== null ? s.avg_score.toFixed(2) : '—'}</td>
|
||||
<td style="padding:10px 14px;text-align:center;color:#22c55e">${s.passed}</td>
|
||||
<td style="padding:10px 14px;text-align:center;color:#ef4444">${s.failed}</td>
|
||||
</tr>`).join('')}
|
||||
</tbody></table>`
|
||||
}
|
||||
</div>
|
||||
|
||||
<h3 style="margin:0 0 12px;font-size:1.1rem">Recent Evaluations</h3>
|
||||
<div class="db-card" style="border-radius:12px;overflow:hidden;margin-bottom:24px">
|
||||
<h3 style="margin:0 0 12px;font-size:1.1rem">Run History${evaluations.length ? ` (${evaluations.length})` : ''}</h3>
|
||||
<div class="db-card" style="border-radius:12px;overflow:hidden;overflow-x:auto">
|
||||
${evaluations.length === 0
|
||||
? '<div style="padding:24px;text-align:center;opacity:.5">No evaluations recorded yet.</div>'
|
||||
: `<table style="width:100%;border-collapse:collapse">
|
||||
: `<table style="width:100%;border-collapse:collapse;min-width:640px">
|
||||
<thead><tr style="border-bottom:1px solid rgba(128,128,128,.2)">
|
||||
<th style="padding:10px 14px;text-align:left;font-size:.8rem;opacity:.6">Time</th>
|
||||
<th style="padding:10px 14px;text-align:left;font-size:.8rem;opacity:.6">Agent</th>
|
||||
<th style="padding:10px 14px;text-align:left;font-size:.8rem;opacity:.6">Evaluator</th>
|
||||
<th style="padding:10px 14px;text-align:center;font-size:.8rem;opacity:.6">Score</th>
|
||||
<th style="padding:10px 14px;text-align:center;font-size:.8rem;opacity:.6">Pass</th>
|
||||
<th style="padding:10px 14px;text-align:left;font-size:.8rem;opacity:.6">Feedback</th>
|
||||
<th style="padding:8px 10px"></th>
|
||||
<th style="padding:8px 10px;text-align:left;font-size:.75rem;opacity:.6">Time</th>
|
||||
<th style="padding:8px 10px;text-align:left;font-size:.75rem;opacity:.6">Agent</th>
|
||||
<th style="padding:8px 10px;text-align:left;font-size:.75rem;opacity:.6">Evaluator</th>
|
||||
<th style="padding:8px 10px;text-align:center;font-size:.75rem;opacity:.6">Score</th>
|
||||
<th style="padding:8px 10px;text-align:center;font-size:.75rem;opacity:.6">Pass</th>
|
||||
<th style="padding:8px 10px;text-align:right;font-size:.75rem;opacity:.6">Actions</th>
|
||||
</tr></thead>
|
||||
<tbody>${evaluations.map(e => `
|
||||
<tr style="border-bottom:1px solid rgba(128,128,128,.1)">
|
||||
<td style="padding:10px 14px;font-size:.75rem;opacity:.6">${new Date(e.timestamp*1000).toLocaleTimeString()}</td>
|
||||
<td style="padding:10px 14px;font-size:.85rem">${e.agent_id}</td>
|
||||
<td style="padding:10px 14px;font-size:.8rem;font-family:monospace">${e.evaluator}</td>
|
||||
<td style="padding:10px 14px;text-align:center;font-weight:600">${e.score !== null ? e.score : '—'}</td>
|
||||
<td style="padding:10px 14px;text-align:center">${e.passed === true ? '✅' : e.passed === false ? '❌' : '—'}</td>
|
||||
<td style="padding:10px 14px;font-size:.8rem;max-width:200px;overflow:hidden;text-overflow:ellipsis">${e.feedback || ''}</td>
|
||||
</tr>`).join('')}
|
||||
</tbody></table>`
|
||||
<tbody>${historyRows}</tbody>
|
||||
</table>`
|
||||
}
|
||||
</div>
|
||||
|
||||
${judges.length > 0 ? `
|
||||
<h3 style="margin:0 0 12px;font-size:1.1rem">Judges</h3>
|
||||
<div class="db-card" style="border-radius:12px;padding:16px">
|
||||
${judges.map(j => `<span style="display:inline-block;padding:4px 12px;margin:4px;border-radius:8px;font-size:.8rem;background:rgba(168,85,247,.1)">${j.name} (${j.type})</span>`).join('')}
|
||||
</div>` : ''}
|
||||
</div>`;
|
||||
|
||||
wire(container, ordered, scores);
|
||||
}
|
||||
|
||||
function wire(container, ordered, scores) {
|
||||
container.querySelectorAll('.ev-filter-btn').forEach((btn) => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const agent = btn.dataset.agent;
|
||||
const target = container.querySelector(`.ev-history-row td:nth-child(3)`);
|
||||
const first = [...container.querySelectorAll('.ev-history-row')].find(
|
||||
(r) => r.children[2]?.textContent?.trim() === agent);
|
||||
(first || target)?.scrollIntoView?.({ behavior: 'smooth', block: 'center' });
|
||||
});
|
||||
});
|
||||
|
||||
container.querySelectorAll('.ev-history-row').forEach((row) => {
|
||||
row.addEventListener('click', (e) => {
|
||||
if (e.target.closest('button')) return;
|
||||
const idx = row.dataset.idx;
|
||||
const detail = container.querySelector(`.ev-detail-row[data-detail="${idx}"]`);
|
||||
if (detail) {
|
||||
const open = detail.style.display !== 'none';
|
||||
detail.style.display = open ? 'none' : 'table-row';
|
||||
row.children[0].textContent = open ? '▸' : '▾';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
container.querySelectorAll('.ev-debug-btn').forEach((btn) => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const session = btn.dataset.session;
|
||||
const hasRuns = window.aiui?.selectPage
|
||||
&& document.querySelector('[data-page="runs"], [data-nav="runs"]');
|
||||
try {
|
||||
history.pushState({}, '', `/runs?session=${encodeURIComponent(session)}`);
|
||||
} catch (err) { /* ignore */ }
|
||||
navigate(hasRuns ? 'runs' : 'traces');
|
||||
});
|
||||
});
|
||||
|
||||
container.querySelectorAll('.ev-pin-btn').forEach((btn) => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const ev = ordered[Number(btn.dataset.idx)];
|
||||
if (!ev) return;
|
||||
const agents = {};
|
||||
for (const s of scores) if (s.avg_score != null) agents[s.agent_id] = s.avg_score;
|
||||
saveBaseline({ id: ev.id, timestamp: ev.timestamp, agents });
|
||||
showToast(`Baseline set to ${ev.id}`, 'success');
|
||||
render(container);
|
||||
});
|
||||
});
|
||||
|
||||
container.querySelector('#ev-clear-baseline')?.addEventListener('click', () => {
|
||||
clearBaseline();
|
||||
showToast('Baseline cleared', 'info');
|
||||
render(container);
|
||||
});
|
||||
|
||||
const submit = container.querySelector('#ev-run-submit');
|
||||
submit?.addEventListener('click', async () => {
|
||||
if (submit.disabled) return;
|
||||
const errBox = container.querySelector('#ev-run-error');
|
||||
const agent = container.querySelector('#ev-run-agent')?.value || '';
|
||||
const evaluator = container.querySelector('#ev-run-judge')?.value || '';
|
||||
const input = container.querySelector('#ev-run-input')?.value?.trim() || '';
|
||||
const output = container.querySelector('#ev-run-output')?.value?.trim() || '';
|
||||
const expected = container.querySelector('#ev-run-expected')?.value?.trim() || '';
|
||||
if (!input) {
|
||||
if (errBox) { errBox.textContent = 'Input is required.'; errBox.style.display = 'block'; }
|
||||
return;
|
||||
}
|
||||
if (errBox) errBox.style.display = 'none';
|
||||
submit.disabled = true;
|
||||
submit.textContent = 'Running…';
|
||||
try {
|
||||
const res = await fetch('/api/eval/run', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ agent_id: agent || 'ui', evaluator, input, output, expected }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
showToast('Evaluation complete', 'success');
|
||||
render(container);
|
||||
} catch (err) {
|
||||
submit.disabled = false;
|
||||
submit.textContent = 'Run eval';
|
||||
if (errBox) { errBox.textContent = 'Run failed: ' + err.message; errBox.style.display = 'block'; }
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,498 @@
|
||||
/**
|
||||
* Channel Inbox (/inbox) — unified multi-channel conversation triage (STITCH-010).
|
||||
*
|
||||
* Track B (session-filter fallback): derives threads from channel-backed
|
||||
* sessions + chat history, groups messages by sender, and persists status +
|
||||
* assignee via /api/sessions/{id}/state. Replies use the same chat transport
|
||||
* as chat.js (/api/chat/send). Deep-links to /chat and /work.
|
||||
*
|
||||
* Three panes: Queue (list + filters) · Conversation (history + composer) ·
|
||||
* Context rail (customer meta + assign agent + status + actions).
|
||||
*/
|
||||
import { showToast } from '../toast.js';
|
||||
|
||||
const STYLE_ID = 'channel-inbox-styles';
|
||||
|
||||
const PLATFORM_ICONS = {
|
||||
discord: '🎮', slack: '💬', telegram: '✈️', whatsapp: '📱',
|
||||
imessage: '🍎', signal: '🔒', googlechat: '💼', nostr: '🟣',
|
||||
};
|
||||
|
||||
const STATUS_META = {
|
||||
open: { label: 'Open', color: '#6366f1', priority: 2 },
|
||||
waiting_on_agent: { label: 'Waiting on agent', color: '#f59e0b', priority: 1 },
|
||||
waiting_on_customer: { label: 'Waiting on customer', color: '#a1a1aa', priority: 3 },
|
||||
resolved: { label: 'Resolved', color: '#22c55e', priority: 4 },
|
||||
};
|
||||
|
||||
const STATUS_ORDER = ['open', 'waiting_on_agent', 'waiting_on_customer', 'resolved'];
|
||||
|
||||
function esc(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s == null ? '' : String(s);
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function platformIcon(p) {
|
||||
return PLATFORM_ICONS[String(p || '').toLowerCase()] || '📡';
|
||||
}
|
||||
|
||||
function relativeTime(ts) {
|
||||
if (!ts) return '';
|
||||
const t = typeof ts === 'number' ? ts * (ts < 1e12 ? 1000 : 1) : Date.parse(ts);
|
||||
if (!t || isNaN(t)) return '';
|
||||
const diff = Math.floor((Date.now() - t) / 1000);
|
||||
if (diff < 60) return 'now';
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)}m`;
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)}h`;
|
||||
return `${Math.floor(diff / 86400)}d`;
|
||||
}
|
||||
|
||||
function statusMeta(status) {
|
||||
return STATUS_META[status] || STATUS_META.open;
|
||||
}
|
||||
|
||||
// ── Pure thread-building helpers (unit-tested via source assertions) ──
|
||||
|
||||
export function senderKey(channelId, sender) {
|
||||
const s = (sender == null || sender === '') ? 'general' : String(sender);
|
||||
return `${channelId || 'unknown'}:${s}`;
|
||||
}
|
||||
|
||||
export function groupMessagesBySender(sessionId, channelId, messages) {
|
||||
const groups = new Map();
|
||||
(messages || []).forEach((m) => {
|
||||
const sender = m.sender || m.sender_id || m.user || '';
|
||||
const key = senderKey(channelId, sender);
|
||||
if (!groups.has(key)) {
|
||||
groups.set(key, { key, sessionId, channelId, sender, messages: [] });
|
||||
}
|
||||
groups.get(key).messages.push(m);
|
||||
});
|
||||
return Array.from(groups.values());
|
||||
}
|
||||
|
||||
export function buildThreadRow(group, platform, stateThreads) {
|
||||
const msgs = group.messages || [];
|
||||
const last = msgs[msgs.length - 1] || {};
|
||||
const persisted = (stateThreads && stateThreads[group.key]) || {};
|
||||
const preview = String(last.content || last.text || '').slice(0, 120);
|
||||
const senderDisplay = last.sender_display || group.sender || 'General';
|
||||
const updatedAt = last.timestamp || last.created_at || last.ts || 0;
|
||||
return {
|
||||
threadId: group.key,
|
||||
sessionId: group.sessionId,
|
||||
channelId: group.channelId,
|
||||
platform: platform || last.platform || '',
|
||||
sender: group.sender || '',
|
||||
senderDisplay,
|
||||
preview,
|
||||
status: persisted.status || 'open',
|
||||
assignedAgent: persisted.assigned_agent || '',
|
||||
updatedAt,
|
||||
messageCount: msgs.length,
|
||||
messages: msgs,
|
||||
};
|
||||
}
|
||||
|
||||
export function sortThreads(rows) {
|
||||
return rows.slice().sort((a, b) => {
|
||||
const pa = statusMeta(a.status).priority;
|
||||
const pb = statusMeta(b.status).priority;
|
||||
if (pa !== pb) return pa - pb;
|
||||
return (b.updatedAt || 0) - (a.updatedAt || 0);
|
||||
});
|
||||
}
|
||||
|
||||
function injectStyles() {
|
||||
if (document.getElementById(STYLE_ID)) return;
|
||||
const style = document.createElement('style');
|
||||
style.id = STYLE_ID;
|
||||
style.textContent = `
|
||||
.inbox { display:flex; height:100%; min-height:0; gap:1px; background:var(--db-border,#3f3f46); }
|
||||
.inbox-pane { background:var(--db-bg,#18181b); overflow:auto; min-height:0; display:flex; flex-direction:column; }
|
||||
.inbox-queue { width:300px; min-width:240px; flex:0 0 auto; }
|
||||
.inbox-conv { flex:1 1 auto; min-width:0; }
|
||||
.inbox-rail { width:300px; min-width:260px; flex:0 0 auto; }
|
||||
.inbox-head { padding:12px 14px; border-bottom:1px solid var(--db-border,#3f3f46);
|
||||
font-size:14px; font-weight:600; position:sticky; top:0; background:var(--db-bg,#18181b); z-index:2; }
|
||||
.inbox-filters { padding:10px 12px; border-bottom:1px solid var(--db-border,#3f3f46);
|
||||
display:flex; flex-direction:column; gap:8px; }
|
||||
.inbox-search { width:100%; box-sizing:border-box; padding:6px 10px; border-radius:8px;
|
||||
background:var(--db-card-bg,#27272a); border:1px solid var(--db-border,#3f3f46); color:var(--db-text,#e4e4e7); font-size:13px; }
|
||||
.inbox-chips { display:flex; flex-wrap:wrap; gap:6px; }
|
||||
.inbox-chip { padding:4px 10px; font-size:12px; border-radius:999px; cursor:pointer;
|
||||
background:var(--db-card-bg,#27272a); border:1px solid var(--db-border,#3f3f46); color:var(--db-text-dim,#a1a1aa); }
|
||||
.inbox-chip.active { color:#fff; background:var(--db-accent,#6366f1); border-color:var(--db-accent,#6366f1); }
|
||||
.inbox-list { flex:1 1 auto; overflow:auto; }
|
||||
.inbox-row { display:flex; gap:10px; padding:10px 12px; cursor:pointer; border-bottom:1px solid var(--db-border,#3f3f46); }
|
||||
.inbox-row:hover { background:var(--db-card-bg,#27272a); }
|
||||
.inbox-row.active { background:var(--db-card-bg,#27272a); border-left:3px solid var(--db-accent,#6366f1); }
|
||||
.inbox-row-icon { font-size:18px; flex:0 0 auto; }
|
||||
.inbox-row-body { flex:1 1 auto; min-width:0; }
|
||||
.inbox-row-top { display:flex; justify-content:space-between; gap:6px; align-items:center; }
|
||||
.inbox-row-sender { font-weight:600; font-size:13px; color:var(--db-text,#e4e4e7); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.inbox-row-time { font-size:11px; color:var(--db-text-dim,#a1a1aa); flex:0 0 auto; }
|
||||
.inbox-row-preview { font-size:12px; color:var(--db-text-dim,#a1a1aa); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.inbox-dot { width:8px; height:8px; border-radius:50%; flex:0 0 auto; margin-top:5px; }
|
||||
.inbox-empty { padding:32px 20px; text-align:center; color:var(--db-text-dim,#a1a1aa); font-size:13px; }
|
||||
.inbox-empty a { color:var(--db-accent,#6366f1); }
|
||||
.inbox-msgs { flex:1 1 auto; overflow:auto; padding:14px; display:flex; flex-direction:column; gap:8px; }
|
||||
.inbox-msg { max-width:80%; padding:8px 12px; border-radius:10px; font-size:13px; line-height:1.5; }
|
||||
.inbox-msg-in { align-self:flex-start; background:var(--db-card-bg,#27272a); border:1px solid var(--db-border,#3f3f46); }
|
||||
.inbox-msg-out { align-self:flex-end; background:var(--db-accent,#6366f1); color:#fff; }
|
||||
.inbox-composer { display:flex; gap:8px; padding:12px; border-top:1px solid var(--db-border,#3f3f46); }
|
||||
.inbox-composer textarea { flex:1; resize:none; min-height:40px; max-height:96px; padding:8px 10px;
|
||||
background:var(--db-card-bg,#27272a); border:1px solid var(--db-border,#3f3f46); border-radius:8px; color:var(--db-text,#e4e4e7); font-size:13px; box-sizing:border-box; }
|
||||
.inbox-btn { padding:8px 16px; background:var(--db-accent,#6366f1); color:#fff; border:none; border-radius:8px; cursor:pointer; font-weight:600; font-size:13px; }
|
||||
.inbox-btn:disabled { opacity:.5; cursor:not-allowed; }
|
||||
.inbox-rail-sec { padding:14px; border-bottom:1px solid var(--db-border,#3f3f46); }
|
||||
.inbox-rail-label { font-size:11px; text-transform:uppercase; letter-spacing:.5px; color:var(--db-text-dim,#a1a1aa); margin-bottom:6px; }
|
||||
.inbox-select { width:100%; box-sizing:border-box; padding:6px 10px; border-radius:8px;
|
||||
background:var(--db-card-bg,#27272a); border:1px solid var(--db-border,#3f3f46); color:var(--db-text,#e4e4e7); font-size:13px; }
|
||||
.inbox-pill-group { display:flex; flex-wrap:wrap; gap:6px; }
|
||||
.inbox-pill { padding:4px 10px; font-size:12px; border-radius:999px; cursor:pointer;
|
||||
background:var(--db-card-bg,#27272a); border:1px solid var(--db-border,#3f3f46); color:var(--db-text-dim,#a1a1aa); }
|
||||
.inbox-pill.active { color:#fff; }
|
||||
.inbox-action { display:block; width:100%; box-sizing:border-box; text-align:center; margin-top:8px; padding:8px;
|
||||
border-radius:8px; font-size:13px; cursor:pointer; text-decoration:none;
|
||||
background:var(--db-card-bg,#27272a); border:1px solid var(--db-border,#3f3f46); color:var(--db-text,#e4e4e7); }
|
||||
.inbox-banner { padding:8px 12px; background:rgba(245,158,11,.15); border:1px solid rgba(245,158,11,.35);
|
||||
color:#f59e0b; font-size:12px; border-radius:8px; margin:12px; }
|
||||
.inbox-strip { font-size:12px; color:var(--db-text-dim,#a1a1aa); font-weight:400; }
|
||||
@media (max-width:768px) {
|
||||
.inbox { flex-direction:column; }
|
||||
.inbox-queue, .inbox-conv, .inbox-rail { width:auto; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
async function fetchJSON(url, opts) {
|
||||
try {
|
||||
const r = await fetch(url, opts);
|
||||
if (!r.ok) return null;
|
||||
return await r.json();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function render(container) {
|
||||
container.setAttribute('data-page', 'inbox');
|
||||
injectStyles();
|
||||
container.innerHTML = '<div class="db-loading"><div class="db-spinner"></div></div>';
|
||||
|
||||
const [sessRes, chRes, agentsRes, defsRes] = await Promise.allSettled([
|
||||
fetchJSON('/api/sessions'),
|
||||
fetchJSON('/api/channels'),
|
||||
fetchJSON('/agents'),
|
||||
fetchJSON('/api/agents/definitions'),
|
||||
]);
|
||||
|
||||
const val = (r) => (r.status === 'fulfilled' ? r.value : null) || {};
|
||||
const sessions = (val(sessRes).sessions || val(sessRes) || []);
|
||||
let channels = val(chRes).channels || val(chRes) || [];
|
||||
if (!Array.isArray(channels)) {
|
||||
channels = Object.entries(channels).map(([id, c]) => ({ id, ...(typeof c === 'object' ? c : { platform: c }) }));
|
||||
}
|
||||
const agentList = new Set();
|
||||
const av = val(agentsRes);
|
||||
(av.agents || av || []).forEach((a) => agentList.add(typeof a === 'string' ? a : (a.name || a.id)));
|
||||
const dv = val(defsRes);
|
||||
(dv.definitions || dv.agents || dv || []).forEach((a) => agentList.add(typeof a === 'string' ? a : (a.name || a.id)));
|
||||
const agents = Array.from(agentList).filter(Boolean);
|
||||
|
||||
const channelById = {};
|
||||
channels.forEach((c) => { channelById[c.id || c.channel_id] = c; });
|
||||
|
||||
const channelSessions = sessions.filter((s) => {
|
||||
const sid = s.id || s.session_id || '';
|
||||
return String(sid).startsWith('channel-') || !!s.platform;
|
||||
});
|
||||
|
||||
const runningChannels = channels.filter((c) => c.running === true).length;
|
||||
const totalChannels = channels.length;
|
||||
|
||||
// Load state + history for each channel session, build threads.
|
||||
let threads = [];
|
||||
await Promise.allSettled(channelSessions.map(async (s) => {
|
||||
const sid = s.id || s.session_id;
|
||||
const channelId = s.channel_id || (String(sid).startsWith('channel-') ? String(sid).slice('channel-'.length) : sid);
|
||||
const [hist, state] = await Promise.all([
|
||||
fetchJSON('/api/chat/history/' + encodeURIComponent(sid)),
|
||||
fetchJSON('/api/sessions/' + encodeURIComponent(sid) + '/state'),
|
||||
]);
|
||||
const messages = (hist && (hist.messages || hist.history || hist)) || [];
|
||||
const stateThreads = (state && state.state && state.state.inbox_threads) || {};
|
||||
const groups = groupMessagesBySender(sid, channelId, Array.isArray(messages) ? messages : []);
|
||||
if (groups.length === 0) {
|
||||
threads.push(buildThreadRow(
|
||||
{ key: senderKey(channelId, ''), sessionId: sid, channelId, sender: '', messages: [] },
|
||||
s.platform, stateThreads));
|
||||
} else {
|
||||
groups.forEach((g) => threads.push(buildThreadRow(g, s.platform, stateThreads)));
|
||||
}
|
||||
}));
|
||||
|
||||
const state = {
|
||||
platform: 'all',
|
||||
status: 'open',
|
||||
search: '',
|
||||
selected: null,
|
||||
threads,
|
||||
};
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
if (params.get('status')) state.status = params.get('status');
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="inbox">
|
||||
<div class="inbox-pane inbox-queue">
|
||||
<div class="inbox-head">Channel Inbox
|
||||
<div class="inbox-strip">${runningChannels}/${totalChannels} channels online · ${threads.length} threads</div>
|
||||
</div>
|
||||
<div class="inbox-filters">
|
||||
<input class="inbox-search" id="inbox-search" placeholder="Search conversations" />
|
||||
<div class="inbox-chips" id="inbox-platform-chips"></div>
|
||||
<div class="inbox-chips" id="inbox-status-chips"></div>
|
||||
</div>
|
||||
<div class="inbox-list" id="inbox-list"></div>
|
||||
</div>
|
||||
<div class="inbox-pane inbox-conv" id="inbox-conv"></div>
|
||||
<div class="inbox-pane inbox-rail" id="inbox-rail"></div>
|
||||
</div>`;
|
||||
|
||||
const listEl = container.querySelector('#inbox-list');
|
||||
const convEl = container.querySelector('#inbox-conv');
|
||||
const railEl = container.querySelector('#inbox-rail');
|
||||
const platEl = container.querySelector('#inbox-platform-chips');
|
||||
const statEl = container.querySelector('#inbox-status-chips');
|
||||
const searchEl = container.querySelector('#inbox-search');
|
||||
|
||||
const platformsPresent = Array.from(new Set(threads.map((t) => t.platform).filter(Boolean)));
|
||||
const platformChips = ['all', ...platformsPresent];
|
||||
platEl.innerHTML = platformChips.map((p) =>
|
||||
`<span class="inbox-chip ${p === state.platform ? 'active' : ''}" data-platform="${esc(p)}">${p === 'all' ? 'All' : platformIcon(p) + ' ' + esc(p)}</span>`
|
||||
).join('');
|
||||
|
||||
const statusTabs = ['open', 'waiting_on_agent', 'all', 'resolved'];
|
||||
statEl.innerHTML = statusTabs.map((s) =>
|
||||
`<span class="inbox-chip ${s === state.status ? 'active' : ''}" data-status="${esc(s)}">${s === 'all' ? 'All' : statusMeta(s).label}</span>`
|
||||
).join('');
|
||||
|
||||
function filtered() {
|
||||
let rows = state.threads.filter((t) => {
|
||||
if (state.platform !== 'all' && t.platform !== state.platform) return false;
|
||||
if (state.status === 'all') { /* show all */ }
|
||||
else if (state.status === 'open') { if (t.status === 'resolved') return false; }
|
||||
else if (t.status !== state.status) return false;
|
||||
if (state.search) {
|
||||
const q = state.search.toLowerCase();
|
||||
if (!(t.senderDisplay + ' ' + t.preview).toLowerCase().includes(q)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return sortThreads(rows);
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
const rows = filtered();
|
||||
if (rows.length === 0) {
|
||||
if (channels.length === 0) {
|
||||
listEl.innerHTML = `<div class="inbox-empty">No channels configured<br><a href="/channels">Configure channels →</a></div>`;
|
||||
} else {
|
||||
listEl.innerHTML = `<div class="inbox-empty">No conversations in this view</div>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
listEl.innerHTML = rows.map((t) => {
|
||||
const active = state.selected === t.threadId ? 'active' : '';
|
||||
const sm = statusMeta(t.status);
|
||||
return `<div class="inbox-row ${active}" data-thread="${esc(t.threadId)}">
|
||||
<div class="inbox-row-icon">${platformIcon(t.platform)}</div>
|
||||
<div class="inbox-row-body">
|
||||
<div class="inbox-row-top">
|
||||
<span class="inbox-row-sender">${esc(t.senderDisplay)}</span>
|
||||
<span class="inbox-row-time">${relativeTime(t.updatedAt)}</span>
|
||||
</div>
|
||||
<div class="inbox-row-preview">${esc(t.preview) || '<em>No messages yet</em>'}</div>
|
||||
</div>
|
||||
<div class="inbox-dot" style="background:${sm.color}" title="${sm.label}"></div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
listEl.querySelectorAll('.inbox-row').forEach((row) => {
|
||||
row.addEventListener('click', () => selectThread(row.getAttribute('data-thread')));
|
||||
});
|
||||
}
|
||||
|
||||
function findThread(id) {
|
||||
return state.threads.find((t) => t.threadId === id) || null;
|
||||
}
|
||||
|
||||
async function persistThread(t) {
|
||||
const body = { state: { inbox_threads: { [t.threadId]: { status: t.status, assigned_agent: t.assignedAgent } } } };
|
||||
const r = await fetchJSON('/api/sessions/' + encodeURIComponent(t.sessionId) + '/state', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
|
||||
});
|
||||
return r != null;
|
||||
}
|
||||
|
||||
function renderConversation(t) {
|
||||
if (!t) {
|
||||
convEl.innerHTML = `<div class="inbox-empty">Select a conversation to view messages</div>`;
|
||||
return;
|
||||
}
|
||||
const ch = channelById[t.channelId] || {};
|
||||
const offline = ch.running === false;
|
||||
const sm = statusMeta(t.status);
|
||||
convEl.innerHTML = `
|
||||
<div class="inbox-head">${platformIcon(t.platform)} ${esc(t.senderDisplay)}
|
||||
<span class="inbox-strip">${esc(t.platform || 'channel')} · <span style="color:${sm.color}">${sm.label}</span></span>
|
||||
</div>
|
||||
${offline ? `<div class="inbox-banner">This channel is offline. Replies are disabled until it reconnects.</div>` : ''}
|
||||
<div class="inbox-msgs" id="inbox-msgs"></div>
|
||||
<div class="inbox-composer">
|
||||
<textarea id="inbox-reply" placeholder="Reply${t.assignedAgent ? ' as ' + esc(t.assignedAgent) : ''}…" ${offline ? 'disabled' : ''}></textarea>
|
||||
<button class="inbox-btn" id="inbox-send" ${offline ? 'disabled' : ''}>Send</button>
|
||||
</div>`;
|
||||
const msgsEl = convEl.querySelector('#inbox-msgs');
|
||||
if (!t.messages || t.messages.length === 0) {
|
||||
msgsEl.innerHTML = `<div class="inbox-empty">No messages yet</div>`;
|
||||
} else {
|
||||
msgsEl.innerHTML = t.messages.map((m) => {
|
||||
const role = String(m.role || '').toLowerCase();
|
||||
const out = role === 'assistant' || role === 'agent' || m.direction === 'outbound';
|
||||
return `<div class="inbox-msg ${out ? 'inbox-msg-out' : 'inbox-msg-in'}">${esc(m.content || m.text || '')}</div>`;
|
||||
}).join('');
|
||||
msgsEl.scrollTop = msgsEl.scrollHeight;
|
||||
}
|
||||
const sendBtn = convEl.querySelector('#inbox-send');
|
||||
const replyEl = convEl.querySelector('#inbox-reply');
|
||||
if (sendBtn) sendBtn.addEventListener('click', () => sendReply(t, replyEl, msgsEl, sendBtn));
|
||||
}
|
||||
|
||||
async function sendReply(t, replyEl, msgsEl, sendBtn) {
|
||||
const content = (replyEl.value || '').trim();
|
||||
if (!content) return;
|
||||
sendBtn.disabled = true;
|
||||
const data = await fetchJSON('/api/chat/send', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content, session_id: t.sessionId, agent_name: t.assignedAgent || undefined }),
|
||||
});
|
||||
sendBtn.disabled = false;
|
||||
if (data == null || data.error) {
|
||||
showToast(data && data.error ? data.error : 'Failed to send reply', 'error');
|
||||
return;
|
||||
}
|
||||
const msg = { role: 'agent', content, direction: 'outbound' };
|
||||
t.messages.push(msg);
|
||||
t.preview = content.slice(0, 120);
|
||||
t.updatedAt = Date.now() / 1000;
|
||||
t.status = 'waiting_on_customer';
|
||||
replyEl.value = '';
|
||||
persistThread(t);
|
||||
renderConversation(t);
|
||||
renderRail(t);
|
||||
renderList();
|
||||
}
|
||||
|
||||
function renderRail(t) {
|
||||
if (!t) {
|
||||
railEl.innerHTML = `<div class="inbox-empty">Details</div>`;
|
||||
return;
|
||||
}
|
||||
const ch = channelById[t.channelId] || {};
|
||||
const chStatus = ch.running === true ? 'Connected' : ch.start_error ? 'Error' : 'Stopped';
|
||||
const opts = ['', ...agents].map((a) =>
|
||||
`<option value="${esc(a)}" ${a === t.assignedAgent ? 'selected' : ''}>${a ? esc(a) : 'Unassigned'}</option>`
|
||||
).join('');
|
||||
const pills = STATUS_ORDER.map((s) => {
|
||||
const sm = STATUS_META[s];
|
||||
const active = s === t.status ? 'active' : '';
|
||||
const style = s === t.status ? `background:${sm.color};border-color:${sm.color}` : '';
|
||||
return `<span class="inbox-pill ${active}" data-status="${s}" style="${style}">${sm.label}</span>`;
|
||||
}).join('');
|
||||
railEl.innerHTML = `
|
||||
<div class="inbox-head">Details</div>
|
||||
<div class="inbox-rail-sec">
|
||||
<div class="inbox-rail-label">Customer</div>
|
||||
<div style="font-size:13px">${esc(t.senderDisplay)}</div>
|
||||
<div style="font-size:12px;color:var(--db-text-dim,#a1a1aa)">${esc(t.platform || 'channel')} · ${esc(t.sender || 'general')}</div>
|
||||
</div>
|
||||
<div class="inbox-rail-sec">
|
||||
<div class="inbox-rail-label">Assigned agent</div>
|
||||
<select class="inbox-select" id="inbox-assign">${opts}</select>
|
||||
</div>
|
||||
<div class="inbox-rail-sec">
|
||||
<div class="inbox-rail-label">Status</div>
|
||||
<div class="inbox-pill-group" id="inbox-status-pills">${pills}</div>
|
||||
</div>
|
||||
<div class="inbox-rail-sec">
|
||||
<div class="inbox-rail-label">Actions</div>
|
||||
<a class="inbox-action" href="/chat?session=${encodeURIComponent(t.sessionId)}">Open in Chat</a>
|
||||
<a class="inbox-action" href="/work?card=${encodeURIComponent(t.sessionId)}">Escalate to Work Hub</a>
|
||||
<button class="inbox-action" id="inbox-resolve">Mark resolved</button>
|
||||
</div>
|
||||
<div class="inbox-rail-sec">
|
||||
<div class="inbox-rail-label">Channel</div>
|
||||
<div style="font-size:12px">${platformIcon(t.platform)} ${esc(t.channelId)} · ${esc(chStatus)}</div>
|
||||
</div>`;
|
||||
const assignEl = railEl.querySelector('#inbox-assign');
|
||||
assignEl.addEventListener('change', async () => {
|
||||
t.assignedAgent = assignEl.value;
|
||||
const ok = await persistThread(t);
|
||||
if (ok) showToast(t.assignedAgent ? `Assigned to ${t.assignedAgent}` : 'Unassigned', 'success');
|
||||
renderList();
|
||||
renderConversation(t);
|
||||
});
|
||||
railEl.querySelectorAll('#inbox-status-pills .inbox-pill').forEach((pill) => {
|
||||
pill.addEventListener('click', async () => {
|
||||
t.status = pill.getAttribute('data-status');
|
||||
await persistThread(t);
|
||||
renderRail(t);
|
||||
renderConversation(t);
|
||||
renderList();
|
||||
});
|
||||
});
|
||||
const resolveBtn = railEl.querySelector('#inbox-resolve');
|
||||
resolveBtn.addEventListener('click', async () => {
|
||||
t.status = 'resolved';
|
||||
await persistThread(t);
|
||||
renderRail(t);
|
||||
renderList();
|
||||
showToast('Thread resolved', 'success');
|
||||
});
|
||||
}
|
||||
|
||||
function selectThread(id) {
|
||||
const t = findThread(id);
|
||||
state.selected = id;
|
||||
renderList();
|
||||
renderConversation(t);
|
||||
renderRail(t);
|
||||
}
|
||||
|
||||
platEl.querySelectorAll('.inbox-chip').forEach((chip) => {
|
||||
chip.addEventListener('click', () => {
|
||||
state.platform = chip.getAttribute('data-platform');
|
||||
platEl.querySelectorAll('.inbox-chip').forEach((c) => c.classList.toggle('active', c === chip));
|
||||
renderList();
|
||||
});
|
||||
});
|
||||
statEl.querySelectorAll('.inbox-chip').forEach((chip) => {
|
||||
chip.addEventListener('click', () => {
|
||||
state.status = chip.getAttribute('data-status');
|
||||
statEl.querySelectorAll('.inbox-chip').forEach((c) => c.classList.toggle('active', c === chip));
|
||||
renderList();
|
||||
});
|
||||
});
|
||||
searchEl.addEventListener('input', () => { state.search = searchEl.value; renderList(); });
|
||||
|
||||
renderList();
|
||||
renderConversation(null);
|
||||
renderRail(null);
|
||||
|
||||
const first = filtered()[0];
|
||||
if (first) selectThread(first.threadId);
|
||||
}
|
||||
@@ -10,7 +10,11 @@ export async function render(container) {
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'aiui-board-page-header';
|
||||
header.innerHTML = '<h2 style="margin:0 0 12px">Kanban</h2>';
|
||||
header.innerHTML = '<h2 style="margin:0 0 12px">Kanban</h2>'
|
||||
+ '<div style="margin:0 0 12px;padding:8px 12px;border:1px solid rgba(99,102,241,.3);'
|
||||
+ 'border-radius:8px;background:rgba(99,102,241,.06);font-size:13px">'
|
||||
+ 'Try the new <a href="/work" style="color:var(--db-accent,#6366f1);font-weight:600">Work Hub</a>'
|
||||
+ ' — kanban, card detail, chat and job logs on one screen.</div>';
|
||||
container.appendChild(header);
|
||||
|
||||
const root = document.createElement('div');
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* Marketplace View — browse, inspect, install, and uninstall plugins.
|
||||
* API: /api/marketplace/plugins, /api/marketplace/search,
|
||||
* /api/marketplace/install, /api/marketplace/uninstall,
|
||||
* /api/marketplace/plugins/{id}
|
||||
*/
|
||||
import { helpBanner, pageToolbar, filterChips, searchInput } from './_helpers.js';
|
||||
import { showToast, showConfirm } from '../toast.js';
|
||||
|
||||
const CATEGORY_ICONS = { tools: '🔧', memory: '🧠', all: '📦' };
|
||||
|
||||
function esc(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s == null ? '' : String(s);
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function catIcon(cat) {
|
||||
return CATEGORY_ICONS[cat] || '🧩';
|
||||
}
|
||||
|
||||
function permissionsFor(plugin) {
|
||||
if (Array.isArray(plugin.permissions) && plugin.permissions.length) return plugin.permissions;
|
||||
const map = {
|
||||
web_search: ['network:outbound'],
|
||||
code_executor: ['process:spawn', 'filesystem:temp'],
|
||||
file_manager: ['filesystem:read', 'filesystem:write'],
|
||||
memory_plugin: ['filesystem:read', 'filesystem:write', 'network:outbound'],
|
||||
};
|
||||
return map[plugin.id] || [];
|
||||
}
|
||||
|
||||
export async function render(container) {
|
||||
container.innerHTML = '<div class="db-loading"><div class="db-spinner"></div></div>';
|
||||
|
||||
let plugins = [];
|
||||
try {
|
||||
const r = await fetch('/api/marketplace/plugins');
|
||||
const d = await r.json();
|
||||
plugins = d.plugins || [];
|
||||
} catch (e) {
|
||||
container.innerHTML = '<div class="db-viewer"><pre>Failed to load marketplace: ' + e.message + '</pre></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const state = { query: '', category: 'all', tab: 'browse' };
|
||||
|
||||
const categories = ['all', ...Array.from(new Set(plugins.map((p) => p.category).filter(Boolean)))];
|
||||
|
||||
function dedupe(list) {
|
||||
const seen = new Map();
|
||||
list.forEach((p) => { seen.set(p.id, { ...seen.get(p.id), ...p }); });
|
||||
return Array.from(seen.values());
|
||||
}
|
||||
|
||||
function filtered() {
|
||||
const all = dedupe(plugins);
|
||||
return all.filter((p) => {
|
||||
if (state.tab === 'installed' && !p.installed) return false;
|
||||
if (state.category !== 'all' && p.category !== state.category) return false;
|
||||
if (state.query) {
|
||||
const q = state.query.toLowerCase();
|
||||
return (p.name || '').toLowerCase().includes(q) || (p.description || '').toLowerCase().includes(q);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function cardHtml(p) {
|
||||
const installed = !!p.installed;
|
||||
const id = esc(p.id);
|
||||
return `
|
||||
<div class="db-card" data-plugin="${id}" style="padding:16px 18px;display:flex;flex-direction:column;gap:10px;cursor:pointer">
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<span style="font-size:22px">${catIcon(p.category)}</span>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-size:14px;font-weight:600">${esc(p.name || p.id)}</div>
|
||||
<div style="font-size:11px;color:var(--db-text-dim)">${esc(p.category || 'plugin')} · v${esc(p.version || '1.0.0')}</div>
|
||||
</div>
|
||||
${installed ? '<span style="font-size:10px;font-weight:600;color:#22c55e;padding:2px 8px;border-radius:6px;background:rgba(34,197,94,.12)">Installed</span>' : ''}
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--db-text-dim);line-height:1.5;min-height:34px">${esc(p.description || '')}</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
<button type="button" class="mkt-open" data-plugin="${id}" style="flex:1;padding:6px 10px;border:1px solid var(--db-border);background:transparent;color:var(--db-text);border-radius:7px;cursor:pointer;font-size:12px">Details</button>
|
||||
${installed
|
||||
? `<button type="button" class="mkt-uninstall" data-plugin="${id}" style="flex:1;padding:6px 10px;border:1px solid rgba(239,68,68,.35);background:rgba(239,68,68,.08);color:#f87171;border-radius:7px;cursor:pointer;font-size:12px">Uninstall</button>`
|
||||
: `<button type="button" class="mkt-install" data-plugin="${id}" style="flex:1;padding:6px 10px;border:1px solid var(--db-accent);background:var(--db-accent);color:#fff;border-radius:7px;cursor:pointer;font-size:12px">Install</button>`}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function paint() {
|
||||
const list = filtered();
|
||||
const installedCount = dedupe(plugins).filter((p) => p.installed).length;
|
||||
container.innerHTML = `
|
||||
${helpBanner({
|
||||
title: 'Marketplace',
|
||||
what: 'Browse agent plugins, review the permissions they request, then install or uninstall them.',
|
||||
howToUse: 'Search or filter by category, click <strong>Details</strong> to inspect permissions, then <strong>Install</strong>. Switch to the <strong>Installed</strong> tab to manage your active plugins.',
|
||||
tip: 'The <strong>Code Executor</strong> plugin pairs with Code Studio for sandboxed execution.',
|
||||
collapsed: true,
|
||||
})}
|
||||
${pageToolbar('Plugin Marketplace', `
|
||||
<button type="button" class="db-filter-chip mkt-tab" data-tab="browse" style="padding:5px 14px;border:1px solid var(--db-border);background:${state.tab === 'browse' ? 'var(--db-accent)' : 'transparent'};color:${state.tab === 'browse' ? '#fff' : 'var(--db-text)'};border-radius:6px;cursor:pointer;font-size:12px;font-weight:500">Browse</button>
|
||||
<button type="button" class="db-filter-chip mkt-tab" data-tab="installed" style="padding:5px 14px;border:1px solid var(--db-border);background:${state.tab === 'installed' ? 'var(--db-accent)' : 'transparent'};color:${state.tab === 'installed' ? '#fff' : 'var(--db-text)'};border-radius:6px;cursor:pointer;font-size:12px;font-weight:500">Installed (${installedCount})</button>
|
||||
`)}
|
||||
<div style="display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-bottom:16px">
|
||||
${searchInput('Search plugins…', 'mkt-search')}
|
||||
<div style="display:flex;gap:6px;flex-wrap:wrap">${filterChips(categories, state.category, 'data-cat')}</div>
|
||||
</div>
|
||||
<div id="mkt-grid" class="db-columns" style="grid-template-columns:repeat(auto-fill,minmax(280px,1fr))">
|
||||
${list.map(cardHtml).join('') || '<div class="db-viewer"><pre>No plugins match your filters</pre></div>'}
|
||||
</div>
|
||||
<div id="mkt-drawer"></div>`;
|
||||
|
||||
wire();
|
||||
}
|
||||
|
||||
function drawerHtml(p) {
|
||||
const perms = permissionsFor(p);
|
||||
const installed = !!p.installed;
|
||||
const id = esc(p.id);
|
||||
return `
|
||||
<div class="mkt-drawer-overlay" style="position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:120;display:flex;justify-content:flex-end">
|
||||
<div style="width:min(480px,100%);height:100%;background:var(--db-sidebar-bg);border-left:1px solid var(--db-border);overflow-y:auto;padding:24px;box-sizing:border-box">
|
||||
<div style="display:flex;align-items:flex-start;gap:12px;margin-bottom:16px">
|
||||
<span style="font-size:32px">${catIcon(p.category)}</span>
|
||||
<div style="flex:1">
|
||||
<div style="font-size:18px;font-weight:700">${esc(p.name || p.id)}</div>
|
||||
<div style="font-size:12px;color:var(--db-text-dim)">${esc(p.category || 'plugin')} · v${esc(p.version || '1.0.0')}</div>
|
||||
</div>
|
||||
<button type="button" class="mkt-close" style="background:transparent;border:none;color:var(--db-text-dim);font-size:22px;cursor:pointer;line-height:1">×</button>
|
||||
</div>
|
||||
<div style="font-size:13px;line-height:1.6;color:var(--db-text-dim);margin-bottom:20px">${esc(p.description || 'No description provided.')}</div>
|
||||
<div style="font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--db-text-dim);margin-bottom:8px">Permissions</div>
|
||||
<div style="display:flex;flex-direction:column;gap:6px;margin-bottom:24px">
|
||||
${perms.length
|
||||
? perms.map((perm) => `<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;border:1px solid var(--db-border);border-radius:8px;font-size:12px"><span>🔐</span><code style="font-size:12px">${esc(perm)}</code></div>`).join('')
|
||||
: '<div style="font-size:12px;color:var(--db-text-dim)">No special permissions requested.</div>'}
|
||||
</div>
|
||||
${installed
|
||||
? `<button type="button" class="mkt-uninstall" data-plugin="${id}" style="width:100%;padding:11px;border:1px solid rgba(239,68,68,.35);background:rgba(239,68,68,.08);color:#f87171;border-radius:9px;cursor:pointer;font-size:14px;font-weight:600">Uninstall</button>`
|
||||
: `<button type="button" class="mkt-install" data-plugin="${id}" style="width:100%;padding:11px;border:1px solid var(--db-accent);background:var(--db-accent);color:#fff;border-radius:9px;cursor:pointer;font-size:14px;font-weight:600">Install ${perms.length ? '(' + perms.length + ' permissions)' : ''}</button>`}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function openDrawer(id) {
|
||||
const p = dedupe(plugins).find((x) => x.id === id);
|
||||
if (!p) return;
|
||||
const drawer = container.querySelector('#mkt-drawer');
|
||||
drawer.innerHTML = drawerHtml(p);
|
||||
drawer.querySelector('.mkt-close').addEventListener('click', () => { drawer.innerHTML = ''; });
|
||||
drawer.querySelector('.mkt-drawer-overlay').addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('mkt-drawer-overlay')) drawer.innerHTML = '';
|
||||
});
|
||||
wireActions(drawer);
|
||||
}
|
||||
|
||||
async function install(id) {
|
||||
const p = dedupe(plugins).find((x) => x.id === id);
|
||||
const perms = p ? permissionsFor(p) : [];
|
||||
const ok = await showConfirm(
|
||||
`Install ${p ? p.name || id : id}?`,
|
||||
perms.length ? 'This plugin requests: ' + perms.join(', ') : 'This plugin requests no special permissions.'
|
||||
);
|
||||
if (!ok) return;
|
||||
try {
|
||||
const r = await fetch('/api/marketplace/install', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ plugin_id: id }),
|
||||
});
|
||||
const d = await r.json();
|
||||
if (!r.ok || d.status !== 'installed') throw new Error(d.error || `HTTP ${r.status}`);
|
||||
plugins = plugins.map((x) => (x.id === id ? { ...x, installed: true } : x));
|
||||
showToast(`Installed ${id}`, 'success');
|
||||
paint();
|
||||
} catch (e) {
|
||||
showToast('Install failed: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function uninstall(id) {
|
||||
const ok = await showConfirm(`Uninstall ${id}?`, 'This will remove the plugin from your installed set.');
|
||||
if (!ok) return;
|
||||
try {
|
||||
const r = await fetch('/api/marketplace/uninstall', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ plugin_id: id }),
|
||||
});
|
||||
const d = await r.json();
|
||||
if (!r.ok || d.status !== 'uninstalled') throw new Error(d.error || `HTTP ${r.status}`);
|
||||
plugins = plugins.map((x) => (x.id === id ? { ...x, installed: false } : x));
|
||||
showToast(`Uninstalled ${id}`, 'success');
|
||||
paint();
|
||||
} catch (e) {
|
||||
showToast('Uninstall failed: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function wireActions(root) {
|
||||
root.querySelectorAll('.mkt-install').forEach((b) =>
|
||||
b.addEventListener('click', (e) => { e.stopPropagation(); install(b.dataset.plugin); })
|
||||
);
|
||||
root.querySelectorAll('.mkt-uninstall').forEach((b) =>
|
||||
b.addEventListener('click', (e) => { e.stopPropagation(); uninstall(b.dataset.plugin); })
|
||||
);
|
||||
}
|
||||
|
||||
function wire() {
|
||||
const search = container.querySelector('#mkt-search');
|
||||
if (search) {
|
||||
search.value = state.query;
|
||||
search.addEventListener('input', () => {
|
||||
state.query = search.value;
|
||||
const grid = container.querySelector('#mkt-grid');
|
||||
grid.innerHTML = filtered().map(cardHtml).join('') || '<div class="db-viewer"><pre>No plugins match your filters</pre></div>';
|
||||
wireActions(grid);
|
||||
grid.querySelectorAll('.mkt-open').forEach((b) =>
|
||||
b.addEventListener('click', (e) => { e.stopPropagation(); openDrawer(b.dataset.plugin); })
|
||||
);
|
||||
});
|
||||
}
|
||||
container.querySelectorAll('.mkt-tab').forEach((b) =>
|
||||
b.addEventListener('click', () => { state.tab = b.dataset.tab; paint(); })
|
||||
);
|
||||
container.querySelectorAll('.db-filter-chip[data-cat]').forEach((b) =>
|
||||
b.addEventListener('click', () => { state.category = b.dataset.cat; paint(); })
|
||||
);
|
||||
container.querySelectorAll('.mkt-open').forEach((b) =>
|
||||
b.addEventListener('click', (e) => { e.stopPropagation(); openDrawer(b.dataset.plugin); })
|
||||
);
|
||||
container.querySelectorAll('.db-card[data-plugin]').forEach((c) =>
|
||||
c.addEventListener('click', () => openDrawer(c.dataset.plugin))
|
||||
);
|
||||
wireActions(container);
|
||||
}
|
||||
|
||||
paint();
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* Mini-chat transport — shared agent WebSocket bridge for embedded chat panes.
|
||||
*
|
||||
* Reuses the same wire protocol as the full Chat view (chat.js): it connects
|
||||
* to /api/chat/ws, sends { type:'chat', content, session_id, agent_name },
|
||||
* and streams run_content / run_completed / run_error events back to the caller.
|
||||
*
|
||||
* The full Chat view keeps its own rich rendering; this module only owns the
|
||||
* transport so embedded surfaces (Work Hub Chat tab, etc.) can send to and
|
||||
* stream from the same agent without rebuilding chat from scratch.
|
||||
*
|
||||
* Usage:
|
||||
* import { createMiniChat } from '/plugins/views/mini-chat.js';
|
||||
* const chat = createMiniChat({ agentId, sessionId });
|
||||
* chat.send({ text, onDelta, onComplete, onError });
|
||||
* chat.disconnect(); // on card change / teardown
|
||||
*/
|
||||
|
||||
function wsUrl() {
|
||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
return protocol + '//' + location.host + '/api/chat/ws';
|
||||
}
|
||||
|
||||
function genSessionId() {
|
||||
if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID();
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mini-chat transport bound to one agent + session.
|
||||
*
|
||||
* @param {Object} opts
|
||||
* @param {string} [opts.agentId] - Agent name to route the message to.
|
||||
* @param {string} [opts.sessionId] - Session id (generated if omitted).
|
||||
* @returns {{ send: Function, disconnect: Function, sessionId: string }}
|
||||
*/
|
||||
export function createMiniChat({ agentId = '', sessionId = '' } = {}) {
|
||||
let ws = null;
|
||||
let closed = false;
|
||||
const session = sessionId || genSessionId();
|
||||
let active = null; // { onDelta, onComplete, onError, done }
|
||||
|
||||
function finish(handler, arg) {
|
||||
if (!active || active.done) return;
|
||||
active.done = true;
|
||||
const cb = active[handler];
|
||||
active = null;
|
||||
if (typeof cb === 'function') {
|
||||
try { cb(arg); } catch (_) { /* caller handler error */ }
|
||||
}
|
||||
}
|
||||
|
||||
function handle(data) {
|
||||
const type = data && data.type;
|
||||
switch (type) {
|
||||
case 'run_content':
|
||||
case 'team_run_content':
|
||||
if (data.token && active && typeof active.onDelta === 'function') {
|
||||
try { active.onDelta(data.token); } catch (_) { /* handler error */ }
|
||||
}
|
||||
break;
|
||||
case 'run_completed':
|
||||
case 'team_run_completed':
|
||||
finish('onComplete', data.content || '');
|
||||
break;
|
||||
case 'run_error':
|
||||
case 'team_run_error':
|
||||
finish('onError', new Error(data.error || 'Run error'));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function ensureSocket() {
|
||||
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return ws;
|
||||
ws = new WebSocket(wsUrl());
|
||||
ws.onmessage = (event) => {
|
||||
let data;
|
||||
try { data = JSON.parse(event.data); } catch (_) { return; }
|
||||
if (data.session_id && data.session_id !== session) return;
|
||||
handle(data);
|
||||
};
|
||||
ws.onerror = () => {
|
||||
finish('onError', new Error('Connection error'));
|
||||
};
|
||||
ws.onclose = () => {
|
||||
if (!closed) finish('onError', new Error('Connection closed'));
|
||||
};
|
||||
return ws;
|
||||
}
|
||||
|
||||
function transmit(text) {
|
||||
const sock = ensureSocket();
|
||||
const payload = JSON.stringify({
|
||||
type: 'chat',
|
||||
content: text,
|
||||
session_id: session,
|
||||
agent_name: agentId || null,
|
||||
});
|
||||
if (sock.readyState === WebSocket.OPEN) {
|
||||
sock.send(payload);
|
||||
} else if (sock.readyState === WebSocket.CONNECTING) {
|
||||
sock.addEventListener('open', () => {
|
||||
if (!closed) {
|
||||
try { sock.send(payload); } catch (_) { finish('onError', new Error('Connection error')); }
|
||||
}
|
||||
}, { once: true });
|
||||
} else {
|
||||
finish('onError', new Error('Connection unavailable'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a user message and stream the agent reply.
|
||||
*
|
||||
* @param {Object} req
|
||||
* @param {string} req.text - Message text.
|
||||
* @param {Function} [req.onDelta] - Called with each streamed token.
|
||||
* @param {Function} [req.onComplete] - Called with the final content string.
|
||||
* @param {Function} [req.onError] - Called with an Error on failure.
|
||||
*/
|
||||
function send({ text, onDelta, onComplete, onError }) {
|
||||
if (closed) {
|
||||
if (typeof onError === 'function') onError(new Error('Disconnected'));
|
||||
return;
|
||||
}
|
||||
if (active && !active.done) {
|
||||
finish('onError', new Error('Superseded'));
|
||||
}
|
||||
active = { onDelta, onComplete, onError, done: false };
|
||||
if (typeof navigator !== 'undefined' && navigator.onLine === false) {
|
||||
finish('onError', new Error('You are offline.'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
transmit(String(text == null ? '' : text));
|
||||
} catch (_) {
|
||||
finish('onError', new Error('Connection error'));
|
||||
}
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
closed = true;
|
||||
active = null;
|
||||
if (ws) {
|
||||
try { ws.onclose = null; ws.onerror = null; ws.onmessage = null; ws.close(); } catch (_) { /* noop */ }
|
||||
ws = null;
|
||||
}
|
||||
}
|
||||
|
||||
return { send, disconnect, sessionId: session };
|
||||
}
|
||||
@@ -1,59 +1,486 @@
|
||||
/**
|
||||
* Overview View — system stats, uptime, feature health
|
||||
* Renders into [data-page="overview"]
|
||||
* Overview View — Agent Command Center (STITCH-001).
|
||||
*
|
||||
* Mission-control home: live metric cards, agent table, needs-attention feed,
|
||||
* activity timeline, and an omnibar that opens the existing command palette.
|
||||
*
|
||||
* Data sources (parallel, fault-tolerant via Promise.allSettled):
|
||||
* /api/agents, /api/sessions, /api/approvals/pending, /api/usage/summary,
|
||||
* /api/usage/timeseries, /api/channels, /api/gateway/status,
|
||||
* /api/features, /api/traces
|
||||
*
|
||||
* Renders into [data-page="overview"].
|
||||
*/
|
||||
export async function render(container) {
|
||||
container.innerHTML = '<div class="db-loading"><div class="db-spinner"></div></div>';
|
||||
|
||||
let features = [], startTime = Date.now();
|
||||
try {
|
||||
const res = await fetch('/api/features');
|
||||
const data = await res.json();
|
||||
features = data.features || [];
|
||||
} catch(e) { /* ignore */ }
|
||||
import {
|
||||
metricCard, esc, timeAgo,
|
||||
sumTodayTokens, computeBudgetPct, budgetLevelFor,
|
||||
costStripHTML, budgetBannerHTML,
|
||||
} from '/plugins/views/_helpers.js';
|
||||
|
||||
const healthy = features.filter(f => f.health?.status === 'ok').length;
|
||||
const total = features.length;
|
||||
const uptimeMs = Date.now() - (window.__aiuiLoadTime || Date.now());
|
||||
const uptime = uptimeMs < 60000 ? `${Math.round(uptimeMs/1000)}s` : `${Math.round(uptimeMs/60000)}m`;
|
||||
const METRICS_POLL_MS = 30000;
|
||||
const APPROVALS_POLL_MS = 10000;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="db-columns" style="grid-template-columns:repeat(4,1fr)">
|
||||
<div class="db-card">
|
||||
<div class="db-card-title">Status</div>
|
||||
<div class="db-card-value" style="color:#22c55e">● Online</div>
|
||||
<div class="db-card-footer">Uptime: ${uptime}</div>
|
||||
let _metricsTimer = null;
|
||||
let _approvalsTimer = null;
|
||||
let _visibilityHandler = null;
|
||||
let _container = null;
|
||||
|
||||
const STATUS_COLORS = { running: '#22c55e', active: '#22c55e', idle: '#71717a', error: '#ef4444' };
|
||||
|
||||
function navigate(pageId) {
|
||||
if (window.aiui?.selectPage) window.aiui.selectPage(pageId);
|
||||
}
|
||||
|
||||
function openPalette() {
|
||||
if (window.aiui?.openCommandPalette) window.aiui.openCommandPalette();
|
||||
else if (window.openCommandPalette) window.openCommandPalette();
|
||||
}
|
||||
|
||||
function chatWithAgent(agentName) {
|
||||
if (agentName) {
|
||||
const url = `/chat?agent=${encodeURIComponent(agentName)}`;
|
||||
history.pushState({ pageId: 'chat' }, '', url);
|
||||
}
|
||||
navigate('chat');
|
||||
}
|
||||
|
||||
async function fetchJson(url) {
|
||||
const r = await fetch(url);
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||
return r.json();
|
||||
}
|
||||
|
||||
function settled(results, idx, fallback) {
|
||||
const r = results[idx];
|
||||
return r && r.status === 'fulfilled' ? r.value : fallback;
|
||||
}
|
||||
|
||||
function toArray(data, ...keys) {
|
||||
if (Array.isArray(data)) return data;
|
||||
for (const k of keys) {
|
||||
if (data && Array.isArray(data[k])) return data[k];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function statusDot(status) {
|
||||
const s = (status || 'idle').toLowerCase();
|
||||
const color = STATUS_COLORS[s] || STATUS_COLORS.idle;
|
||||
return `<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${color}" title="${esc(s)}"></span>`;
|
||||
}
|
||||
|
||||
async function loadAll() {
|
||||
const results = await Promise.allSettled([
|
||||
fetchJson('/api/agents'),
|
||||
fetchJson('/api/sessions'),
|
||||
fetchJson('/api/approvals/pending'),
|
||||
fetchJson('/api/usage/summary'),
|
||||
fetchJson('/api/usage/timeseries?hours=24'),
|
||||
fetchJson('/api/channels'),
|
||||
fetchJson('/api/gateway/status'),
|
||||
fetchJson('/api/features'),
|
||||
fetchJson('/api/traces?limit=10'),
|
||||
fetchJson('/api/usage/models'),
|
||||
fetchJson('/api/config/runtime'),
|
||||
]);
|
||||
|
||||
const agents = toArray(settled(results, 0, {}), 'agents');
|
||||
const sessions = toArray(settled(results, 1, {}), 'sessions');
|
||||
const pending = toArray(settled(results, 2, {}), 'approvals', 'pending');
|
||||
const usage = settled(results, 3, null);
|
||||
const timeseries = settled(results, 4, null);
|
||||
const channels = toArray(settled(results, 5, {}), 'channels');
|
||||
const gateway = settled(results, 6, null);
|
||||
const features = toArray(settled(results, 7, {}), 'features');
|
||||
const traces = toArray(settled(results, 8, {}), 'traces');
|
||||
const models = toArray(settled(results, 9, {}), 'models');
|
||||
const runtime = settled(results, 10, null);
|
||||
|
||||
return {
|
||||
agents, sessions, pending, usage, timeseries, channels, gateway, features, traces, models,
|
||||
finops: (runtime && runtime.config && runtime.config.finops) || null,
|
||||
errors: {
|
||||
agents: results[0].status === 'rejected',
|
||||
sessions: results[1].status === 'rejected',
|
||||
usage: results[3].status === 'rejected',
|
||||
timeseries: results[4].status === 'rejected',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function usageSpark(timeseries) {
|
||||
const ts = timeseries && Array.isArray(timeseries.timeseries) ? timeseries.timeseries : [];
|
||||
return ts.map((p) => Number(p.tokens) || 0);
|
||||
}
|
||||
|
||||
function todayTokens(usage) {
|
||||
if (!usage) return null;
|
||||
const s = usage.summary || usage;
|
||||
return s.today_tokens ?? s.total_tokens ?? s.tokens ?? null;
|
||||
}
|
||||
|
||||
function gatewayLabel(gateway) {
|
||||
if (!gateway) return { value: 'Standalone', accent: '#71717a' };
|
||||
if (gateway.status === 'unavailable' || gateway.connected === false) {
|
||||
if (gateway.auth_required || gateway.status === 'auth_required') {
|
||||
return { value: 'Auth required', accent: '#eab308' };
|
||||
}
|
||||
return { value: 'Standalone', accent: '#71717a' };
|
||||
}
|
||||
return { value: 'OK', accent: '#22c55e' };
|
||||
}
|
||||
|
||||
function renderMetrics(data) {
|
||||
const { agents, sessions, pending, usage, timeseries, channels, gateway, errors } = data;
|
||||
const channelsOnline = channels.filter((c) => c.running === true || c.connected === true).length;
|
||||
const tokens = todayTokens(usage);
|
||||
const gw = gatewayLabel(gateway);
|
||||
|
||||
const cards = [
|
||||
metricCard({
|
||||
title: 'Active Agents', value: agents.length, subtitle: 'registered', nav: 'agents',
|
||||
}),
|
||||
metricCard({
|
||||
title: 'Live Sessions', value: errors.sessions ? '—' : sessions.length, subtitle: 'open', nav: 'sessions',
|
||||
}),
|
||||
metricCard({
|
||||
title: 'Pending Approvals', value: pending.length, subtitle: 'awaiting action', nav: 'approvals',
|
||||
accent: pending.length > 0 ? '#eab308' : undefined,
|
||||
}),
|
||||
metricCard({
|
||||
title: 'Tokens Today', value: tokens == null ? '—' : Number(tokens).toLocaleString(),
|
||||
subtitle: 'tokens', nav: 'usage', spark: usageSpark(timeseries),
|
||||
}),
|
||||
metricCard({
|
||||
title: 'Channel Health', value: `${channelsOnline}/${channels.length}`, subtitle: 'channels online', nav: 'channels',
|
||||
}),
|
||||
metricCard({
|
||||
title: 'Gateway Status', value: gw.value, subtitle: gateway?.runtime_type || 'runtime', nav: 'config', accent: gw.accent,
|
||||
}),
|
||||
];
|
||||
|
||||
return `<div class="db-metric-grid" style="display:grid;grid-template-columns:repeat(6,1fr);gap:12px">${cards.join('')}</div>`;
|
||||
}
|
||||
|
||||
function finopsState(data) {
|
||||
const finops = data.finops || {};
|
||||
// E1: usage timeseries unreachable → hide all FinOps UI (no error toast)
|
||||
if (data.errors.usage || data.errors.timeseries) return null;
|
||||
// E2: master toggle off → hide
|
||||
if (finops.enabled === false) return null;
|
||||
const ts = data.timeseries && Array.isArray(data.timeseries.timeseries) ? data.timeseries.timeseries : [];
|
||||
const today = sumTodayTokens(ts);
|
||||
const todayCost = ts.reduce((sum, p) => sum + (Number(p.cost) || 0), 0);
|
||||
const budget = finops.daily_token_budget != null ? Number(finops.daily_token_budget) : null;
|
||||
const warnPct = Number(finops.warn_pct) || 80;
|
||||
const criticalPct = Number(finops.critical_pct) || 95;
|
||||
const pct = computeBudgetPct(today, budget);
|
||||
const level = budgetLevelFor(pct, warnPct, criticalPct);
|
||||
return { today, todayCost, budget, warnPct, criticalPct, pct, level };
|
||||
}
|
||||
|
||||
function renderFinOps(data) {
|
||||
const st = finopsState(data);
|
||||
if (!st) return '';
|
||||
const bannerDismissed = sessionStorage.getItem('finops-banner-dismissed') === '1';
|
||||
const banner = (st.level !== 'none' && !bannerDismissed) ? budgetBannerHTML(st.level, st.pct) : '';
|
||||
const strip = costStripHTML({
|
||||
todayTokens: st.today, todayCost: st.todayCost, budget: st.budget,
|
||||
warnPct: st.warnPct, criticalPct: st.criticalPct, models: data.models,
|
||||
});
|
||||
return `<div id="ov-finops-banner">${banner}</div>${strip}`;
|
||||
}
|
||||
|
||||
function renderAgentTable(agents) {
|
||||
if (agents.length === 0) {
|
||||
return `<div class="db-card" style="text-align:center;padding:2.5rem 1.5rem;color:var(--db-text-dim)">
|
||||
<div style="font-size:2.5rem;opacity:.4;margin-bottom:.75rem">🤖</div>
|
||||
<div style="color:var(--db-text);font-weight:600;margin-bottom:.35rem">No agents registered yet</div>
|
||||
<div style="font-size:13px;margin-bottom:1rem">Configure agents to see them here.</div>
|
||||
<button data-nav="agents" style="padding:8px 16px;background:var(--db-accent,#6366f1);color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:13px">Configure agents</button>
|
||||
</div>`;
|
||||
}
|
||||
const rows = agents.slice(0, 20).map((a) => {
|
||||
const id = a.id || a.name || '';
|
||||
const preview = a.instructions || a.description || '';
|
||||
return `<tr data-agent-id="${esc(id)}" class="db-agent-row" style="cursor:pointer;border-top:1px solid var(--db-border)">
|
||||
<td style="padding:10px 12px">${statusDot(a.status)}</td>
|
||||
<td style="padding:10px 12px;font-weight:500">${esc(a.name || id)}</td>
|
||||
<td style="padding:10px 12px"><span style="font-size:11px;padding:2px 8px;border-radius:10px;background:var(--db-card-bg);border:1px solid var(--db-border)">${esc(a.model || '—')}</span></td>
|
||||
<td style="padding:10px 12px;color:var(--db-text-dim);font-size:12px">${esc(timeAgo(a.last_active || a.updated_at) || (a.status || 'idle'))}</td>
|
||||
<td style="padding:10px 12px;color:var(--db-text-dim);font-size:12px;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(preview.slice(0, 80))}</td>
|
||||
<td style="padding:10px 12px"><button class="db-agent-chat" data-agent-id="${esc(id)}" data-agent-name="${esc(a.name || id)}" style="font-size:11px;padding:4px 12px;background:var(--db-accent,#6366f1);color:#fff;border:none;border-radius:6px;cursor:pointer">Chat</button></td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
return `<div class="db-card" style="padding:0;overflow-x:auto">
|
||||
<table style="width:100%;border-collapse:collapse;font-size:13px">
|
||||
<thead><tr style="text-align:left;color:var(--db-text-dim);font-size:11px;text-transform:uppercase">
|
||||
<th style="padding:10px 12px"></th><th style="padding:10px 12px">Name</th><th style="padding:10px 12px">Model</th>
|
||||
<th style="padding:10px 12px">Last active</th><th style="padding:10px 12px">Preview</th><th style="padding:10px 12px"></th>
|
||||
</tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function buildAttention(data) {
|
||||
const items = [];
|
||||
data.pending.forEach((a) => {
|
||||
items.push({
|
||||
priority: 0, border: '#ef4444',
|
||||
html: `<div style="flex:1"><div style="font-weight:500">⚠ Approve: ${esc(a.tool_name || a.tool || a.action || 'request')}</div>
|
||||
<div style="font-size:12px;color:var(--db-text-dim)">${esc(a.agent_name || a.agent || '')}</div></div>
|
||||
<div style="display:flex;gap:6px">
|
||||
<button class="atn-approve" data-id="${esc(a.id || a.approval_id)}" style="font-size:11px;padding:4px 10px;background:rgba(34,197,94,.15);color:#22c55e;border:1px solid rgba(34,197,94,.3);border-radius:6px;cursor:pointer">Approve</button>
|
||||
<button class="atn-deny" data-id="${esc(a.id || a.approval_id)}" style="font-size:11px;padding:4px 10px;background:rgba(239,68,68,.15);color:#ef4444;border:1px solid rgba(239,68,68,.3);border-radius:6px;cursor:pointer">Deny</button>
|
||||
</div>`,
|
||||
});
|
||||
});
|
||||
data.features.filter((f) => f.health && f.health.status !== 'ok').forEach((f) => {
|
||||
items.push({
|
||||
priority: 1, border: '#eab308',
|
||||
html: `<div style="flex:1"><div style="font-weight:500">⚠ Feature degraded: ${esc(f.name || 'unknown')}</div></div>
|
||||
<button data-nav="debug" style="font-size:11px;padding:4px 10px;border:1px solid var(--db-border);background:transparent;color:var(--db-text);border-radius:6px;cursor:pointer">View</button>`,
|
||||
});
|
||||
});
|
||||
data.channels.filter((c) => c.enabled !== false && c.running !== true && c.connected !== true).forEach((c) => {
|
||||
items.push({
|
||||
priority: 2, border: '#eab308',
|
||||
html: `<div style="flex:1"><div style="font-weight:500">⚠ Channel offline: ${esc(c.name || c.id || c.platform || 'channel')}</div></div>
|
||||
<button data-nav="channels" style="font-size:11px;padding:4px 10px;border:1px solid var(--db-border);background:transparent;color:var(--db-text);border-radius:6px;cursor:pointer">Open</button>`,
|
||||
});
|
||||
});
|
||||
if (data.agents.length === 0) {
|
||||
items.push({
|
||||
priority: 4, border: 'var(--db-accent,#6366f1)',
|
||||
html: `<div style="flex:1"><div style="font-weight:500">No agents configured</div></div>
|
||||
<button data-nav="agents" style="font-size:11px;padding:4px 10px;border:1px solid var(--db-border);background:transparent;color:var(--db-text);border-radius:6px;cursor:pointer">Configure</button>`,
|
||||
});
|
||||
}
|
||||
items.sort((a, b) => a.priority - b.priority);
|
||||
return items.slice(0, 8);
|
||||
}
|
||||
|
||||
function renderAttention(data) {
|
||||
const items = buildAttention(data);
|
||||
if (items.length === 0) {
|
||||
return `<div style="padding:20px;color:var(--db-text-dim);font-size:13px;text-align:center">✓ All clear — nothing needs attention</div>`;
|
||||
}
|
||||
return items.map((it) => `<div class="db-card" style="display:flex;align-items:center;gap:10px;padding:12px 14px;margin-bottom:8px;border-left:3px solid ${it.border}">${it.html}</div>`).join('');
|
||||
}
|
||||
|
||||
function renderTimeline(traces) {
|
||||
if (traces.length === 0) {
|
||||
return `<div style="padding:16px;color:var(--db-text-dim);font-size:13px">No recent activity</div>`;
|
||||
}
|
||||
return traces.slice(0, 10).map((t) => {
|
||||
const label = t.name || t.event || t.tool || t.type || 'event';
|
||||
const agent = t.agent_name || t.agent || '';
|
||||
const when = timeAgo(t.timestamp || t.created_at || t.start_time);
|
||||
return `<div style="display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid var(--db-border);font-size:13px">
|
||||
<span style="color:var(--db-text-dim);font-size:11px;min-width:64px">${esc(when)}</span>
|
||||
<span style="flex:1">${esc(label)}</span>
|
||||
${agent ? `<span style="color:var(--db-text-dim);font-size:12px">${esc(agent)}</span>` : ''}
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
const OVERVIEW_STYLE = `<style id="ov-style">
|
||||
@media (max-width: 768px) {
|
||||
.db-metric-grid { grid-template-columns: repeat(2, 1fr) !important; }
|
||||
.db-ov-columns { grid-template-columns: 1fr !important; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.db-sparkline polyline, .db-sparkline polygon { transition: none !important; }
|
||||
}
|
||||
</style>`;
|
||||
|
||||
function shell(data) {
|
||||
return `${OVERVIEW_STYLE}
|
||||
<div class="db-omnibar" data-omnibar="1" role="button" tabindex="0" style="display:flex;align-items:center;gap:10px;padding:10px 14px;margin-bottom:18px;background:var(--db-card-bg);border:1px solid var(--db-border);border-radius:10px;cursor:pointer;color:var(--db-text-dim);max-width:520px">
|
||||
<span>🔍</span><span style="flex:1;font-size:13px">Search pages, agents, sessions…</span>
|
||||
<span style="font-size:11px;padding:2px 8px;border:1px solid var(--db-border);border-radius:6px">Ctrl+K</span>
|
||||
</div>
|
||||
|
||||
<div id="ov-metrics">${renderMetrics(data)}</div>
|
||||
|
||||
<div id="ov-finops">${renderFinOps(data)}</div>
|
||||
|
||||
<div class="db-ov-columns" style="display:grid;grid-template-columns:3fr 2fr;gap:16px;margin-top:20px">
|
||||
<div>
|
||||
<h3 style="margin:0 0 12px;font-size:15px;font-weight:600">Active Agents</h3>
|
||||
<div id="ov-agents">${renderAgentTable(data.agents)}</div>
|
||||
</div>
|
||||
<div class="db-card">
|
||||
<div class="db-card-title">Features</div>
|
||||
<div class="db-card-value">${total}</div>
|
||||
<div class="db-card-footer">${healthy} active</div>
|
||||
</div>
|
||||
<div class="db-card">
|
||||
<div class="db-card-title">Health</div>
|
||||
<div class="db-card-value">${total > 0 ? Math.round(healthy/total*100) : 0}%</div>
|
||||
<div class="db-card-footer">${total - healthy} degraded</div>
|
||||
</div>
|
||||
<div class="db-card">
|
||||
<div class="db-card-title">API Latency</div>
|
||||
<div class="db-card-value">${Date.now() - startTime}ms</div>
|
||||
<div class="db-card-footer">/api/features</div>
|
||||
<div>
|
||||
<h3 style="margin:0 0 12px;font-size:15px;font-weight:600">Needs Attention</h3>
|
||||
<div id="ov-attention">${renderAttention(data)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="margin:28px 0 16px;font-size:15px;font-weight:600">Feature Health</h3>
|
||||
<div class="db-columns" style="grid-template-columns:repeat(auto-fill,minmax(260px,1fr))">
|
||||
${features.map(f => `
|
||||
<div class="db-card" style="padding:14px 18px;display:flex;align-items:center;justify-content:space-between">
|
||||
<div>
|
||||
<div style="font-size:13px;font-weight:500">${f.name || 'unknown'}</div>
|
||||
<div style="font-size:11px;color:var(--db-text-dim)">${f.description || ''}</div>
|
||||
</div>
|
||||
<span style="font-size:12px;padding:3px 10px;border-radius:20px;${f.health?.status === 'ok'
|
||||
? 'background:rgba(34,197,94,0.15);color:#22c55e'
|
||||
: 'background:rgba(239,68,68,0.15);color:#ef4444'}">${f.health?.status === 'ok' ? '● active' : '○ inactive'}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<h3 style="margin:24px 0 12px;font-size:15px;font-weight:600">Recent Activity</h3>
|
||||
<div id="ov-timeline" class="db-card" style="padding:6px 16px">${renderTimeline(data.traces)}</div>
|
||||
|
||||
<details style="margin-top:24px">
|
||||
<summary style="cursor:pointer;font-size:15px;font-weight:600;margin-bottom:12px">Feature Health</summary>
|
||||
<div class="db-columns" style="grid-template-columns:repeat(auto-fill,minmax(260px,1fr));margin-top:12px">
|
||||
${data.features.map((f) => `
|
||||
<div class="db-card" style="padding:14px 18px;display:flex;align-items:center;justify-content:space-between">
|
||||
<div><div style="font-size:13px;font-weight:500">${esc(f.name || 'unknown')}</div>
|
||||
<div style="font-size:11px;color:var(--db-text-dim)">${esc(f.description || '')}</div></div>
|
||||
<span style="font-size:12px;padding:3px 10px;border-radius:20px;${f.health?.status === 'ok'
|
||||
? 'background:rgba(34,197,94,0.15);color:#22c55e'
|
||||
: 'background:rgba(239,68,68,0.15);color:#ef4444'}">${f.health?.status === 'ok' ? '● active' : '○ inactive'}</span>
|
||||
</div>`).join('')}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div id="ov-drawer"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
function openDrawer(agent) {
|
||||
const host = _container?.querySelector('#ov-drawer');
|
||||
if (!host) return;
|
||||
const id = agent.id || agent.name || '';
|
||||
const tools = Array.isArray(agent.tools) ? agent.tools : [];
|
||||
host.innerHTML = `
|
||||
<div class="ov-drawer-overlay" style="position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:200">
|
||||
<div style="position:absolute;top:0;right:0;height:100%;width:400px;max-width:92vw;background:var(--db-sidebar-bg);border-left:1px solid var(--db-border);padding:24px;overflow-y:auto;box-sizing:border-box">
|
||||
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px">
|
||||
<div>
|
||||
<h2 style="margin:0;font-size:18px">${esc(agent.name || id)}</h2>
|
||||
<div style="margin-top:6px;display:flex;align-items:center;gap:8px">
|
||||
<span style="font-size:11px;padding:2px 8px;border-radius:10px;background:var(--db-card-bg);border:1px solid var(--db-border)">${esc(agent.model || '—')}</span>
|
||||
${statusDot(agent.status)}<span style="font-size:12px;color:var(--db-text-dim)">${esc(agent.status || 'idle')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="ov-drawer-close" style="background:none;border:none;color:var(--db-text-dim);font-size:20px;cursor:pointer">×</button>
|
||||
</div>
|
||||
<div style="margin-top:20px">
|
||||
<div style="font-size:12px;color:var(--db-text-dim);text-transform:uppercase;margin-bottom:6px">Instructions</div>
|
||||
<div style="font-size:13px;line-height:1.6;color:var(--db-text)">${esc((agent.instructions || agent.description || '—').slice(0, 400))}</div>
|
||||
</div>
|
||||
${tools.length ? `<div style="margin-top:18px">
|
||||
<div style="font-size:12px;color:var(--db-text-dim);text-transform:uppercase;margin-bottom:6px">Tools</div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:6px">${tools.map((t) => `<span style="font-size:11px;padding:3px 9px;border-radius:10px;background:var(--db-card-bg);border:1px solid var(--db-border)">${esc(typeof t === 'string' ? t : t.name || 'tool')}</span>`).join('')}</div>
|
||||
</div>` : ''}
|
||||
<div style="margin-top:24px;display:flex;gap:10px">
|
||||
<button class="ov-drawer-chat" data-agent-name="${esc(agent.name || id)}" style="flex:1;padding:9px;background:var(--db-accent,#6366f1);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:500">Chat now</button>
|
||||
<button class="ov-drawer-traces" style="padding:9px 14px;border:1px solid var(--db-border);background:transparent;color:var(--db-text);border-radius:8px;cursor:pointer">View traces</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const close = () => { host.innerHTML = ''; };
|
||||
host.querySelector('.ov-drawer-overlay').addEventListener('click', (e) => { if (e.target === e.currentTarget) close(); });
|
||||
host.querySelector('.ov-drawer-close').addEventListener('click', close);
|
||||
host.querySelector('.ov-drawer-chat')?.addEventListener('click', (e) => {
|
||||
close();
|
||||
chatWithAgent(e.currentTarget.dataset.agentName);
|
||||
});
|
||||
host.querySelector('.ov-drawer-traces')?.addEventListener('click', () => { close(); navigate('traces'); });
|
||||
}
|
||||
|
||||
function bindEvents(container, data) {
|
||||
const agentById = (id) => data.agents.find((a) => (a.id || a.name) === id);
|
||||
|
||||
container.querySelectorAll('[data-nav]').forEach((el) => {
|
||||
const go = () => navigate(el.getAttribute('data-nav'));
|
||||
el.addEventListener('click', go);
|
||||
el.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); go(); } });
|
||||
});
|
||||
|
||||
container.querySelector('[data-omnibar]')?.addEventListener('click', openPalette);
|
||||
|
||||
container.querySelector('.db-finops-banner-usage')?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
navigate('usage');
|
||||
});
|
||||
container.querySelector('.db-finops-banner-dismiss')?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
sessionStorage.setItem('finops-banner-dismissed', '1');
|
||||
const host = container.querySelector('#ov-finops-banner');
|
||||
if (host) host.innerHTML = '';
|
||||
});
|
||||
|
||||
container.querySelectorAll('.db-agent-row').forEach((row) => {
|
||||
row.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.db-agent-chat')) return;
|
||||
const a = agentById(row.dataset.agentId);
|
||||
if (a) openDrawer(a);
|
||||
});
|
||||
});
|
||||
|
||||
container.querySelectorAll('.db-agent-chat').forEach((btn) => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
chatWithAgent(btn.dataset.agentName);
|
||||
});
|
||||
});
|
||||
|
||||
container.querySelectorAll('.atn-approve').forEach((btn) => {
|
||||
btn.addEventListener('click', async () => {
|
||||
try { await fetch(`/api/approvals/${btn.dataset.id}/approve`, { method: 'POST' }); } catch (e) { /* ignore */ }
|
||||
refreshApprovals();
|
||||
});
|
||||
});
|
||||
container.querySelectorAll('.atn-deny').forEach((btn) => {
|
||||
btn.addEventListener('click', async () => {
|
||||
try { await fetch(`/api/approvals/${btn.dataset.id}/deny`, { method: 'POST' }); } catch (e) { /* ignore */ }
|
||||
refreshApprovals();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function paint() {
|
||||
if (!_container) return;
|
||||
const data = await loadAll();
|
||||
_container.__ovData = data;
|
||||
_container.innerHTML = shell(data);
|
||||
bindEvents(_container, data);
|
||||
}
|
||||
|
||||
async function refreshApprovals() {
|
||||
if (!_container || document.hidden) return;
|
||||
let pending = [];
|
||||
try { pending = toArray(await fetchJson('/api/approvals/pending'), 'approvals', 'pending'); } catch (e) { return; }
|
||||
const data = _container.__ovData;
|
||||
if (!data) return;
|
||||
data.pending = pending;
|
||||
const metricsEl = _container.querySelector('#ov-metrics');
|
||||
const attnEl = _container.querySelector('#ov-attention');
|
||||
if (metricsEl) {
|
||||
metricsEl.innerHTML = renderMetrics(data);
|
||||
bindEvents(metricsEl, data);
|
||||
}
|
||||
if (attnEl) {
|
||||
attnEl.innerHTML = renderAttention(data);
|
||||
bindEvents(attnEl, data);
|
||||
}
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
stopPolling();
|
||||
_metricsTimer = setInterval(() => { if (!document.hidden) paint(); }, METRICS_POLL_MS);
|
||||
_approvalsTimer = setInterval(refreshApprovals, APPROVALS_POLL_MS);
|
||||
_visibilityHandler = () => { if (!document.hidden) refreshApprovals(); };
|
||||
document.addEventListener('visibilitychange', _visibilityHandler);
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (_metricsTimer) { clearInterval(_metricsTimer); _metricsTimer = null; }
|
||||
if (_approvalsTimer) { clearInterval(_approvalsTimer); _approvalsTimer = null; }
|
||||
if (_visibilityHandler) { document.removeEventListener('visibilitychange', _visibilityHandler); _visibilityHandler = null; }
|
||||
}
|
||||
|
||||
export async function render(container) {
|
||||
_container = container;
|
||||
container.innerHTML = '<div class="db-loading"><div class="db-spinner"></div></div>';
|
||||
await paint();
|
||||
startPolling();
|
||||
}
|
||||
|
||||
export function cleanup() {
|
||||
stopPolling();
|
||||
_container = null;
|
||||
}
|
||||
@@ -1,72 +1,354 @@
|
||||
/**
|
||||
* Skills View — tool/skill catalog with enable/disable toggles
|
||||
* API: /api/skills
|
||||
* Skills Studio (STITCH-008) — unified Pending → Diff → Approve → Test →
|
||||
* Installed lifecycle for self_improve skill writes.
|
||||
*
|
||||
* Tabs: Pending (skill-filtered approvals) · Installed (toggle catalog) · History.
|
||||
* URL state: /skills?tab=pending|installed|history&approval_id={id}
|
||||
*
|
||||
* Reuses existing /api/approvals and /api/skills endpoints; no new backend.
|
||||
*/
|
||||
export async function render(container) {
|
||||
container.innerHTML = '<div class="db-loading"><div class="db-spinner"></div></div>';
|
||||
import { esc, timeAgo, diffLines, renderMarkdownPreview, isSkillApproval } from '/plugins/views/_helpers.js';
|
||||
|
||||
let tools = [], categories = [];
|
||||
try { const r = await fetch('/api/skills'); const d = await r.json(); tools = d.tools || d.skills || d || []; if (!Array.isArray(tools)) tools = Object.entries(tools).map(([id,t]) => ({id,...(typeof t==='object'?t:{name:t})})); } catch(e) {}
|
||||
try { const r = await fetch('/api/skills/categories'); const d = await r.json(); categories = d.categories || d || []; } catch(e) {}
|
||||
const RISK_ICONS = { low: '✅', medium: '⚠️', high: '🟠', critical: '🔴' };
|
||||
|
||||
// Group by category
|
||||
const grouped = {};
|
||||
tools.forEach(t => { const cat = t.category || 'other'; if (!grouped[cat]) grouped[cat] = []; grouped[cat].push(t); });
|
||||
let _container = null;
|
||||
|
||||
const enabledCount = tools.filter(t => t.enabled !== false).length;
|
||||
function currentTab() {
|
||||
const p = new URLSearchParams(location.search);
|
||||
return p.get('tab') || '';
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="db-columns" style="grid-template-columns:repeat(3,1fr);margin-bottom:24px">
|
||||
<div class="db-card"><div class="db-card-title">Total Skills</div><div class="db-card-value">${tools.length}</div></div>
|
||||
<div class="db-card"><div class="db-card-title">Enabled</div><div class="db-card-value" style="color:#22c55e">${enabledCount}</div></div>
|
||||
<div class="db-card"><div class="db-card-title">Categories</div><div class="db-card-value">${Object.keys(grouped).length}</div></div>
|
||||
function currentApprovalId() {
|
||||
return new URLSearchParams(location.search).get('approval_id') || '';
|
||||
}
|
||||
|
||||
function setUrlState(tab, approvalId) {
|
||||
const p = new URLSearchParams(location.search);
|
||||
if (tab) p.set('tab', tab); else p.delete('tab');
|
||||
if (approvalId) p.set('approval_id', approvalId); else p.delete('approval_id');
|
||||
history.replaceState(history.state, '', `${location.pathname}?${p.toString()}`);
|
||||
}
|
||||
|
||||
async function fetchJson(url, opts) {
|
||||
const r = await fetch(url, opts);
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||
return r.json();
|
||||
}
|
||||
|
||||
function approvalId(a) {
|
||||
return a.id || a.approval_id || '';
|
||||
}
|
||||
|
||||
function skillName(a) {
|
||||
const args = a.arguments || {};
|
||||
if (args.skill_name) return args.skill_name;
|
||||
if (args.name) return args.name;
|
||||
const path = String(args.path || '');
|
||||
const parts = path.split('/').filter(Boolean);
|
||||
if (path.endsWith('SKILL.md') && parts.length >= 2) return parts[parts.length - 2];
|
||||
return a.tool_name || a.action || 'skill';
|
||||
}
|
||||
|
||||
function proposedContent(a) {
|
||||
const args = a.arguments || {};
|
||||
return a.proposed_content || args.content || args.skill_md || '';
|
||||
}
|
||||
|
||||
function changeType(a) {
|
||||
if (a.current_content) return 'Update';
|
||||
const args = a.arguments || {};
|
||||
if (String(args.op || a.type || '').toLowerCase().includes('delete')) return 'Delete';
|
||||
return 'New';
|
||||
}
|
||||
|
||||
// ── Diff overlay ────────────────────────────────────────────────────
|
||||
|
||||
function renderDiffRows(before, after) {
|
||||
const rows = diffLines(before, after);
|
||||
const colorFor = { add: 'rgba(34,197,94,.12)', del: 'rgba(239,68,68,.12)', ctx: 'transparent' };
|
||||
const signFor = { add: '+', del: '-', ctx: ' ' };
|
||||
let adds = 0;
|
||||
let dels = 0;
|
||||
const body = rows.map((r) => {
|
||||
if (r.type === 'add') adds++;
|
||||
if (r.type === 'del') dels++;
|
||||
return `<div style="display:flex;font-family:monospace;font-size:12px;background:${colorFor[r.type]}">
|
||||
<span style="width:16px;text-align:center;color:var(--db-text-dim);flex-shrink:0">${signFor[r.type]}</span>
|
||||
<span style="white-space:pre-wrap;word-break:break-word">${esc(r.text)}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
return { html: body, adds, dels };
|
||||
}
|
||||
|
||||
function diffOverlayHtml(a, skills) {
|
||||
const before = a.current_content || '';
|
||||
const after = proposedContent(a);
|
||||
const hasProposed = after !== '';
|
||||
const { html: rawRows, adds, dels } = renderDiffRows(before, after);
|
||||
const name = skillName(a);
|
||||
const dupe = changeType(a) === 'New' && skills.some((s) => (s.name || s.id) === name);
|
||||
const leftEmpty = before === '';
|
||||
return `
|
||||
<div class="ss-overlay" style="position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:200;display:flex;justify-content:flex-end">
|
||||
<div class="ss-panel" style="width:60%;max-width:920px;height:100%;background:var(--db-sidebar-bg);border-left:1px solid var(--db-border);display:flex;flex-direction:column;box-sizing:border-box">
|
||||
<div style="padding:16px 20px;border-bottom:1px solid var(--db-border);display:flex;align-items:center;justify-content:space-between;gap:12px">
|
||||
<div>
|
||||
<div style="font-size:16px;font-weight:600">${esc(name)}
|
||||
<span style="font-size:11px;padding:2px 8px;border-radius:10px;background:var(--db-card-bg);border:1px solid var(--db-border);margin-left:6px">${changeType(a)}</span>
|
||||
${dupe ? '<span style="font-size:11px;padding:2px 8px;border-radius:10px;background:rgba(234,179,8,.15);color:#eab308;margin-left:4px">Name collision</span>' : ''}
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--db-text-dim);margin-top:4px">${RISK_ICONS[a.risk_level] || ''} ${esc(a.agent_name || '')} · +${adds} −${dels}</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<button class="ss-mode" data-mode="raw" style="padding:5px 12px;font-size:12px;border:1px solid var(--db-border);background:var(--db-accent);color:#fff;border-radius:6px;cursor:pointer">Raw</button>
|
||||
<button class="ss-mode" data-mode="preview" style="padding:5px 12px;font-size:12px;border:1px solid var(--db-border);background:transparent;color:var(--db-text);border-radius:6px;cursor:pointer">Preview</button>
|
||||
<button class="ss-close" style="background:none;border:none;color:var(--db-text-dim);font-size:22px;cursor:pointer">×</button>
|
||||
</div>
|
||||
</div>
|
||||
${hasProposed ? '' : '<div style="padding:12px 20px;background:rgba(239,68,68,.1);color:#ef4444;font-size:13px">Content unavailable — approval disabled</div>'}
|
||||
<div class="ss-diff-body" style="flex:1;overflow:auto;padding:16px 20px">
|
||||
<div class="ss-columns" style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
|
||||
<div>
|
||||
<div style="font-size:12px;color:var(--db-text-dim);text-transform:uppercase;margin-bottom:8px">Current</div>
|
||||
<div class="ss-left">${leftEmpty ? '<div style="color:var(--db-text-dim);font-style:italic">No existing skill</div>' : `<pre style="white-space:pre-wrap;font-size:12px;margin:0">${esc(before)}</pre>`}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:12px;color:var(--db-text-dim);text-transform:uppercase;margin-bottom:8px">Proposed</div>
|
||||
<div class="ss-right">${rawRows}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding:14px 20px;border-top:1px solid var(--db-border);display:flex;gap:10px;justify-content:flex-end">
|
||||
<button class="ss-deny" data-id="${esc(approvalId(a))}" style="padding:8px 18px;background:rgba(239,68,68,.15);color:#ef4444;border:1px solid rgba(239,68,68,.3);border-radius:6px;cursor:pointer;font-size:13px">Reject</button>
|
||||
<button class="ss-approve" data-id="${esc(approvalId(a))}" ${hasProposed ? '' : 'disabled'} style="padding:8px 18px;background:${hasProposed ? 'rgba(34,197,94,.15)' : 'var(--db-card-bg)'};color:${hasProposed ? '#22c55e' : 'var(--db-text-dim)'};border:1px solid rgba(34,197,94,.3);border-radius:6px;cursor:${hasProposed ? 'pointer' : 'not-allowed'};font-size:13px">Approve</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function openDiff(a, skills) {
|
||||
const host = _container.querySelector('#ss-overlay-host');
|
||||
if (!host) return;
|
||||
host.innerHTML = diffOverlayHtml(a, skills);
|
||||
setUrlState('pending', approvalId(a));
|
||||
const before = a.current_content || '';
|
||||
const after = proposedContent(a);
|
||||
|
||||
const close = () => { host.innerHTML = ''; setUrlState('pending', ''); };
|
||||
host.querySelector('.ss-overlay').addEventListener('click', (e) => { if (e.target === e.currentTarget) close(); });
|
||||
host.querySelector('.ss-close').addEventListener('click', close);
|
||||
|
||||
host.querySelectorAll('.ss-mode').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const mode = btn.dataset.mode;
|
||||
host.querySelectorAll('.ss-mode').forEach((b) => {
|
||||
const on = b === btn;
|
||||
b.style.background = on ? 'var(--db-accent)' : 'transparent';
|
||||
b.style.color = on ? '#fff' : 'var(--db-text)';
|
||||
});
|
||||
const left = host.querySelector('.ss-left');
|
||||
const right = host.querySelector('.ss-right');
|
||||
if (mode === 'preview') {
|
||||
left.innerHTML = before === '' ? '<div style="color:var(--db-text-dim);font-style:italic">No existing skill</div>' : renderMarkdownPreview(before);
|
||||
right.innerHTML = renderMarkdownPreview(after);
|
||||
} else {
|
||||
left.innerHTML = before === '' ? '<div style="color:var(--db-text-dim);font-style:italic">No existing skill</div>' : `<pre style="white-space:pre-wrap;font-size:12px;margin:0">${esc(before)}</pre>`;
|
||||
right.innerHTML = renderDiffRows(before, after).html;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const resolve = async (verb, id) => {
|
||||
try { await fetch(`/api/approvals/${id}/${verb}`, { method: 'POST' }); } catch (e) { /* ignore */ }
|
||||
close();
|
||||
paint();
|
||||
};
|
||||
host.querySelector('.ss-approve')?.addEventListener('click', (e) => {
|
||||
if (e.currentTarget.disabled) return;
|
||||
resolve('approve', e.currentTarget.dataset.id);
|
||||
});
|
||||
host.querySelector('.ss-deny')?.addEventListener('click', (e) => resolve('deny', e.currentTarget.dataset.id));
|
||||
}
|
||||
|
||||
// ── Tabs ────────────────────────────────────────────────────────────
|
||||
|
||||
function pendingCard(a, skills) {
|
||||
const name = skillName(a);
|
||||
const dupe = changeType(a) === 'New' && skills.some((s) => (s.name || s.id) === name);
|
||||
const preview = proposedContent(a).slice(0, 200);
|
||||
return `<div class="ss-pending-card db-card" data-id="${esc(approvalId(a))}" style="margin-bottom:10px;padding:16px 20px;cursor:pointer">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px">
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-weight:600;font-size:14px">${esc(name.slice(0, 40))}
|
||||
<span style="font-size:11px;padding:2px 8px;border-radius:10px;background:var(--db-card-bg);border:1px solid var(--db-border);margin-left:6px">${changeType(a)}</span>
|
||||
${dupe ? '<span style="font-size:11px;padding:2px 8px;border-radius:10px;background:rgba(234,179,8,.15);color:#eab308;margin-left:4px">Name collision</span>' : ''}
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--db-text-dim);margin-top:4px">🤖 ${esc(a.agent_name || '—')} · ${esc((a.metadata && a.metadata.source) || 'self_improve')} · ${esc(timeAgo(a.created_at))}</div>
|
||||
</div>
|
||||
<div style="font-size:12px">${RISK_ICONS[a.risk_level] || ''}</div>
|
||||
</div>
|
||||
<div id="skills-list"></div>
|
||||
`;
|
||||
${preview ? `<pre style="margin:10px 0 0;font-size:11px;color:var(--db-text-dim);white-space:pre-wrap;max-height:44px;overflow:hidden">${esc(preview)}</pre>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const list = container.querySelector('#skills-list');
|
||||
for (const [category, catTools] of Object.entries(grouped)) {
|
||||
const section = document.createElement('div');
|
||||
section.style.marginBottom = '20px';
|
||||
section.innerHTML = `<h3 style="font-size:14px;font-weight:600;margin:0 0 10px;text-transform:capitalize">${category} (${catTools.length})</h3>`;
|
||||
function pendingTab(pending, skills) {
|
||||
if (pending.length === 0) {
|
||||
return `<div class="db-card" style="text-align:center;padding:2.5rem 1.5rem;color:var(--db-text-dim)">
|
||||
<div style="font-size:2.5rem;opacity:.4;margin-bottom:.75rem">📖</div>
|
||||
<div style="color:var(--db-text);font-weight:600;margin-bottom:.35rem">No pending skill writes</div>
|
||||
<div style="font-size:13px">Set <code>SKILL_WRITE_APPROVAL=1</code> and run an agent with <code>self_improve</code> to stage proposals here.</div>
|
||||
</div>`;
|
||||
}
|
||||
return pending.map((a) => pendingCard(a, skills)).join('');
|
||||
}
|
||||
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'db-columns';
|
||||
grid.style.gridTemplateColumns = 'repeat(auto-fill,minmax(280px,1fr))';
|
||||
|
||||
catTools.forEach(t => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'db-card';
|
||||
card.style.cssText = 'padding:14px 18px;display:flex;align-items:center;justify-content:space-between';
|
||||
function installedTab(tools, pendingNames) {
|
||||
const grouped = {};
|
||||
tools.forEach((t) => { const c = t.category || 'other'; (grouped[c] = grouped[c] || []).push(t); });
|
||||
if (tools.length === 0) return '<div class="db-viewer"><pre>No skills/tools available</pre></div>';
|
||||
return Object.entries(grouped).map(([category, catTools]) => {
|
||||
const cards = catTools.map((t) => {
|
||||
const enabled = t.enabled !== false;
|
||||
const icon = t.icon || '🔧';
|
||||
const keyStatus = t.api_key_set ? '🔑' : '';
|
||||
card.innerHTML = `
|
||||
const hasPending = pendingNames.has(t.name || t.id);
|
||||
return `<div class="db-card" style="padding:14px 18px;display:flex;align-items:center;justify-content:space-between">
|
||||
<div style="display:flex;align-items:center;gap:10px;flex:1">
|
||||
<span style="font-size:20px">${icon}</span>
|
||||
<div>
|
||||
<div style="font-size:13px;font-weight:500">${t.name || t.id} ${keyStatus}</div>
|
||||
<div style="font-size:11px;color:var(--db-text-dim)">${t.description || ''}</div>
|
||||
<div style="font-size:13px;font-weight:500">${esc(t.name || t.id)} ${keyStatus}${hasPending ? ' <span title="Pending change" style="color:#eab308">●</span>' : ''}</div>
|
||||
<div style="font-size:11px;color:var(--db-text-dim)">${esc(t.description || '')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<label style="position:relative;display:inline-block;width:40px;height:22px;cursor:pointer">
|
||||
<input type="checkbox" class="skill-toggle" data-id="${t.id || t.name}" ${enabled ? 'checked' : ''} style="opacity:0;width:0;height:0">
|
||||
<input type="checkbox" class="skill-toggle" data-id="${esc(t.id || t.name)}" ${enabled ? 'checked' : ''} style="opacity:0;width:0;height:0">
|
||||
<span style="position:absolute;inset:0;background:${enabled ? 'var(--db-accent)' : 'var(--db-border)'};border-radius:22px;transition:0.3s"></span>
|
||||
<span style="position:absolute;top:2px;left:${enabled ? '20px' : '2px'};width:18px;height:18px;background:#fff;border-radius:50%;transition:0.3s"></span>
|
||||
</label>
|
||||
`;
|
||||
grid.appendChild(card);
|
||||
});
|
||||
</div>`;
|
||||
}).join('');
|
||||
return `<div style="margin-bottom:20px">
|
||||
<h3 style="font-size:14px;font-weight:600;margin:0 0 10px;text-transform:capitalize">${esc(category)} (${catTools.length})</h3>
|
||||
<div class="db-columns" style="grid-template-columns:repeat(auto-fill,minmax(280px,1fr))">${cards}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
section.appendChild(grid);
|
||||
list.appendChild(section);
|
||||
function historyTab(history) {
|
||||
const skillHist = (Array.isArray(history) ? history : []).filter(isSkillApproval);
|
||||
if (skillHist.length === 0) return '<div style="font-size:13px;color:var(--db-text-dim)">No skill approval history</div>';
|
||||
return skillHist.slice(0, 25).map((h) => {
|
||||
const approved = h.status === 'approved' || h.approved;
|
||||
return `<div style="padding:8px 0;border-bottom:1px solid var(--db-border);font-size:13px;display:flex;justify-content:space-between;gap:10px">
|
||||
<span>${esc(skillName(h))} — ${esc(h.agent_name || '')}</span>
|
||||
<span style="color:${approved ? '#22c55e' : '#ef4444'}">${approved ? '✓ approved' : '✗ denied'} · ${esc(timeAgo(h.resolved_at || h.created_at))}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
const STUDIO_STYLE = `<style id="ss-style">
|
||||
@media (max-width: 768px) {
|
||||
.ss-panel { width: 100% !important; }
|
||||
.ss-columns { grid-template-columns: 1fr !important; }
|
||||
}
|
||||
</style>`;
|
||||
|
||||
if (tools.length === 0) list.innerHTML = '<div class="db-viewer"><pre>No skills/tools available</pre></div>';
|
||||
function shell(data) {
|
||||
const { pending, tools } = data;
|
||||
const tab = data.activeTab;
|
||||
const enabledCount = tools.filter((t) => t.enabled !== false).length;
|
||||
const catCount = new Set(tools.map((t) => t.category || 'other')).size;
|
||||
const tabBtn = (id, label, badge) => {
|
||||
const on = tab === id;
|
||||
return `<button class="ss-tab" data-tab="${id}" style="padding:8px 16px;border:none;border-bottom:2px solid ${on ? 'var(--db-accent)' : 'transparent'};background:none;color:${on ? 'var(--db-text)' : 'var(--db-text-dim)'};font-size:13px;font-weight:${on ? '600' : '400'};cursor:pointer">${label}${badge ? ` <span style="background:rgba(234,179,8,.2);color:#eab308;padding:1px 7px;border-radius:10px;font-size:11px;margin-left:4px">${badge}</span>` : ''}</button>`;
|
||||
};
|
||||
let body = '';
|
||||
if (tab === 'installed') body = installedTab(tools, data.pendingNames);
|
||||
else if (tab === 'history') body = historyTab(data.history);
|
||||
else body = pendingTab(pending, tools);
|
||||
|
||||
container.querySelectorAll('.skill-toggle').forEach(cb => {
|
||||
return `${STUDIO_STYLE}
|
||||
<div style="margin-bottom:8px">
|
||||
<h2 style="margin:0;font-size:1.1rem;font-weight:600">Skills Studio</h2>
|
||||
<div style="font-size:13px;color:var(--db-text-dim)">Review self_improve proposals</div>
|
||||
</div>
|
||||
<div class="db-columns" style="grid-template-columns:repeat(3,1fr);margin:16px 0 20px">
|
||||
<div class="db-card"><div class="db-card-title">Pending</div><div class="db-card-value" style="color:#eab308">${pending.length}</div></div>
|
||||
<div class="db-card"><div class="db-card-title">Enabled</div><div class="db-card-value" style="color:#22c55e">${enabledCount}</div></div>
|
||||
<div class="db-card"><div class="db-card-title">Categories</div><div class="db-card-value">${catCount}</div></div>
|
||||
</div>
|
||||
<div style="display:flex;gap:4px;border-bottom:1px solid var(--db-border);margin-bottom:16px">
|
||||
${tabBtn('pending', 'Pending', pending.length || '')}
|
||||
${tabBtn('installed', 'Installed', '')}
|
||||
${tabBtn('history', 'History', '')}
|
||||
</div>
|
||||
<div id="ss-body">${body}</div>
|
||||
<div id="ss-overlay-host"></div>`;
|
||||
}
|
||||
|
||||
async function loadAll() {
|
||||
const results = await Promise.allSettled([
|
||||
fetchJson('/api/skills'),
|
||||
fetchJson('/api/approvals/pending'),
|
||||
fetchJson('/api/approvals/history'),
|
||||
]);
|
||||
const val = (i, f) => (results[i].status === 'fulfilled' ? results[i].value : f);
|
||||
const sk = val(0, {});
|
||||
let tools = sk.tools || sk.skills || sk || [];
|
||||
if (!Array.isArray(tools)) tools = Object.entries(tools).map(([id, t]) => ({ id, ...(typeof t === 'object' ? t : { name: t }) }));
|
||||
const rawPending = (val(1, {}).approvals) || [];
|
||||
const history = (val(2, {}).approvals) || (val(2, {}).history) || [];
|
||||
const pending = rawPending.filter(isSkillApproval);
|
||||
const pendingNames = new Set(pending.map((a) => skillName(a)));
|
||||
return { tools, pending, history, pendingNames };
|
||||
}
|
||||
|
||||
function bindEvents(data) {
|
||||
_container.querySelectorAll('.ss-tab').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
data.activeTab = btn.dataset.tab;
|
||||
setUrlState(data.activeTab, '');
|
||||
renderShell(data);
|
||||
});
|
||||
});
|
||||
_container.querySelectorAll('.ss-pending-card').forEach((card) => {
|
||||
card.addEventListener('click', () => {
|
||||
const a = data.pending.find((x) => approvalId(x) === card.dataset.id);
|
||||
if (a) openDiff(a, data.tools);
|
||||
});
|
||||
});
|
||||
_container.querySelectorAll('.skill-toggle').forEach((cb) => {
|
||||
cb.addEventListener('change', async () => {
|
||||
try { await fetch(`/api/skills/${cb.dataset.id}/toggle`, {method:'POST'}); render(container); } catch(e) {}
|
||||
try { await fetch(`/api/skills/${cb.dataset.id}/toggle`, { method: 'POST' }); } catch (e) { /* ignore */ }
|
||||
paint();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderShell(data) {
|
||||
_container.innerHTML = shell(data);
|
||||
bindEvents(data);
|
||||
}
|
||||
|
||||
async function paint() {
|
||||
if (!_container) return;
|
||||
const data = await loadAll();
|
||||
const tab = currentTab() || (data.pending.length > 0 ? 'pending' : 'installed');
|
||||
data.activeTab = tab;
|
||||
renderShell(data);
|
||||
const deepId = currentApprovalId();
|
||||
if (deepId && tab === 'pending') {
|
||||
const a = data.pending.find((x) => approvalId(x) === deepId);
|
||||
if (a) openDiff(a, data.tools);
|
||||
}
|
||||
}
|
||||
|
||||
export async function render(container) {
|
||||
_container = container;
|
||||
container.innerHTML = '<div class="db-loading"><div class="db-spinner"></div></div>';
|
||||
await paint();
|
||||
}
|
||||
|
||||
export function cleanup() {
|
||||
_container = null;
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* Unified Work Hub (/work) — three-pane operator surface (STITCH-003).
|
||||
*
|
||||
* Pane 1: Kanban board (reuses createInteractiveBoard from board.js).
|
||||
* Pane 2: Selected card detail with Description / Chat / Tools / Traces tabs.
|
||||
* Pane 3: Activity — job runs, live log, trace link for the card's latest run.
|
||||
*
|
||||
* Card selection updates panes 2+3 without a page navigation. URL state
|
||||
* (?board=&card=&tab=) is kept in sync for deep links.
|
||||
*/
|
||||
import { createInteractiveBoard, relativeTime, escapeHtml } from '../board.js';
|
||||
import { createMiniChat } from './mini-chat.js';
|
||||
|
||||
const STYLE_ID = 'work-hub-styles';
|
||||
|
||||
function injectStyles() {
|
||||
if (document.getElementById(STYLE_ID)) return;
|
||||
const style = document.createElement('style');
|
||||
style.id = STYLE_ID;
|
||||
style.textContent = `
|
||||
.work-hub { display:flex; height:100%; min-height:0; gap:1px; background:var(--db-border,#3f3f46); }
|
||||
.work-hub-pane { background:var(--db-bg,#18181b); overflow:auto; min-height:0; }
|
||||
.work-hub-p1 { width:300px; min-width:240px; flex:0 0 auto; }
|
||||
.work-hub-p2 { flex:1 1 auto; min-width:0; }
|
||||
.work-hub-p3 { width:320px; min-width:280px; flex:0 0 auto; }
|
||||
.work-hub-pane-head { padding:10px 14px; border-bottom:1px solid var(--db-border,#3f3f46);
|
||||
font-size:13px; font-weight:600; position:sticky; top:0; background:var(--db-bg,#18181b); z-index:2; }
|
||||
.work-hub-empty { padding:24px; text-align:center; color:var(--db-text-dim,#a1a1aa); font-size:13px; }
|
||||
.work-hub-tabs { display:flex; gap:4px; padding:8px 14px 0; border-bottom:1px solid var(--db-border,#3f3f46); }
|
||||
.work-hub-tab { padding:6px 12px; font-size:13px; cursor:pointer; border:none; background:none;
|
||||
color:var(--db-text-dim,#a1a1aa); border-bottom:2px solid transparent; }
|
||||
.work-hub-tab.active { color:var(--db-text,#e4e4e7); border-bottom-color:var(--db-accent,#6366f1); }
|
||||
.work-hub-tab:disabled { opacity:.4; cursor:not-allowed; }
|
||||
.work-hub-tabbody { padding:14px; }
|
||||
.work-hub-chat-log { display:flex; flex-direction:column; gap:8px; margin-bottom:12px; }
|
||||
.work-hub-chat-msg { padding:8px 10px; border-radius:8px; font-size:13px; line-height:1.5;
|
||||
background:var(--db-card-bg,#27272a); border:1px solid var(--db-border,#3f3f46); }
|
||||
.work-hub-chat-msg .role { font-size:11px; font-weight:600; color:var(--db-accent,#6366f1); }
|
||||
.work-hub-chat-msg .work-hub-chat-body { white-space:pre-wrap; }
|
||||
.work-hub-chat-streaming .work-hub-chat-body::after { content:'▌'; animation:work-hub-blink 1s infinite; }
|
||||
.work-hub-chat-error { border-color:#ef4444; }
|
||||
.work-hub-chat-error .work-hub-chat-body { color:#ef4444; }
|
||||
@keyframes work-hub-blink { 50% { opacity:0; } }
|
||||
.work-hub-composer { display:flex; gap:8px; align-items:flex-end; }
|
||||
.work-hub-composer textarea { flex:1; resize:none; min-height:40px; max-height:64px; padding:8px 10px;
|
||||
background:var(--db-card-bg,#27272a); border:1px solid var(--db-border,#3f3f46); border-radius:8px;
|
||||
color:var(--db-text,#e4e4e7); font-size:13px; box-sizing:border-box; }
|
||||
.work-hub-log { font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12px;
|
||||
line-height:1.6; white-space:pre-wrap; background:var(--db-card-bg,#27272a);
|
||||
border:1px solid var(--db-border,#3f3f46); border-radius:8px; padding:10px; max-height:40vh; overflow:auto; }
|
||||
.work-hub-job { display:flex; align-items:center; gap:8px; padding:6px 0; font-size:13px; }
|
||||
.work-hub-dot { width:8px; height:8px; border-radius:50%; flex:0 0 auto; }
|
||||
@media (max-width:1024px) {
|
||||
.work-hub { flex-direction:column; }
|
||||
.work-hub-p1, .work-hub-p2, .work-hub-p3 { width:auto; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function statusColor(status) {
|
||||
const s = String(status || '').toLowerCase();
|
||||
if (s === 'done' || s === 'completed' || s === 'success') return '#22c55e';
|
||||
if (s === 'running' || s === 'in_progress') return '#f59e0b';
|
||||
if (s === 'failed' || s === 'error' || s === 'blocked') return '#ef4444';
|
||||
return '#6366f1';
|
||||
}
|
||||
|
||||
export async function render(container) {
|
||||
container.setAttribute('data-page', 'work');
|
||||
container.classList.add('work-hub-page');
|
||||
container.innerHTML = '';
|
||||
injectStyles();
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const boardState = { id: params.get('board') || 'default' };
|
||||
let selectedCard = null;
|
||||
let activeTab = params.get('tab') || 'description';
|
||||
const pendingCardId = params.get('card');
|
||||
let miniChat = null;
|
||||
|
||||
function teardownChat() {
|
||||
if (miniChat) {
|
||||
try { miniChat.disconnect(); } catch (_) { /* noop */ }
|
||||
miniChat = null;
|
||||
}
|
||||
}
|
||||
|
||||
const hub = document.createElement('div');
|
||||
hub.className = 'work-hub';
|
||||
container.appendChild(hub);
|
||||
|
||||
const p1 = document.createElement('div');
|
||||
p1.className = 'work-hub-pane work-hub-p1';
|
||||
const p1Body = document.createElement('div');
|
||||
p1.appendChild(p1Body);
|
||||
hub.appendChild(p1);
|
||||
|
||||
const p2 = document.createElement('div');
|
||||
p2.className = 'work-hub-pane work-hub-p2';
|
||||
hub.appendChild(p2);
|
||||
|
||||
const p3 = document.createElement('div');
|
||||
p3.className = 'work-hub-pane work-hub-p3';
|
||||
hub.appendChild(p3);
|
||||
|
||||
function syncUrl() {
|
||||
const q = new URLSearchParams();
|
||||
q.set('board', boardState.id);
|
||||
if (selectedCard) { q.set('card', selectedCard.id); q.set('tab', activeTab); }
|
||||
const url = `${window.location.pathname}?${q.toString()}`;
|
||||
window.history.replaceState({}, '', url);
|
||||
}
|
||||
|
||||
function renderDetail() {
|
||||
teardownChat();
|
||||
if (!selectedCard) {
|
||||
p2.innerHTML = '<div class="work-hub-empty">Select a card to see its details.</div>';
|
||||
return;
|
||||
}
|
||||
const c = selectedCard;
|
||||
const meta = c.meta || {};
|
||||
const hasAgent = !!(c.assignee || meta.agent_id);
|
||||
const tabs = [
|
||||
['description', 'Description', false],
|
||||
['chat', 'Chat', !hasAgent],
|
||||
['tools', 'Tools', false],
|
||||
['traces', 'Traces', false],
|
||||
];
|
||||
p2.innerHTML = `
|
||||
<div class="work-hub-pane-head">${escapeHtml(c.title || 'Card')}
|
||||
<span class="db-text-dim" style="font-weight:400;font-size:12px;margin-left:8px">${escapeHtml(c.status || '')}</span>
|
||||
</div>
|
||||
<div class="work-hub-tabs">
|
||||
${tabs.map(([id, label, disabled]) =>
|
||||
`<button class="work-hub-tab ${id === activeTab ? 'active' : ''}" data-tab="${id}" ${disabled ? 'disabled' : ''}>${label}</button>`
|
||||
).join('')}
|
||||
</div>
|
||||
<div class="work-hub-tabbody" id="work-hub-tabbody"></div>`;
|
||||
|
||||
p2.querySelectorAll('.work-hub-tab').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
if (btn.disabled) return;
|
||||
activeTab = btn.getAttribute('data-tab');
|
||||
syncUrl();
|
||||
renderDetail();
|
||||
});
|
||||
});
|
||||
|
||||
const body = p2.querySelector('#work-hub-tabbody');
|
||||
if (activeTab === 'description') {
|
||||
body.innerHTML = c.body
|
||||
? `<div style="font-size:13px;line-height:1.7;white-space:pre-wrap">${escapeHtml(c.body)}</div>`
|
||||
: '<div class="work-hub-empty">No description.</div>';
|
||||
} else if (activeTab === 'chat') {
|
||||
renderChat(body, c);
|
||||
} else if (activeTab === 'tools') {
|
||||
const tools = meta.tools || [];
|
||||
body.innerHTML = tools.length
|
||||
? tools.map((t) => `<span class="db-badge" style="display:inline-block;margin:2px;padding:2px 8px;border-radius:6px;background:var(--db-card-bg,#27272a);font-size:12px">${escapeHtml(String(t))}</span>`).join('')
|
||||
: '<div class="work-hub-empty">No tools listed for this card.</div>';
|
||||
} else if (activeTab === 'traces') {
|
||||
const sessionId = meta.session_id;
|
||||
body.innerHTML = sessionId
|
||||
? `<a href="/traces?session_id=${encodeURIComponent(sessionId)}" style="color:var(--db-accent,#6366f1)">Open traces for this session →</a>`
|
||||
: '<div class="work-hub-empty">No traces linked.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderChat(body, card) {
|
||||
const meta = card.meta || {};
|
||||
const agentId = card.assignee || meta.agent_id || '';
|
||||
const sessionId = meta.session_id || '';
|
||||
body.innerHTML = `
|
||||
<div class="work-hub-chat-log" id="work-hub-chat-log">
|
||||
<div class="work-hub-empty">Ask the assigned agent (${escapeHtml(agentId)}) about this card.</div>
|
||||
</div>
|
||||
<div class="work-hub-composer">
|
||||
<textarea id="work-hub-composer" rows="2" placeholder="Message ${escapeHtml(agentId)}…"></textarea>
|
||||
<button class="db-btn db-btn-sm" id="work-hub-send">Send</button>
|
||||
</div>
|
||||
<div style="margin-top:8px;font-size:12px"><a href="${sessionId ? '/chat?session_id=' + encodeURIComponent(sessionId) : '/chat'}" style="color:var(--db-accent,#6366f1)">Open full chat →</a></div>`;
|
||||
|
||||
const log = body.querySelector('#work-hub-chat-log');
|
||||
const ta = body.querySelector('#work-hub-composer');
|
||||
const send = body.querySelector('#work-hub-send');
|
||||
|
||||
teardownChat();
|
||||
miniChat = createMiniChat({ agentId, sessionId });
|
||||
|
||||
function appendMsg(role, text) {
|
||||
if (log.querySelector('.work-hub-empty')) log.innerHTML = '';
|
||||
const el = document.createElement('div');
|
||||
el.className = 'work-hub-chat-msg';
|
||||
el.innerHTML = `<div class="role">${escapeHtml(role)}</div>${escapeHtml(text)}`;
|
||||
log.appendChild(el);
|
||||
log.scrollTop = log.scrollHeight;
|
||||
return el;
|
||||
}
|
||||
|
||||
function submit() {
|
||||
const text = ta.value.trim();
|
||||
if (!text || !miniChat) return;
|
||||
appendMsg('You', text);
|
||||
ta.value = '';
|
||||
|
||||
if (log.querySelector('.work-hub-empty')) log.innerHTML = '';
|
||||
const reply = document.createElement('div');
|
||||
reply.className = 'work-hub-chat-msg work-hub-chat-streaming';
|
||||
const roleEl = document.createElement('div');
|
||||
roleEl.className = 'role';
|
||||
roleEl.textContent = agentId || 'Agent';
|
||||
const textEl = document.createElement('div');
|
||||
textEl.className = 'work-hub-chat-body';
|
||||
textEl.textContent = '…';
|
||||
reply.appendChild(roleEl);
|
||||
reply.appendChild(textEl);
|
||||
log.appendChild(reply);
|
||||
log.scrollTop = log.scrollHeight;
|
||||
|
||||
let acc = '';
|
||||
send.disabled = true;
|
||||
miniChat.send({
|
||||
text,
|
||||
onDelta: (token) => {
|
||||
acc += token;
|
||||
textEl.textContent = acc;
|
||||
log.scrollTop = log.scrollHeight;
|
||||
},
|
||||
onComplete: (content) => {
|
||||
reply.classList.remove('work-hub-chat-streaming');
|
||||
if (content && content.trim()) textEl.textContent = content;
|
||||
else if (!acc) textEl.textContent = '(no response)';
|
||||
send.disabled = false;
|
||||
log.scrollTop = log.scrollHeight;
|
||||
},
|
||||
onError: (err) => {
|
||||
reply.classList.remove('work-hub-chat-streaming');
|
||||
reply.classList.add('work-hub-chat-error');
|
||||
textEl.textContent = '⚠ ' + ((err && err.message) || 'Could not reach agent.');
|
||||
send.disabled = false;
|
||||
log.scrollTop = log.scrollHeight;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
send.addEventListener('click', submit);
|
||||
ta.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); submit(); }
|
||||
});
|
||||
}
|
||||
|
||||
async function renderActivity() {
|
||||
if (!selectedCard) {
|
||||
p3.innerHTML = '<div class="work-hub-pane-head">Activity</div><div class="work-hub-empty">No card selected.</div>';
|
||||
return;
|
||||
}
|
||||
const meta = selectedCard.meta || {};
|
||||
const jobId = meta.job_id;
|
||||
p3.innerHTML = '<div class="work-hub-pane-head">Activity</div><div class="work-hub-empty">Loading…</div>';
|
||||
if (!jobId) {
|
||||
p3.innerHTML = '<div class="work-hub-pane-head">Activity</div><div class="work-hub-empty">No jobs linked to this card.</div>';
|
||||
return;
|
||||
}
|
||||
let job = null;
|
||||
try {
|
||||
const res = await fetch(`/api/jobs/${encodeURIComponent(jobId)}`);
|
||||
if (res.ok) job = await res.json();
|
||||
} catch (_) { /* offline */ }
|
||||
if (!job) {
|
||||
p3.innerHTML = '<div class="work-hub-pane-head">Activity</div><div class="work-hub-empty">Could not load job.</div>';
|
||||
return;
|
||||
}
|
||||
const logLines = (job.log || job.output || '').split('\n').slice(-50).join('\n');
|
||||
p3.innerHTML = `
|
||||
<div class="work-hub-pane-head">Activity</div>
|
||||
<div style="padding:14px">
|
||||
<div class="work-hub-job">
|
||||
<span class="work-hub-dot" style="background:${statusColor(job.status)}"></span>
|
||||
<span>${escapeHtml(job.id || jobId)}</span>
|
||||
<span class="db-text-dim" style="margin-left:auto;font-size:12px">${escapeHtml(job.status || '')}</span>
|
||||
</div>
|
||||
<div style="font-size:11px;color:var(--db-text-dim,#a1a1aa);margin:4px 0 10px">${job.updated_at ? relativeTime(job.updated_at) : ''}</div>
|
||||
<div class="work-hub-log">${escapeHtml(logLines) || 'No log output.'}</div>
|
||||
${meta.session_id ? `<div style="margin-top:10px"><a href="/traces?session_id=${encodeURIComponent(meta.session_id)}" style="color:var(--db-accent,#6366f1);font-size:12px">Open trace →</a></div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function selectCard(card) {
|
||||
selectedCard = card;
|
||||
if (card && (!activeTab)) activeTab = 'description';
|
||||
syncUrl();
|
||||
renderDetail();
|
||||
renderActivity();
|
||||
}
|
||||
|
||||
let pendingHandled = !pendingCardId;
|
||||
function tryDeepLink(data) {
|
||||
for (const col of data.columns || []) {
|
||||
for (const card of col.cards || []) {
|
||||
if (card.id === pendingCardId) { pendingHandled = true; selectCard(card); return; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const board = createInteractiveBoard(p1Body, {
|
||||
apiBase: '/api/kanban',
|
||||
eventsUrl: '/api/kanban/events',
|
||||
pollMs: 8000,
|
||||
debounceMs: 500,
|
||||
interactive: true,
|
||||
laneByProfile: true,
|
||||
boardSwitcher: true,
|
||||
boardState,
|
||||
onOpen: (card) => {
|
||||
pendingHandled = true;
|
||||
selectCard(card);
|
||||
},
|
||||
fetch: async () => {
|
||||
const [boardRes, boardsRes] = await Promise.all([
|
||||
fetch(`/api/kanban/board?board=${encodeURIComponent(boardState.id)}`),
|
||||
fetch('/api/kanban/boards'),
|
||||
]);
|
||||
if (!boardRes.ok) throw new Error(await boardRes.text());
|
||||
const data = await boardRes.json();
|
||||
if (boardsRes.ok) {
|
||||
const b = await boardsRes.json();
|
||||
data.boards = b.boards || [{ id: 'default', title: 'Default' }];
|
||||
}
|
||||
if (!pendingHandled && pendingCardId) tryDeepLink(data);
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
renderDetail();
|
||||
renderActivity();
|
||||
|
||||
return () => { teardownChat(); board.destroy(); };
|
||||
}
|
||||
@@ -49,13 +49,16 @@ templates:
|
||||
routes:
|
||||
- match: "/docs/**"
|
||||
template: docs
|
||||
"""
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
docs_dir = tmp_path / "docs"
|
||||
docs_dir.mkdir()
|
||||
(docs_dir / "index.md").write_text("# Welcome\n\nHello from integration test.")
|
||||
(docs_dir / "guide.md").write_text("# Guide\n\nA test guide.")
|
||||
(docs_dir / "index.md").write_text(
|
||||
"# Welcome\n\nHello from integration test.", encoding="utf-8"
|
||||
)
|
||||
(docs_dir / "guide.md").write_text("# Guide\n\nA test guide.", encoding="utf-8")
|
||||
|
||||
return tmp_path
|
||||
|
||||
@@ -94,7 +97,7 @@ class TestBuildCommand:
|
||||
["build", "--config", str(project_dir / "aiui.template.yaml"), "--output", str(output)],
|
||||
)
|
||||
|
||||
ui_config = json.loads((output / "ui-config.json").read_text())
|
||||
ui_config = json.loads((output / "ui-config.json").read_text(encoding="utf-8"))
|
||||
assert ui_config["site"]["title"] == "Integration Test Site"
|
||||
assert "header_main" in ui_config["components"]
|
||||
assert "footer_main" in ui_config["components"]
|
||||
@@ -111,12 +114,12 @@ class TestBuildCommand:
|
||||
],
|
||||
)
|
||||
|
||||
content = (output / "ui-config.json").read_text()
|
||||
content = (output / "ui-config.json").read_text(encoding="utf-8")
|
||||
assert "\n" not in content # Minified = no newlines
|
||||
|
||||
def test_build_invalid_config(self, tmp_path: Path):
|
||||
config = tmp_path / "bad.yaml"
|
||||
config.write_text("not: valid: yaml: config")
|
||||
config.write_text("not: valid: yaml: config", encoding="utf-8")
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["build", "--config", str(config), "--output", str(tmp_path / "out")],
|
||||
@@ -141,7 +144,8 @@ templates:
|
||||
routes:
|
||||
- match: "/docs/**"
|
||||
template: docs
|
||||
"""
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
result = runner.invoke(
|
||||
app,
|
||||
@@ -215,8 +219,8 @@ class TestFullPipeline:
|
||||
assert (output / "index.html").exists()
|
||||
|
||||
# Verify content
|
||||
ui_config = json.loads((output / "ui-config.json").read_text())
|
||||
ui_config = json.loads((output / "ui-config.json").read_text(encoding="utf-8"))
|
||||
assert ui_config["site"]["title"] == "Integration Test Site"
|
||||
|
||||
routes = json.loads((output / "route-manifest.json").read_text())
|
||||
routes = json.loads((output / "route-manifest.json").read_text(encoding="utf-8"))
|
||||
assert len(routes["routes"]) >= 1
|
||||
@@ -488,12 +488,16 @@ class TestTelemetryAPI:
|
||||
|
||||
class TestTracingAPI:
|
||||
def test_list_traces(self, client):
|
||||
r = client.get("/api/tracing")
|
||||
r = client.get("/api/traces")
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert isinstance(data, (list, dict))
|
||||
|
||||
def test_tracing_status(self, client):
|
||||
r = client.get("/api/tracing/status")
|
||||
r = client.get("/api/traces/status")
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert "status" in data or "feature" in data
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
@@ -516,9 +520,33 @@ class TestProtocolAPI:
|
||||
|
||||
class TestMarketplaceAPI:
|
||||
def test_list_marketplace(self, client):
|
||||
r = client.get("/api/marketplace")
|
||||
r = client.get("/api/marketplace/plugins")
|
||||
assert r.status_code == 200
|
||||
|
||||
def test_list_plugins(self, client):
|
||||
r = client.get("/api/marketplace/plugins")
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert "plugins" in data
|
||||
assert isinstance(data["plugins"], list)
|
||||
|
||||
def test_install_uninstall_roundtrip(self, client):
|
||||
r = client.post("/api/marketplace/install", json={"plugin_id": "web_search"})
|
||||
assert r.status_code == 200
|
||||
assert r.json()["status"] == "installed"
|
||||
|
||||
r = client.get("/api/marketplace/plugins")
|
||||
installed = [p for p in r.json()["plugins"] if p.get("installed")]
|
||||
assert any(p["id"] == "web_search" for p in installed)
|
||||
|
||||
r = client.post("/api/marketplace/uninstall", json={"plugin_id": "web_search"})
|
||||
assert r.status_code == 200
|
||||
assert r.json()["status"] == "uninstalled"
|
||||
|
||||
def test_install_unknown_plugin(self, client):
|
||||
r = client.post("/api/marketplace/install", json={"plugin_id": "does_not_exist"})
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Memory (from original tests, kept for completeness)
|
||||
@@ -747,6 +775,14 @@ class TestPagesAPI:
|
||||
assert r.status_code == 200
|
||||
assert "servers" in r.json()
|
||||
|
||||
def test_marketplace_page_registered(self, client):
|
||||
r = client.get("/api/pages")
|
||||
pages = r.json().get("pages", r.json())
|
||||
mkt = next((p for p in pages if p["id"] == "marketplace"), None)
|
||||
assert mkt is not None, "Marketplace page should be registered"
|
||||
assert mkt["group"] == "Agent"
|
||||
assert mkt["api_endpoint"] == "/api/marketplace/plugins"
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Persistence across restart
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Unit tests for ADP JSON Schema validation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
from jsonschema import Draft202012Validator, ValidationError
|
||||
|
||||
ADP_ROOT = Path(__file__).resolve().parents[2] / "docs" / "protocols" / "adp"
|
||||
SCHEMA_DIR = ADP_ROOT / "schema"
|
||||
EXAMPLES_DIR = ADP_ROOT / "examples"
|
||||
CATALOGS_DIR = ADP_ROOT / "catalogs"
|
||||
|
||||
|
||||
def _load_json(path: Path) -> dict:
|
||||
return json.loads(path.read_text())
|
||||
|
||||
|
||||
def _load_yaml(path: Path) -> dict:
|
||||
return yaml.safe_load(path.read_text())
|
||||
|
||||
|
||||
def _application_validator() -> Draft202012Validator:
|
||||
schema = _load_json(SCHEMA_DIR / "application.schema.json")
|
||||
return Draft202012Validator(schema)
|
||||
|
||||
|
||||
def _catalog_validator() -> Draft202012Validator:
|
||||
schema = _load_json(SCHEMA_DIR / "component-catalog.schema.json")
|
||||
return Draft202012Validator(schema)
|
||||
|
||||
|
||||
def _theme_validator() -> Draft202012Validator:
|
||||
schema = _load_json(SCHEMA_DIR / "theme.schema.json")
|
||||
return Draft202012Validator(schema)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"schema_path",
|
||||
[
|
||||
SCHEMA_DIR / "application.schema.json",
|
||||
SCHEMA_DIR / "component-catalog.schema.json",
|
||||
SCHEMA_DIR / "theme.schema.json",
|
||||
],
|
||||
)
|
||||
def test_schema_files_are_valid_meta_schema(schema_path: Path) -> None:
|
||||
schema = _load_json(schema_path)
|
||||
Draft202012Validator.check_schema(schema)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"example_name",
|
||||
["minimal.application.yaml", "docs-site.application.yaml", "landing-page.application.yaml"],
|
||||
)
|
||||
def test_examples_validate_against_application_schema(example_name: str) -> None:
|
||||
doc = _load_yaml(EXAMPLES_DIR / example_name)
|
||||
_application_validator().validate(doc)
|
||||
|
||||
|
||||
def test_default_catalog_validates_against_catalog_schema() -> None:
|
||||
catalog = _load_json(CATALOGS_DIR / "default.catalog.json")
|
||||
_catalog_validator().validate(catalog)
|
||||
|
||||
|
||||
def test_theme_mutual_exclusion_r27() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
_theme_validator().validate({"preset": "zinc", "tokens": "./tokens.json"})
|
||||
|
||||
|
||||
def test_theme_preset_only_valid() -> None:
|
||||
_theme_validator().validate({"preset": "zinc", "radius": "md", "darkMode": True})
|
||||
|
||||
|
||||
def test_x_adp_extension_accepted_at_root() -> None:
|
||||
doc = _load_yaml(EXAMPLES_DIR / "minimal.application.yaml")
|
||||
doc["x-adp-custom"] = True
|
||||
_application_validator().validate(doc)
|
||||
|
||||
|
||||
def test_template_requires_slots_or_zones() -> None:
|
||||
doc = _load_yaml(EXAMPLES_DIR / "minimal.application.yaml")
|
||||
doc["spec"]["templates"]["docs"] = {"layout": "TwoColumnLayout"}
|
||||
with pytest.raises(ValidationError):
|
||||
_application_validator().validate(doc)
|
||||
@@ -305,7 +305,7 @@ class TestAudioSessionManagement:
|
||||
session = _audio_sessions[session_id]
|
||||
assert session["chunks_received"] == 1
|
||||
assert session["bytes_received"] == len(test_data)
|
||||
assert session["last_chunk_at"] > session["started_at"]
|
||||
assert session["last_chunk_at"] >= session["started_at"]
|
||||
|
||||
# Check global stats were updated
|
||||
assert _audio_stats["total_chunks"] == 1
|
||||
|
||||
@@ -224,25 +224,37 @@ def test_constant_time_comparison():
|
||||
|
||||
|
||||
def test_timing_attack_resistance():
|
||||
"""Test timing attack resistance - all incorrect tokens take similar time."""
|
||||
"""Test timing attack resistance - all incorrect tokens take similar time.
|
||||
|
||||
Timing is inherently noisy: a single sample per token trips on scheduler
|
||||
spikes when the full suite runs under CPU contention (issue #256). We take
|
||||
several samples per token and compare the *median* time, which is resistant
|
||||
to transient spikes, so the test measures the constant-time property rather
|
||||
than system load.
|
||||
"""
|
||||
correct = "secret123456789012345678901234567890"
|
||||
wrong_same_length = "wrongg123456789012345678901234567890"
|
||||
wrong_diff_length = "wrong"
|
||||
tokens = [wrong_same_length, wrong_diff_length, ""]
|
||||
|
||||
# Measure times for different wrong tokens
|
||||
times = []
|
||||
for wrong_token in [wrong_same_length, wrong_diff_length, ""]:
|
||||
start = time.perf_counter()
|
||||
for _ in range(1000): # Multiple iterations for better timing measurement
|
||||
_constant_time_eq(correct, wrong_token)
|
||||
end = time.perf_counter()
|
||||
times.append(end - start)
|
||||
def _median_time(wrong_token: str) -> float:
|
||||
samples = []
|
||||
for _ in range(7):
|
||||
start = time.perf_counter()
|
||||
for _ in range(1000):
|
||||
_constant_time_eq(correct, wrong_token)
|
||||
samples.append(time.perf_counter() - start)
|
||||
samples.sort()
|
||||
return samples[len(samples) // 2]
|
||||
|
||||
# Times should be similar (within reasonable variance)
|
||||
# This is a best-effort test - timing can be affected by system load
|
||||
# Warm up so JIT/cache effects do not skew the first token's samples.
|
||||
_median_time(correct)
|
||||
|
||||
times = [_median_time(token) for token in tokens]
|
||||
|
||||
# Median times should be similar (all wrong tokens take comparable time).
|
||||
avg_time = sum(times) / len(times)
|
||||
for t in times:
|
||||
# Allow up to 50% variance due to system noise
|
||||
assert abs(t - avg_time) / avg_time < 0.5, f"Timing variance too high: {times}"
|
||||
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ EXPLORER_VIEW = _FRONTEND / "views" / "explorer.js"
|
||||
|
||||
|
||||
def _view() -> str:
|
||||
return CHANNELS_VIEW.read_text()
|
||||
return CHANNELS_VIEW.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
class TestAddChannelControls:
|
||||
@@ -91,6 +91,6 @@ class TestEmptyState:
|
||||
|
||||
class TestExplorerPreset:
|
||||
def test_explorer_has_add_channel_post(self):
|
||||
src = EXPLORER_VIEW.read_text()
|
||||
src = EXPLORER_VIEW.read_text(encoding="utf-8")
|
||||
assert "Add Channel" in src
|
||||
assert "env:TELEGRAM_BOT_TOKEN" in src
|
||||
@@ -25,13 +25,13 @@ CHAT_JS = FRONTEND / "views" / "chat.js"
|
||||
|
||||
class TestSelectPageShellError:
|
||||
def test_shell_error_helper_defined(self):
|
||||
src = DASHBOARD_JS.read_text()
|
||||
src = DASHBOARD_JS.read_text(encoding="utf-8")
|
||||
assert "function showDashboardInitError(" in src
|
||||
assert "'alert'" in src
|
||||
assert "db-error" in src
|
||||
|
||||
def test_select_page_calls_shell_error_on_missing_main(self):
|
||||
src = DASHBOARD_JS.read_text()
|
||||
src = DASHBOARD_JS.read_text(encoding="utf-8")
|
||||
idx = src.find("const main = document.getElementById('db-main-content')")
|
||||
assert idx != -1
|
||||
region = src[idx:idx + 400]
|
||||
@@ -41,16 +41,16 @@ class TestSelectPageShellError:
|
||||
|
||||
class TestChatOfflineMessaging:
|
||||
def test_toast_imported(self):
|
||||
src = CHAT_JS.read_text()
|
||||
src = CHAT_JS.read_text(encoding="utf-8")
|
||||
assert "import { showToast } from '../toast.js';" in src
|
||||
|
||||
def test_offline_hint_in_fallback(self):
|
||||
src = CHAT_JS.read_text()
|
||||
src = CHAT_JS.read_text(encoding="utf-8")
|
||||
assert "navigator.onLine === false" in src
|
||||
assert "You are offline. " in src
|
||||
|
||||
def test_fallback_catch_shows_toast(self):
|
||||
src = CHAT_JS.read_text()
|
||||
src = CHAT_JS.read_text(encoding="utf-8")
|
||||
idx = src.find("fetch('/api/chat/send'")
|
||||
assert idx != -1
|
||||
region = src[idx:idx + 900]
|
||||
@@ -62,7 +62,7 @@ class TestChatOfflineMessaging:
|
||||
|
||||
class TestSystemErrorRoleAlert:
|
||||
def test_append_message_sets_role_alert(self):
|
||||
src = CHAT_JS.read_text()
|
||||
src = CHAT_JS.read_text(encoding="utf-8")
|
||||
idx = src.find("function appendMessage(")
|
||||
assert idx != -1
|
||||
region = src[idx:idx + 700]
|
||||
@@ -72,7 +72,7 @@ class TestSystemErrorRoleAlert:
|
||||
|
||||
class TestWebSocketSendGuard:
|
||||
def test_ws_send_wrapped_in_try_catch(self):
|
||||
src = CHAT_JS.read_text()
|
||||
src = CHAT_JS.read_text(encoding="utf-8")
|
||||
idx = src.find("ws.send(JSON.stringify(payload))")
|
||||
assert idx != -1
|
||||
before = src[max(0, idx - 120):idx]
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Tests for the chat prefill-composer listener lifecycle (issue #209).
|
||||
|
||||
The 'aiui:prefill-composer' listener must be registered as a module-scope
|
||||
singleton and torn down in cleanup(), so repeated SPA navigations between
|
||||
/code-studio and /chat do not stack duplicate handlers on window.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
VIEWS = Path("src/praisonaiui/templates/frontend/plugins/views")
|
||||
|
||||
|
||||
class TestChatPrefillListenerLifecycle:
|
||||
def test_module_scope_handler_declared(self):
|
||||
src = (VIEWS / "chat.js").read_text()
|
||||
assert "let prefillHandler = null;" in src
|
||||
|
||||
def test_ensure_prefill_listener_guard(self):
|
||||
src = (VIEWS / "chat.js").read_text()
|
||||
assert "function ensurePrefillListener()" in src
|
||||
assert "if (prefillHandler) return;" in src
|
||||
|
||||
def test_render_uses_singleton_not_inline_addlistener(self):
|
||||
src = (VIEWS / "chat.js").read_text()
|
||||
assert "ensurePrefillListener();" in src
|
||||
# The listener is added exactly once, inside the guard helper.
|
||||
assert src.count("window.addEventListener('aiui:prefill-composer'") == 1
|
||||
|
||||
def test_cleanup_removes_listener(self):
|
||||
src = (VIEWS / "chat.js").read_text()
|
||||
assert "window.removeEventListener('aiui:prefill-composer', prefillHandler)" in src
|
||||
assert src.count("prefillHandler = null;") >= 2
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Tests for the Code Studio view (STITCH-006)."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from praisonaiui.server import create_app
|
||||
|
||||
VIEWS = Path("src/praisonaiui/templates/frontend/plugins/views")
|
||||
PLUGINS = Path("src/praisonaiui/templates/frontend/plugins")
|
||||
|
||||
|
||||
class TestCodeStudioPage:
|
||||
def test_page_registered(self):
|
||||
client = TestClient(create_app())
|
||||
r = client.get("/api/pages")
|
||||
assert r.status_code == 200
|
||||
ids = {p["id"] for p in r.json()["pages"]}
|
||||
assert "code-studio" in ids
|
||||
|
||||
def test_page_metadata(self):
|
||||
client = TestClient(create_app())
|
||||
pages = {p["id"]: p for p in client.get("/api/pages").json()["pages"]}
|
||||
page = pages["code-studio"]
|
||||
assert page["title"] == "Code Studio"
|
||||
assert page["api_endpoint"] == "/api/code/languages"
|
||||
|
||||
|
||||
class TestCodeExecutionApi:
|
||||
def test_languages(self):
|
||||
client = TestClient(create_app())
|
||||
r = client.get("/api/code/languages")
|
||||
assert r.status_code == 200
|
||||
d = r.json()
|
||||
assert "languages" in d and "count" in d
|
||||
|
||||
def test_execute(self):
|
||||
client = TestClient(create_app())
|
||||
r = client.post(
|
||||
"/api/code/execute",
|
||||
json={"code": "print('hi')", "language": "python", "timeout": 30},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
d = r.json()
|
||||
assert "status" in d and "language" in d
|
||||
|
||||
|
||||
class TestCodeStudioFrontend:
|
||||
def test_view_file_exists(self):
|
||||
assert (VIEWS / "code-studio.js").is_file()
|
||||
|
||||
def test_view_exports_render(self):
|
||||
src = (VIEWS / "code-studio.js").read_text(encoding="utf-8")
|
||||
assert "export async function render" in src
|
||||
assert "export function cleanup" in src
|
||||
|
||||
def test_view_calls_execute_api(self):
|
||||
src = (VIEWS / "code-studio.js").read_text(encoding="utf-8")
|
||||
assert "/api/code/execute" in src
|
||||
assert "/api/code/languages" in src
|
||||
|
||||
def test_no_browser_eval(self):
|
||||
src = (VIEWS / "code-studio.js").read_text(encoding="utf-8")
|
||||
assert "eval(" not in src
|
||||
|
||||
def test_registered_in_dashboard(self):
|
||||
src = (PLUGINS / "dashboard.js").read_text(encoding="utf-8")
|
||||
assert "'code-studio'" in src
|
||||
assert "code-studio.js" in src
|
||||
|
||||
def test_send_to_agent_event(self):
|
||||
src = (VIEWS / "code-studio.js").read_text(encoding="utf-8")
|
||||
assert "aiui:prefill-composer" in src
|
||||
|
||||
def test_chat_has_prefill_listener(self):
|
||||
src = (VIEWS / "chat.js").read_text(encoding="utf-8")
|
||||
assert "aiui:prefill-composer" in src
|
||||
@@ -22,7 +22,7 @@ DASHBOARD_JS = FRONTEND / "dashboard.js"
|
||||
|
||||
class TestCommandPalette:
|
||||
def _src(self) -> str:
|
||||
return DASHBOARD_JS.read_text()
|
||||
return DASHBOARD_JS.read_text(encoding="utf-8")
|
||||
|
||||
def test_open_command_palette_exposed_globally(self):
|
||||
src = self._src()
|
||||
|
||||
+19
-19
@@ -86,7 +86,7 @@ class TestCompilerUiConfig:
|
||||
result = compiler.compile(tmp_path / "output")
|
||||
assert result.success is True
|
||||
|
||||
ui_config = json.loads((tmp_path / "output" / "ui-config.json").read_text())
|
||||
ui_config = json.loads((tmp_path / "output" / "ui-config.json").read_text(encoding="utf-8"))
|
||||
assert ui_config["site"]["title"] == "Test Site"
|
||||
assert "templates" in ui_config
|
||||
assert "docs" in ui_config["templates"]
|
||||
@@ -96,7 +96,7 @@ class TestCompilerUiConfig:
|
||||
result = compiler.compile(tmp_path / "output")
|
||||
assert result.success is True
|
||||
|
||||
ui_config = json.loads((tmp_path / "output" / "ui-config.json").read_text())
|
||||
ui_config = json.loads((tmp_path / "output" / "ui-config.json").read_text(encoding="utf-8"))
|
||||
assert "header_main" in ui_config["components"]
|
||||
assert ui_config["components"]["header_main"]["props"]["logoText"] == "MySite"
|
||||
assert "footer_main" in ui_config["components"]
|
||||
@@ -107,7 +107,7 @@ class TestCompilerUiConfig:
|
||||
result = compiler.compile(tmp_path / "output")
|
||||
assert result.success is True
|
||||
|
||||
ui_config = json.loads((tmp_path / "output" / "ui-config.json").read_text())
|
||||
ui_config = json.loads((tmp_path / "output" / "ui-config.json").read_text(encoding="utf-8"))
|
||||
docs_template = ui_config["templates"]["docs"]
|
||||
assert docs_template["slots"]["header"]["ref"] == "header_main"
|
||||
assert docs_template["slots"]["footer"]["ref"] == "footer_main"
|
||||
@@ -123,7 +123,7 @@ class TestCompilerRouteManifest:
|
||||
assert "route-manifest.json" in result.files
|
||||
|
||||
manifest = json.loads(
|
||||
(tmp_path / "output" / "route-manifest.json").read_text()
|
||||
(tmp_path / "output" / "route-manifest.json").read_text(encoding="utf-8")
|
||||
)
|
||||
assert len(manifest["routes"]) == 1
|
||||
assert manifest["routes"][0]["pattern"] == "/docs/**"
|
||||
@@ -147,7 +147,7 @@ class TestCompilerRouteManifest:
|
||||
assert result.success is True
|
||||
|
||||
manifest = json.loads(
|
||||
(tmp_path / "output" / "route-manifest.json").read_text()
|
||||
(tmp_path / "output" / "route-manifest.json").read_text(encoding="utf-8")
|
||||
)
|
||||
# First route in list gets highest priority
|
||||
assert manifest["routes"][0]["priority"] > manifest["routes"][1]["priority"]
|
||||
@@ -200,7 +200,7 @@ class TestCompilerOutputFiles:
|
||||
compiler = Compiler(minimal_config, base_path=tmp_path)
|
||||
compiler.compile(tmp_path / "output", minify=True)
|
||||
|
||||
content = (tmp_path / "output" / "ui-config.json").read_text()
|
||||
content = (tmp_path / "output" / "ui-config.json").read_text(encoding="utf-8")
|
||||
# Minified JSON should not have newlines
|
||||
assert "\n" not in content
|
||||
|
||||
@@ -208,7 +208,7 @@ class TestCompilerOutputFiles:
|
||||
compiler = Compiler(minimal_config, base_path=tmp_path)
|
||||
compiler.compile(tmp_path / "output", minify=False)
|
||||
|
||||
content = (tmp_path / "output" / "ui-config.json").read_text()
|
||||
content = (tmp_path / "output" / "ui-config.json").read_text(encoding="utf-8")
|
||||
# Non-minified JSON should have newlines
|
||||
assert "\n" in content
|
||||
|
||||
@@ -228,7 +228,7 @@ class TestCompilerDocsContent:
|
||||
result = compiler.compile(tmp_path / "output")
|
||||
assert "docs-nav.json" in result.files
|
||||
|
||||
nav = json.loads((tmp_path / "output" / "docs-nav.json").read_text())
|
||||
nav = json.loads((tmp_path / "output" / "docs-nav.json").read_text(encoding="utf-8"))
|
||||
assert "items" in nav
|
||||
|
||||
def test_no_docs_nav_without_content(self, minimal_config: Config, tmp_path: Path):
|
||||
@@ -291,7 +291,7 @@ class TestCompilerThemeCss:
|
||||
assert result.success is True
|
||||
theme_css = tmp_path / "output" / "assets" / "theme.css"
|
||||
assert theme_css.exists(), "assets/theme.css must be generated"
|
||||
content = theme_css.read_text()
|
||||
content = theme_css.read_text(encoding="utf-8")
|
||||
assert ":root {" in content
|
||||
assert "--radius:" in content
|
||||
|
||||
@@ -340,7 +340,7 @@ class TestInjectThemeCss:
|
||||
inject_theme_css(tmp_path, preset="zinc", dark_mode=True, radius="0.5rem")
|
||||
theme_file = tmp_path / "assets" / "theme.css"
|
||||
assert theme_file.exists()
|
||||
content = theme_file.read_text()
|
||||
content = theme_file.read_text(encoding="utf-8")
|
||||
assert ":root {" in content
|
||||
|
||||
def test_creates_assets_dir(self, tmp_path: Path):
|
||||
@@ -367,7 +367,7 @@ class TestThemePipelineIntegration:
|
||||
assert result.success is True
|
||||
assert "assets/theme.css" in result.files
|
||||
|
||||
theme_css = (tmp_path / "output" / "assets" / "theme.css").read_text()
|
||||
theme_css = (tmp_path / "output" / "assets" / "theme.css").read_text(encoding="utf-8")
|
||||
assert ":root {" in theme_css
|
||||
assert ".dark {" in theme_css
|
||||
assert "--radius: 1rem" in theme_css # xl = 1rem
|
||||
@@ -386,7 +386,7 @@ class TestThemePipelineIntegration:
|
||||
result = compiler.compile(tmp_path / "output")
|
||||
assert result.success is True
|
||||
|
||||
theme_css = (tmp_path / "output" / "assets" / "theme.css").read_text()
|
||||
theme_css = (tmp_path / "output" / "assets" / "theme.css").read_text(encoding="utf-8")
|
||||
assert ":root {" in theme_css
|
||||
assert ".dark" not in theme_css
|
||||
|
||||
@@ -405,7 +405,7 @@ class TestThemePipelineIntegration:
|
||||
assert result.success is True
|
||||
|
||||
ui_config = json.loads(
|
||||
(tmp_path / "output" / "ui-config.json").read_text()
|
||||
(tmp_path / "output" / "ui-config.json").read_text(encoding="utf-8")
|
||||
)
|
||||
theme = ui_config["site"]["theme"]
|
||||
assert theme["preset"] == "blue"
|
||||
@@ -426,7 +426,7 @@ class TestThemePipelineIntegration:
|
||||
|
||||
theme_css_path = tmp_path / "output" / "assets" / "theme.css"
|
||||
assert theme_css_path.exists(), "theme.css must always be generated"
|
||||
content = theme_css_path.read_text()
|
||||
content = theme_css_path.read_text(encoding="utf-8")
|
||||
assert ":root {" in content
|
||||
assert "--radius:" in content
|
||||
|
||||
@@ -444,7 +444,7 @@ class TestThemePipelineIntegration:
|
||||
result = compiler.compile(tmp_path / "output")
|
||||
assert result.success is True
|
||||
|
||||
index_html = (tmp_path / "output" / "index.html").read_text()
|
||||
index_html = (tmp_path / "output" / "index.html").read_text(encoding="utf-8")
|
||||
assert "classList.add('dark')" not in index_html
|
||||
|
||||
def test_dark_mode_antiflicker_has_dark_class(self, tmp_path: Path):
|
||||
@@ -461,7 +461,7 @@ class TestThemePipelineIntegration:
|
||||
result = compiler.compile(tmp_path / "output")
|
||||
assert result.success is True
|
||||
|
||||
index_html = (tmp_path / "output" / "index.html").read_text()
|
||||
index_html = (tmp_path / "output" / "index.html").read_text(encoding="utf-8")
|
||||
assert "classList.add('dark')" in index_html
|
||||
|
||||
|
||||
@@ -514,7 +514,7 @@ class TestCompositionResolver:
|
||||
|
||||
# Load ui-config.json and verify auto-bridging
|
||||
ui_config_path = tmp_path / "output" / "ui-config.json"
|
||||
ui_config = json.loads(ui_config_path.read_text())
|
||||
ui_config = json.loads(ui_config_path.read_text(encoding="utf-8"))
|
||||
|
||||
docs_template = ui_config["templates"]["docs"]
|
||||
assert "zones" in docs_template
|
||||
@@ -556,7 +556,7 @@ class TestCompositionResolver:
|
||||
|
||||
# Load ui-config.json and verify no auto-bridging occurred
|
||||
ui_config_path = tmp_path / "output" / "ui-config.json"
|
||||
ui_config = json.loads(ui_config_path.read_text())
|
||||
ui_config = json.loads(ui_config_path.read_text(encoding="utf-8"))
|
||||
|
||||
docs_template = ui_config["templates"]["docs"]
|
||||
# Should not have zones for non-FlexibleLayout
|
||||
@@ -589,7 +589,7 @@ class TestCompositionResolver:
|
||||
|
||||
# Load ui-config.json and verify no duplicate components
|
||||
ui_config_path = tmp_path / "output" / "ui-config.json"
|
||||
ui_config = json.loads(ui_config_path.read_text())
|
||||
ui_config = json.loads(ui_config_path.read_text(encoding="utf-8"))
|
||||
|
||||
docs_template = ui_config["templates"]["docs"]
|
||||
zones = docs_template["zones"]
|
||||
|
||||
@@ -30,7 +30,7 @@ class TestCompilerChatOutput:
|
||||
result = compiler.compile(tmp_path)
|
||||
|
||||
assert result.success
|
||||
ui_config = json.loads((tmp_path / "ui-config.json").read_text())
|
||||
ui_config = json.loads((tmp_path / "ui-config.json").read_text(encoding="utf-8"))
|
||||
assert ui_config["style"] == "chat"
|
||||
|
||||
def test_layout_in_ui_config(self, tmp_path: Path):
|
||||
@@ -43,7 +43,7 @@ class TestCompilerChatOutput:
|
||||
result = compiler.compile(tmp_path)
|
||||
|
||||
assert result.success
|
||||
ui_config = json.loads((tmp_path / "ui-config.json").read_text())
|
||||
ui_config = json.loads((tmp_path / "ui-config.json").read_text(encoding="utf-8"))
|
||||
assert ui_config["layout"]["mode"] == "bottom-right"
|
||||
assert ui_config["layout"]["width"] == "400px"
|
||||
assert ui_config["layout"]["height"] == "600px"
|
||||
@@ -73,7 +73,7 @@ class TestCompilerChatOutput:
|
||||
result = compiler.compile(tmp_path)
|
||||
|
||||
assert result.success
|
||||
ui_config = json.loads((tmp_path / "ui-config.json").read_text())
|
||||
ui_config = json.loads((tmp_path / "ui-config.json").read_text(encoding="utf-8"))
|
||||
assert ui_config["chat"]["enabled"] is True
|
||||
assert ui_config["chat"]["name"] == "AI Assistant"
|
||||
assert len(ui_config["chat"]["starters"]) == 1
|
||||
@@ -96,7 +96,7 @@ class TestCompilerChatOutput:
|
||||
result = compiler.compile(tmp_path)
|
||||
|
||||
assert result.success
|
||||
ui_config = json.loads((tmp_path / "ui-config.json").read_text())
|
||||
ui_config = json.loads((tmp_path / "ui-config.json").read_text(encoding="utf-8"))
|
||||
assert ui_config["auth"]["enabled"] is True
|
||||
assert "password" in ui_config["auth"]["providers"]
|
||||
assert "google" in ui_config["auth"]["providers"]
|
||||
@@ -128,7 +128,7 @@ class TestCompilerChatOutput:
|
||||
result = compiler.compile(tmp_path)
|
||||
|
||||
assert result.success
|
||||
ui_config = json.loads((tmp_path / "ui-config.json").read_text())
|
||||
ui_config = json.loads((tmp_path / "ui-config.json").read_text(encoding="utf-8"))
|
||||
assert len(ui_config["widgets"]) == 2
|
||||
assert ui_config["widgets"][0]["type"] == "slider"
|
||||
assert ui_config["widgets"][0]["name"] == "temperature"
|
||||
|
||||
@@ -24,7 +24,7 @@ DASHBOARD_JS = (
|
||||
@pytest.fixture
|
||||
def dashboard_source():
|
||||
"""Read dashboard.js source."""
|
||||
return DASHBOARD_JS.read_text()
|
||||
return DASHBOARD_JS.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
class TestComponentRegistry:
|
||||
@@ -68,7 +68,7 @@ class TestPythonJSComponentParity:
|
||||
Path(__file__).resolve().parents[2]
|
||||
/ "src" / "praisonaiui" / "ui.py"
|
||||
)
|
||||
source = ui_py.read_text()
|
||||
source = ui_py.read_text(encoding="utf-8")
|
||||
|
||||
# Extract all "type": "xxx" patterns from ui.py
|
||||
py_types = set(re.findall(r'"type":\s*"(\w+)"', source))
|
||||
|
||||
Loaded 100 of 129 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user