From 123155ac0f8aeb7ca5d0c2518f795221d4ab87dc Mon Sep 17 00:00:00 2001 From: Test Bot Date: Fri, 17 Apr 2026 03:30:04 +0000 Subject: [PATCH] feat(dashboard): guide users to relevant docs --- .../src/components/common/DocsLinkAnchor.tsx | 36 +++ dashboard/src/components/common/HelpTip.tsx | 30 +- .../src/components/common/PageDocsLinks.tsx | 34 +++ dashboard/src/components/ide/IdeHeader.tsx | 13 + dashboard/src/components/layout/Topbar.tsx | 21 +- .../webhooks/ValidationIssuesAlert.tsx | 10 + .../webhooks/WebhooksPageHeader.tsx | 8 +- dashboard/src/lib/docsLinks.ts | 259 ++++++++++++++++++ dashboard/src/pages/SettingsPage.tsx | 15 +- dashboard/src/pages/SetupPage.tsx | 39 ++- dashboard/src/pages/SkillsPage.tsx | 4 + dashboard/src/pages/settings/ModelsTab.tsx | 97 +++++-- .../pages/settings/SettingsCatalogView.tsx | 31 ++- .../pages/settings/SettingsIntentCards.tsx | 108 ++++++++ .../src/pages/skills/LocalSkillsPanel.tsx | 17 +- 15 files changed, 666 insertions(+), 56 deletions(-) create mode 100644 dashboard/src/components/common/DocsLinkAnchor.tsx create mode 100644 dashboard/src/components/common/PageDocsLinks.tsx create mode 100644 dashboard/src/lib/docsLinks.ts create mode 100644 dashboard/src/pages/settings/SettingsIntentCards.tsx diff --git a/dashboard/src/components/common/DocsLinkAnchor.tsx b/dashboard/src/components/common/DocsLinkAnchor.tsx new file mode 100644 index 000000000..589cfabdb --- /dev/null +++ b/dashboard/src/components/common/DocsLinkAnchor.tsx @@ -0,0 +1,36 @@ +import type { AnchorHTMLAttributes, ReactElement, ReactNode } from 'react'; +import { FiExternalLink } from 'react-icons/fi'; + +import type { DocLink } from '../../lib/docsLinks'; +import { cn } from '../../lib/utils'; + +const BUTTON_CLASS_NAME = 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xl border border-border/90 bg-card/80 px-3 py-2 text-xs font-semibold text-foreground shadow-soft transition-all duration-200 hover:border-primary/40 hover:bg-card'; +const TEXT_CLASS_NAME = 'inline-flex items-center gap-1 text-sm font-semibold text-primary underline-offset-4 hover:underline'; + +export interface DocsLinkAnchorProps extends Omit, 'href'> { + doc: DocLink; + children?: ReactNode; + appearance?: 'button' | 'text'; +} + +export function DocsLinkAnchor({ + doc, + children, + appearance = 'button', + className, + ...props +}: DocsLinkAnchorProps): ReactElement { + return ( + + {children ?? doc.shortLabel} + + ); +} diff --git a/dashboard/src/components/common/HelpTip.tsx b/dashboard/src/components/common/HelpTip.tsx index 4bcc123fb..6faa042ab 100644 --- a/dashboard/src/components/common/HelpTip.tsx +++ b/dashboard/src/components/common/HelpTip.tsx @@ -1,11 +1,17 @@ import type { ReactElement } from 'react'; -import { FiHelpCircle } from 'react-icons/fi'; +import { FiExternalLink, FiHelpCircle } from 'react-icons/fi'; -interface HelpTipProps { +export interface HelpTipProps { text: string; + href?: string; + linkLabel?: string; } -export default function HelpTip({ text }: HelpTipProps): ReactElement { +export default function HelpTip({ text, href, linkLabel }: HelpTipProps): ReactElement { + const tooltipClassName = href != null + ? 'absolute bottom-[calc(100%+0.5rem)] left-1/2 z-30 hidden w-64 -translate-x-1/2 rounded-2xl border border-border/80 bg-card/95 px-3 py-2 text-xs leading-5 text-card-foreground shadow-2xl backdrop-blur-sm group-hover:block group-focus-within:block' + : 'pointer-events-none absolute bottom-[calc(100%+0.5rem)] left-1/2 z-30 hidden w-56 -translate-x-1/2 rounded-2xl border border-border/80 bg-card/95 px-3 py-2 text-xs leading-5 text-card-foreground shadow-2xl backdrop-blur-sm group-hover:block group-focus-within:block'; + return ( - - {text} + + {text} + {href != null && ( + + {linkLabel ?? 'Open docs'} + + )} ); diff --git a/dashboard/src/components/common/PageDocsLinks.tsx b/dashboard/src/components/common/PageDocsLinks.tsx new file mode 100644 index 000000000..ced1c6862 --- /dev/null +++ b/dashboard/src/components/common/PageDocsLinks.tsx @@ -0,0 +1,34 @@ +import type { ReactElement } from 'react'; + +import type { DocLink } from '../../lib/docsLinks'; +import { cn } from '../../lib/utils'; +import { DocsLinkAnchor } from './DocsLinkAnchor'; + +export interface PageDocsLinksProps { + title?: string; + docs: DocLink[]; + className?: string; +} + +export function PageDocsLinks({ + title = 'Relevant docs', + docs, + className, +}: PageDocsLinksProps): ReactElement | null { + if (docs.length === 0) { + return null; + } + + return ( +
+
+ {title} +
+
+ {docs.map((doc) => ( + + ))} +
+
+ ); +} diff --git a/dashboard/src/components/ide/IdeHeader.tsx b/dashboard/src/components/ide/IdeHeader.tsx index 9be578041..f8878b98c 100644 --- a/dashboard/src/components/ide/IdeHeader.tsx +++ b/dashboard/src/components/ide/IdeHeader.tsx @@ -1,5 +1,7 @@ import type { ReactElement } from 'react'; import { FiCommand, FiFolder, FiMinus, FiPlus, FiSave } from 'react-icons/fi'; +import { PageDocsLinks } from '../common/PageDocsLinks'; +import { type DocLink, getDocLinks } from '../../lib/docsLinks'; import { Badge } from '../ui/badge'; import { Button } from '../ui/button'; @@ -17,6 +19,14 @@ export interface IdeHeaderProps { onDecreaseSidebarWidth: () => void; } +function resolveIdeDocs(activeFileLabel: string | null): DocLink[] { + const normalizedLabel = activeFileLabel?.toLowerCase() ?? ''; + if (normalizedLabel.endsWith('skill.md') || normalizedLabel.includes('/skills/')) { + return getDocLinks(['skills', 'mcp', 'dashboard']); + } + return getDocLinks(['dashboard', 'skills']); +} + export function IdeHeader({ activeFileLabel, isMobileLayout, @@ -30,6 +40,8 @@ export function IdeHeader({ onIncreaseSidebarWidth, onDecreaseSidebarWidth, }: IdeHeaderProps): ReactElement { + const ideDocs = resolveIdeDocs(activeFileLabel); + return (
@@ -37,6 +49,7 @@ export function IdeHeader({

Open a file, make a quick change, and save it back to the workspace.

+ {isMobileLayout && (
Current file diff --git a/dashboard/src/components/layout/Topbar.tsx b/dashboard/src/components/layout/Topbar.tsx index cc1173c8e..231b0ad6e 100644 --- a/dashboard/src/components/layout/Topbar.tsx +++ b/dashboard/src/components/layout/Topbar.tsx @@ -1,4 +1,4 @@ -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { useAuthStore } from '../../store/authStore'; import { useChatRuntimeStore } from '../../store/chatRuntimeStore'; import { useChatSessionStore } from '../../store/chatSessionStore'; @@ -7,8 +7,9 @@ import { useSidebarStore } from '../../store/sidebarStore'; import { useBackgroundSystemUpdateCheck } from '../../hooks/useBackgroundSystemUpdateCheck'; import { useSystemUpdateStatus } from '../../hooks/useSystem'; import { logout } from '../../api/auth'; +import { getPrimaryDocForPath } from '../../lib/docsLinks'; import { type TopbarUpdateNotice, getTopbarUpdateNotice } from '../../utils/systemUpdateUi'; -import { FiArrowUpCircle, FiLogOut, FiMenu, FiMoon, FiSun } from 'react-icons/fi'; +import { FiArrowUpCircle, FiBookOpen, FiLogOut, FiMenu, FiMoon, FiSun } from 'react-icons/fi'; interface ChatStatusState { trackedSessionId: string | null; @@ -69,6 +70,7 @@ function TopbarUpdateShortcut({ notice, onClick }: { notice: TopbarUpdateNotice; } export default function Topbar() { + const location = useLocation(); const nav = useNavigate(); const doLogout = useAuthStore((s) => s.logout); const activeSessionId = useChatSessionStore((s) => s.activeSessionId); @@ -83,6 +85,7 @@ export default function Topbar() { const { data: updateStatus } = useSystemUpdateStatus(); const chatStatus = resolveChatStatus(activeSessionId, activeSession, runningSessionId, connectionState); const updateNotice = getTopbarUpdateNotice(updateStatus); + const primaryDoc = getPrimaryDocForPath(location.pathname); useBackgroundSystemUpdateCheck(updateStatus); @@ -135,6 +138,20 @@ export default function Topbar() { )}
+ {primaryDoc != null && ( + + + {primaryDoc.shortLabel} + Docs + + )} {updateNotice != null && ( )} diff --git a/dashboard/src/components/webhooks/ValidationIssuesAlert.tsx b/dashboard/src/components/webhooks/ValidationIssuesAlert.tsx index 6887e5d2d..ae41d31ef 100644 --- a/dashboard/src/components/webhooks/ValidationIssuesAlert.tsx +++ b/dashboard/src/components/webhooks/ValidationIssuesAlert.tsx @@ -1,11 +1,16 @@ import type { ReactElement } from 'react'; import { Alert } from '../ui/tailwind-components'; + import type { WebhookValidationResult } from '../../api/webhooks'; +import { DocsLinkAnchor } from '../common/DocsLinkAnchor'; +import { getDocLink } from '../../lib/docsLinks'; interface ValidationIssuesAlertProps { validation: WebhookValidationResult; } +const WEBHOOKS_DOC = getDocLink('webhooks'); + export function ValidationIssuesAlert({ validation }: ValidationIssuesAlertProps): ReactElement | null { if (validation.valid) { return null; @@ -19,6 +24,11 @@ export function ValidationIssuesAlert({ validation }: ValidationIssuesAlertProps
  • {issue}
  • ))} +
    + + Review the webhook guide + +
    ); } diff --git a/dashboard/src/components/webhooks/WebhooksPageHeader.tsx b/dashboard/src/components/webhooks/WebhooksPageHeader.tsx index b6a9e3b2c..3b4eb9d66 100644 --- a/dashboard/src/components/webhooks/WebhooksPageHeader.tsx +++ b/dashboard/src/components/webhooks/WebhooksPageHeader.tsx @@ -2,6 +2,9 @@ import type { ReactElement } from 'react'; import { Badge } from '../ui/tailwind-components'; import { FiGlobe } from 'react-icons/fi'; +import { PageDocsLinks } from '../common/PageDocsLinks'; +import { getDocLinks } from '../../lib/docsLinks'; + export interface WebhookSummary { total: number; agent: number; @@ -13,6 +16,8 @@ interface WebhooksPageHeaderProps { summary: WebhookSummary; } +const WEBHOOK_DOCS = getDocLinks(['webhooks', 'dashboard']); + export function WebhooksPageHeader({ enabled, summary }: WebhooksPageHeaderProps): ReactElement { return (
    @@ -24,8 +29,9 @@ export function WebhooksPageHeader({ enabled, summary }: WebhooksPageHeaderProps

    Configure inbound HTTP hooks, authentication, templates, and delivery routes.

    +
    -
    +
    {enabled ? 'Enabled' : 'Disabled'} diff --git a/dashboard/src/lib/docsLinks.ts b/dashboard/src/lib/docsLinks.ts new file mode 100644 index 000000000..aba56cc87 --- /dev/null +++ b/dashboard/src/lib/docsLinks.ts @@ -0,0 +1,259 @@ +import type { ReactElement } from 'react'; + +const DOCS_BASE_URL = 'https://docs.golemcore.me'; + +export type DocId = + | 'overview' + | 'quickstart' + | 'dashboard' + | 'model-routing' + | 'memory' + | 'memory-tuning' + | 'skills' + | 'plugins' + | 'mcp' + | 'auto-mode' + | 'delayed-actions' + | 'webhooks' + | 'configuration' + | 'deployment' + | 'architecture' + | 'troubleshooting'; + +export interface DocDefinition { + title: string; + shortLabel: string; + path: string; +} + +export interface DocLink extends DocDefinition { + id: DocId; + url: string; +} + +interface PathDocsRule { + docs: readonly DocId[]; + matches: (pathname: string) => boolean; +} + +const DOC_DEFINITIONS: Record = { + overview: { + title: 'Overview', + shortLabel: 'Overview', + path: '/docs', + }, + quickstart: { + title: 'Quickstart', + shortLabel: 'Quickstart', + path: '/docs/user-guide/quickstart', + }, + dashboard: { + title: 'Dashboard', + shortLabel: 'Dashboard', + path: '/docs/user-guide/dashboard', + }, + 'model-routing': { + title: 'Model Routing', + shortLabel: 'Model Routing', + path: '/docs/user-guide/model-routing', + }, + memory: { + title: 'Memory', + shortLabel: 'Memory', + path: '/docs/user-guide/memory', + }, + 'memory-tuning': { + title: 'Memory Tuning', + shortLabel: 'Memory Tuning', + path: '/docs/user-guide/memory-tuning', + }, + skills: { + title: 'Skills', + shortLabel: 'Skills', + path: '/docs/user-guide/skills', + }, + plugins: { + title: 'Plugins', + shortLabel: 'Plugins', + path: '/docs/user-guide/plugins', + }, + mcp: { + title: 'MCP Servers', + shortLabel: 'MCP', + path: '/docs/user-guide/mcp', + }, + 'auto-mode': { + title: 'Auto Mode', + shortLabel: 'Auto Mode', + path: '/docs/user-guide/auto-mode', + }, + 'delayed-actions': { + title: 'Delayed Actions', + shortLabel: 'Delayed Actions', + path: '/docs/user-guide/delayed-actions', + }, + webhooks: { + title: 'Webhooks', + shortLabel: 'Webhooks', + path: '/docs/user-guide/webhooks', + }, + configuration: { + title: 'Configuration', + shortLabel: 'Configuration', + path: '/docs/user-guide/configuration', + }, + deployment: { + title: 'Deployment', + shortLabel: 'Deployment', + path: '/docs/user-guide/deployment', + }, + architecture: { + title: 'Architecture', + shortLabel: 'Architecture', + path: '/docs/developer-guide/architecture', + }, + troubleshooting: { + title: 'Troubleshooting', + shortLabel: 'Troubleshooting', + path: '/docs/reference/troubleshooting', + }, +}; + +const DEFAULT_SETTINGS_DOCS: readonly DocId[] = ['configuration', 'dashboard']; +const SETTINGS_SECTION_DOCS: Record = { + general: ['dashboard', 'configuration'], + 'llm-providers': ['quickstart', 'configuration'], + 'model-catalog': ['quickstart', 'configuration'], + models: ['model-routing', 'configuration'], + 'plugins-marketplace': ['plugins', 'configuration'], + 'tool-filesystem': ['dashboard', 'configuration'], + 'tool-shell': ['dashboard', 'configuration'], + 'tool-automation': ['dashboard', 'configuration'], + 'tool-goals': ['dashboard', 'configuration'], + 'tool-voice': ['dashboard', 'configuration'], + memory: ['memory', 'memory-tuning'], + skills: ['skills', 'mcp'], + turn: ['dashboard', 'configuration'], + usage: ['dashboard', 'configuration'], + telemetry: ['dashboard', 'configuration'], + tracing: ['dashboard', 'configuration'], + mcp: ['mcp', 'skills'], + hive: ['architecture', 'configuration'], + 'self-evolving': ['architecture', 'dashboard'], + plan: ['auto-mode', 'delayed-actions'], + auto: ['auto-mode', 'delayed-actions'], + updates: ['deployment', 'configuration'], + 'advanced-rate-limit': ['configuration', 'troubleshooting'], + 'advanced-security': ['configuration', 'troubleshooting'], + 'advanced-compaction': ['configuration', 'troubleshooting'], +}; +const PATH_DOC_RULES: PathDocsRule[] = [ + { + docs: ['dashboard', 'quickstart'], + matches: (pathname) => pathname === '/' || pathname.startsWith('/chat'), + }, + { + docs: ['quickstart', 'dashboard', 'configuration'], + matches: (pathname) => pathname === '/setup', + }, + { + docs: ['skills', 'mcp'], + matches: (pathname) => pathname.startsWith('/skills'), + }, + { + docs: ['webhooks', 'dashboard'], + matches: (pathname) => pathname.startsWith('/webhooks'), + }, + { + docs: ['dashboard', 'skills'], + matches: (pathname) => pathname.startsWith('/ide'), + }, + { + docs: ['auto-mode', 'delayed-actions'], + matches: (pathname) => pathname.startsWith('/scheduler') || pathname.startsWith('/goals'), + }, + { + docs: ['dashboard', 'troubleshooting'], + matches: (pathname) => pathname.startsWith('/logs') + || pathname.startsWith('/sessions') + || pathname.startsWith('/analytics') + || pathname.startsWith('/diagnostics'), + }, + { + docs: ['dashboard', 'configuration'], + matches: (pathname) => pathname.startsWith('/prompts'), + }, + { + docs: ['architecture', 'dashboard'], + matches: (pathname) => pathname.startsWith('/self-evolving'), + }, +]; + +function normalizePath(pathname: string): string { + if (pathname.length > 1 && pathname.endsWith('/')) { + return pathname.slice(0, -1); + } + return pathname; +} + +function dedupeDocIds(docIds: readonly DocId[]): DocId[] { + const seen = new Set(); + return docIds.filter((docId) => { + if (seen.has(docId)) { + return false; + } + seen.add(docId); + return true; + }); +} + +function cloneDocIds(docIds: readonly DocId[]): DocId[] { + return [...docIds]; +} + +export function getDocLink(docId: DocId): DocLink { + const definition = DOC_DEFINITIONS[docId]; + return { + id: docId, + title: definition.title, + shortLabel: definition.shortLabel, + path: definition.path, + url: `${DOCS_BASE_URL}${definition.path}`, + }; +} + +export function getDocLinks(docIds: readonly DocId[]): DocLink[] { + return dedupeDocIds(docIds).map((docId) => getDocLink(docId)); +} + +export function getDocsForSettingsSection(section: string | null | undefined): DocId[] { + if (section == null) { + return cloneDocIds(DEFAULT_SETTINGS_DOCS); + } + + return cloneDocIds(SETTINGS_SECTION_DOCS[section] ?? DEFAULT_SETTINGS_DOCS); +} + +export function getDocsForPath(pathname: string): DocId[] { + const normalizedPath = normalizePath(pathname); + if (normalizedPath.startsWith('/settings')) { + return getDocsForSettingsSection(normalizedPath.split('/')[2] ?? null); + } + + const matchedRule = PATH_DOC_RULES.find((rule) => rule.matches(normalizedPath)); + if (matchedRule != null) { + return cloneDocIds(matchedRule.docs); + } + + return ['overview']; +} + +export function getPrimaryDocForPath(pathname: string): DocLink | null { + const docs = getDocLinks(getDocsForPath(pathname)); + return docs[0] ?? null; +} + +export interface DocsRouteMatch { + doc: DocLink; + renderLabel: () => ReactElement | string; +} diff --git a/dashboard/src/pages/SettingsPage.tsx b/dashboard/src/pages/SettingsPage.tsx index 104b7b195..ce95690e1 100644 --- a/dashboard/src/pages/SettingsPage.tsx +++ b/dashboard/src/pages/SettingsPage.tsx @@ -1,6 +1,8 @@ import { useDeferredValue, useEffect, useState, type ReactElement } from 'react'; import { Card, Button, Spinner, Placeholder } from '../components/ui/tailwind-components'; import { useNavigate, useParams } from 'react-router-dom'; +import { PageDocsLinks } from '../components/common/PageDocsLinks'; +import { getDocLinks, getDocsForSettingsSection } from '../lib/docsLinks'; import { useSettings, useRuntimeConfig, useUpdateRuntimeConfig, } from '../hooks/useSettings'; @@ -42,7 +44,8 @@ import { SettingsCatalogView, type CatalogBlockView } from './settings/SettingsC import { buildCatalogBlocks, buildMarketplaceBadge, resolveSectionMeta } from './settings/SettingsPageState'; import { useTelemetry } from '../lib/telemetry/TelemetryContext'; -// ==================== Main ==================== +const SETTINGS_CATALOG_DOCS = getDocLinks(['configuration', 'dashboard', 'quickstart']); +const PLUGIN_SETTINGS_DOCS = getDocLinks(['plugins', 'configuration']); export default function SettingsPage(): ReactElement { const navigate = useNavigate(); @@ -74,6 +77,11 @@ export default function SettingsPage(): ReactElement { const sectionMeta = resolveSectionMeta(staticSection, pluginSection); const selectedSectionKey = staticSection ?? pluginSection?.routeKey ?? 'catalog'; + const sectionDocs = staticSection != null + ? getDocLinks(getDocsForSettingsSection(staticSection)) + : pluginSection != null + ? PLUGIN_SETTINGS_DOCS + : SETTINGS_CATALOG_DOCS; const catalogBlocks: CatalogBlockView[] = buildCatalogBlocks(pluginCatalog, marketplaceBadge); const filteredCatalogBlocks = filterCatalogBlocks(catalogBlocks, deferredCatalogSearch); @@ -94,6 +102,7 @@ export default function SettingsPage(): ReactElement {

    Settings

    Configure your GolemCore instance

    +
    @@ -112,6 +121,7 @@ export default function SettingsPage(): ReactElement { if (staticSection == null && pluginSection == null) { return ( { @@ -129,8 +139,9 @@ export default function SettingsPage(): ReactElement {

    {sectionMeta?.title ?? 'Settings'}

    {sectionMeta?.description ?? 'Configure your GolemCore instance'}

    +
    -
    +
    diff --git a/dashboard/src/pages/SetupPage.tsx b/dashboard/src/pages/SetupPage.tsx index 3043c567a..eefdfa442 100644 --- a/dashboard/src/pages/SetupPage.tsx +++ b/dashboard/src/pages/SetupPage.tsx @@ -1,6 +1,8 @@ import type { ReactElement } from 'react'; import { Alert, Badge, Button, Card, Spinner } from '../components/ui/tailwind-components'; import { useNavigate } from 'react-router-dom'; +import { PageDocsLinks } from '../components/common/PageDocsLinks'; +import { getDocLinks } from '../lib/docsLinks'; import { useRuntimeConfig } from '../hooks/useSettings'; import { extractErrorMessage } from '../utils/extractErrorMessage'; import { @@ -18,6 +20,8 @@ interface SetupStepCardProps { onAction: () => void; } +const SETUP_DOCS = getDocLinks(['quickstart', 'dashboard', 'configuration']); + function SetupStepCard({ step, title, @@ -57,6 +61,18 @@ function SetupLoadingState(): ReactElement { ); } +function SetupHeader(): ReactElement { + return ( +
    +

    Startup Setup Wizard

    +

    + Finish recommended startup configuration for reliable model routing and responses. +

    + +
    + ); +} + function getErrorMessage(error: unknown, fallback: string): string { const message = extractErrorMessage(error); return message === 'Unknown error' ? fallback : message; @@ -68,14 +84,22 @@ export default function SetupPage(): ReactElement { const runtimeConfig = runtimeConfigQuery.data; if (runtimeConfigQuery.isLoading) { - return ; + return ( +
    + + +
    + ); } if (runtimeConfig == null) { return ( - - {getErrorMessage(runtimeConfigQuery.error, 'Failed to load runtime configuration.')} - +
    + + + {getErrorMessage(runtimeConfigQuery.error, 'Failed to load runtime configuration.')} + +
    ); } @@ -86,12 +110,7 @@ export default function SetupPage(): ReactElement { return (
    -
    -

    Startup Setup Wizard

    -

    - Finish recommended startup configuration for reliable model routing and responses. -

    -
    + {runtimeConfigQuery.error != null && ( diff --git a/dashboard/src/pages/SkillsPage.tsx b/dashboard/src/pages/SkillsPage.tsx index fcc83882b..b87085f1d 100644 --- a/dashboard/src/pages/SkillsPage.tsx +++ b/dashboard/src/pages/SkillsPage.tsx @@ -4,10 +4,12 @@ import { useSearchParams } from 'react-router-dom'; import toast from 'react-hot-toast'; import type { SkillInfo, SkillUpdateRequest } from '../api/skills'; import ConfirmModal from '../components/common/ConfirmModal'; +import { PageDocsLinks } from '../components/common/PageDocsLinks'; import { Button } from '../components/ui/button'; import { Input } from '../components/ui/field'; import { Modal } from '../components/ui/overlay'; import { Card, CardContent } from '../components/ui/card'; +import { getDocLinks } from '../lib/docsLinks'; import { cn } from '../lib/utils'; import { useCreateSkill, @@ -28,6 +30,7 @@ model_tier: balanced `; const SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/; const SKILLS_TAB_QUERY_PARAM = 'tab'; +const SKILLS_DOCS = getDocLinks(['skills', 'mcp', 'dashboard']); type SkillsTabKey = 'local' | 'marketplace'; @@ -204,6 +207,7 @@ export default function SkillsPage(): ReactElement {

    Manage local skills and install maintained artifacts from one workspace.

    +
    diff --git a/dashboard/src/pages/settings/ModelsTab.tsx b/dashboard/src/pages/settings/ModelsTab.tsx index 8e93f7062..05e3bf647 100644 --- a/dashboard/src/pages/settings/ModelsTab.tsx +++ b/dashboard/src/pages/settings/ModelsTab.tsx @@ -3,6 +3,7 @@ import toast from 'react-hot-toast'; import { useNavigate } from 'react-router-dom'; import HelpTip from '../../components/common/HelpTip'; import SettingsCardTitle from '../../components/common/SettingsCardTitle'; +import { DocsLinkAnchor } from '../../components/common/DocsLinkAnchor'; import type { HiveStatusResponse } from '../../api/hive'; import type { AvailableModel } from '../../api/models'; import { modelReferenceFromSpec, modelReferenceToSpec } from '../../api/settings'; @@ -10,6 +11,7 @@ import type { LlmConfig, ModelRouterConfig } from '../../api/settingsTypes'; import { useUpdateModelRouter } from '../../hooks/useSettings'; import { useAvailableModels } from '../../hooks/useModels'; import { useBeforeUnloadGuard } from '../../hooks/useBeforeUnloadGuard'; +import { getDocLink } from '../../lib/docsLinks'; import { toEditorModelIdForProvider } from '../../lib/providerModelIds'; import { cloneModelRouterConfig, getTierBinding, updateTierBinding } from '../../lib/modelRouter'; import { @@ -45,7 +47,18 @@ interface TierCardConfig { allowEmptyModel: boolean; } +interface GlobalSettingsCardProps { + dynamicTierEnabled: boolean; + onDynamicTierEnabledChange: (enabled: boolean) => void; +} + +interface NoProvidersNoticeProps { + isVisible: boolean; +} + const EMPTY_AVAILABLE_MODELS: Record = {}; +const MODEL_ROUTING_DOC = getDocLink('model-routing'); +const PROVIDER_SETUP_DOC = getDocLink('configuration'); const TIER_CARDS: TierCardConfig[] = EXPLICIT_MODEL_TIER_ORDER.map((tier) => ({ key: tier, label: MODEL_TIER_META[tier].label, @@ -53,6 +66,59 @@ const TIER_CARDS: TierCardConfig[] = EXPLICIT_MODEL_TIER_ORDER.map((tier) => ({ allowEmptyModel: allowsEmptyModelSelection(tier), })); +function GlobalSettingsCard({ + dynamicTierEnabled, + onDynamicTierEnabledChange, +}: GlobalSettingsCardProps): ReactElement { + return ( + + + + + + + Dynamic tier upgrade{' '} + + } + checked={dynamicTierEnabled} + onChange={(event) => onDynamicTierEnabledChange(event.target.checked)} + /> + + + + + ); +} + +function NoProvidersNotice({ isVisible }: NoProvidersNoticeProps): ReactElement | null { + if (!isVisible) { + return null; + } + + return ( + + + + + No LLM providers with API keys configured. Add a provider with an API key in the LLM Providers tab to select models here. + +
    + + Open provider setup guide + +
    +
    +
    + + ); +} + export default function ModelsTab({ config, llmConfig, hiveStatus }: ModelsTabProps): ReactElement { const navigate = useNavigate(); const updateRouter = useUpdateModelRouter(); @@ -129,34 +195,13 @@ export default function ModelsTab({ config, llmConfig, hiveStatus }: ModelsTabPr ) : null}
    - - - - - - Dynamic tier upgrade } - checked={form.dynamicTierEnabled ?? true} - onChange={(e) => setForm({ ...form, dynamicTierEnabled: e.target.checked })} - /> - - - - + setForm({ ...form, dynamicTierEnabled })} + /> - {providerNames.length === 0 && ( - - - - - No LLM providers with API keys configured. Add a provider with an API key in the LLM Providers tab to select models here. - - - - - )} + void; @@ -31,6 +36,7 @@ interface SettingsCatalogViewProps { } export function SettingsCatalogView({ + docs, catalogSearch, filteredCatalogBlocks, onSearchChange, @@ -39,10 +45,29 @@ export function SettingsCatalogView({ }: SettingsCatalogViewProps): ReactElement { return (
    -

    Settings

    Select a settings category

    - +
    +

    Settings

    +

    Select a settings category

    + +
    + + + + + + {filteredCatalogBlocks.length === 0 ? ( -

    Nothing found

    No settings match `{catalogSearch}`. Try another name or clear the search.

    + + +

    Nothing found

    +

    + No settings match `{catalogSearch}`. Try another name or clear the search. +

    + + Open the main settings guide + +
    +
    ) : filteredCatalogBlocks.map((block) => )}
    ); diff --git a/dashboard/src/pages/settings/SettingsIntentCards.tsx b/dashboard/src/pages/settings/SettingsIntentCards.tsx new file mode 100644 index 000000000..4d61a65ed --- /dev/null +++ b/dashboard/src/pages/settings/SettingsIntentCards.tsx @@ -0,0 +1,108 @@ +import type { ReactElement } from 'react'; +import { Card, Button } from '../../components/ui/tailwind-components'; + +import { DocsLinkAnchor } from '../../components/common/DocsLinkAnchor'; +import { getDocLinks, type DocId, type DocLink } from '../../lib/docsLinks'; + +export interface SettingsIntentCard { + key: string; + title: string; + description: string; + routeKey: string; + docs: DocLink[]; +} + +export interface SettingsIntentCardsProps { + onOpenSection: (routeKey: string) => void; +} + +function buildIntentCards(): SettingsIntentCard[] { + const cardDefinitions: Array<{ + key: string; + title: string; + description: string; + routeKey: string; + docs: DocId[]; + }> = [ + { + key: 'connect-model', + title: 'Connect my first model', + description: 'Add a provider key and make the runtime ready for its first chat turn.', + routeKey: 'llm-providers', + docs: ['quickstart', 'configuration'], + }, + { + key: 'route-models', + title: 'Configure routing and fallbacks', + description: 'Choose which concrete models power routing, coding, and deep analysis tiers.', + routeKey: 'models', + docs: ['model-routing', 'configuration'], + }, + { + key: 'tune-memory', + title: 'Tune long-term memory', + description: 'Control recall depth, budgets, and presets before memory becomes noisy.', + routeKey: 'memory', + docs: ['memory', 'memory-tuning'], + }, + { + key: 'install-skills', + title: 'Install skills and MCP tools', + description: 'Use the marketplace and skill runtime controls to add focused capabilities.', + routeKey: 'skills', + docs: ['skills', 'mcp'], + }, + { + key: 'enable-auto-mode', + title: 'Enable autonomous runs', + description: 'Turn on goals, schedules, and delayed follow-ups for recurring workflows.', + routeKey: 'auto', + docs: ['auto-mode', 'delayed-actions'], + }, + ]; + + return cardDefinitions.map((card) => ({ + key: card.key, + title: card.title, + description: card.description, + routeKey: card.routeKey, + docs: getDocLinks(card.docs), + })); +} + +const SETTINGS_INTENT_CARDS = buildIntentCards(); + +export function SettingsIntentCards({ onOpenSection }: SettingsIntentCardsProps): ReactElement { + return ( +
    +
    +

    What are you trying to do?

    +

    + Jump straight to the right settings section and the matching documentation guide. +

    +
    +
    + {SETTINGS_INTENT_CARDS.map((card) => ( +
    + + +
    +

    {card.title}

    +

    {card.description}

    +
    +
    + + {card.docs.map((doc) => ( + + ))} +
    +
    +
    +
    + ))} +
    +
    + ); +} diff --git a/dashboard/src/pages/skills/LocalSkillsPanel.tsx b/dashboard/src/pages/skills/LocalSkillsPanel.tsx index 32666b825..caca3249d 100644 --- a/dashboard/src/pages/skills/LocalSkillsPanel.tsx +++ b/dashboard/src/pages/skills/LocalSkillsPanel.tsx @@ -1,10 +1,12 @@ import type { ReactElement } from 'react'; import { FiArrowRight } from 'react-icons/fi'; import type { SkillInfo, SkillUpdateRequest } from '../../api/skills'; +import { DocsLinkAnchor } from '../../components/common/DocsLinkAnchor'; import { Badge } from '../../components/ui/badge'; import { Button } from '../../components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../components/ui/card'; import { Input } from '../../components/ui/field'; +import { getDocLink } from '../../lib/docsLinks'; import { cn } from '../../lib/utils'; import { LocalSkillDetailPane } from './LocalSkillDetailPane'; @@ -25,14 +27,21 @@ interface LocalSkillsPanelProps { deletePending: boolean; } +const SKILLS_DOC = getDocLink('skills'); + function EmptyState({ onOpenMarketplace }: { onOpenMarketplace: () => void }): ReactElement { return (

    No skills match this filter.

    - +
    + + + How skills work + +
    ); }