feat(ai): P0 client scaffolding — capability flags, settings pane, policy fetch
Per docs/AI-ASSISTANT-CONCEPT.md §12, P0 is deliberately generation-free: prove platform gating and the policy round trip before any model exists behind it. No provider is called anywhere in this change. - lib/platform-capabilities.ts: supportsLocalLlm/localLlmNeedsCorsSetup, mirroring the same-named module in vncmail-native so the capability contract (§3, §11) reads identically on both clients. Web+Electron only here — mobile is a separate codebase. - lib/ai/types.ts: AiPolicy/AiEntitlement schema, locked in now per decision #4 (entitlement from day one — cheap now, a live-tenant migration later). - app/api/ai/policy/route.ts: GET, unauthenticated (users read this, like /api/admin/policy). Composes the real FeatureGates.aiAssistantEnabled toggle with a hardcoded unlicensed entitlement — there's no seats/billing backend yet (P2), so nothing here can honestly claim otherwise. - components/settings/ai-assistant-settings.tsx: fetches that policy, shows a real (not fake) locked/unlicensed state. No model config UI yet — there is nothing real to configure until P1/P2/P5 land. - New admin FeatureGates.aiAssistantEnabled (default false, like pluginsEnabled): the tab is entirely hidden until an admin opts in, so no existing install suddenly sees a tab that does nothing. Verified: typecheck clean, lint clean, translations test passes (48/48), full production build succeeds with /api/ai/policy compiled in.
This commit is contained in:
@@ -66,6 +66,14 @@ export interface FeatureGates {
|
||||
crossStarredViewEnabled: boolean;
|
||||
crossAllViewEnabled: boolean;
|
||||
unifiedCrossAccountEnabled: boolean;
|
||||
/**
|
||||
* Master admin switch for the AI Assistant tab (docs/AI-ASSISTANT-CONCEPT.md).
|
||||
* Defaults false, like pluginsEnabled — unlike every other gate, this one
|
||||
* fronts a feature with no licensed provider class behind it yet (P1/P2 of
|
||||
* that doc's phased rollout), so an admin opts in explicitly rather than
|
||||
* every existing install suddenly showing a tab that does nothing.
|
||||
*/
|
||||
aiAssistantEnabled: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_FEATURE_GATES: FeatureGates = {
|
||||
@@ -92,6 +100,7 @@ export const DEFAULT_FEATURE_GATES: FeatureGates = {
|
||||
crossStarredViewEnabled: false,
|
||||
crossAllViewEnabled: false,
|
||||
unifiedCrossAccountEnabled: false,
|
||||
aiAssistantEnabled: false,
|
||||
};
|
||||
|
||||
export interface ThemePolicy {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// Shared client/server contract for the AI Assistant feature.
|
||||
// docs/AI-ASSISTANT-CONCEPT.md §9 (entitlement), §11 (client shape), §12 (P0).
|
||||
//
|
||||
// P0 scope only: this file defines the schema so it never needs a breaking
|
||||
// migration later (decision #4 — entitlement from day one, cheap now). No
|
||||
// provider class is implemented behind it yet; see the doc's phase table.
|
||||
|
||||
export type AiClass = 'local' | 'server' | 'public';
|
||||
|
||||
export interface AiEntitlement {
|
||||
licensed: boolean;
|
||||
subject: 'user' | 'tenant';
|
||||
tier: 'base' | 'standard' | 'pro';
|
||||
classes: AiClass[];
|
||||
expiresAt: string | null;
|
||||
graceUntil: string | null;
|
||||
}
|
||||
|
||||
export interface AiPolicy {
|
||||
/** Admin FeatureGates.aiAssistantEnabled — the tab is hidden entirely below this. */
|
||||
enabled: boolean;
|
||||
entitlement: AiEntitlement;
|
||||
/** Public-model consent text version currently in force (§7.3). Unset until P2. */
|
||||
publicConsentVersion: string | null;
|
||||
}
|
||||
|
||||
export const DEFAULT_AI_ENTITLEMENT: AiEntitlement = {
|
||||
licensed: false,
|
||||
subject: 'tenant',
|
||||
tier: 'base',
|
||||
classes: [],
|
||||
expiresAt: null,
|
||||
graceUntil: null,
|
||||
};
|
||||
|
||||
export const DEFAULT_AI_POLICY: AiPolicy = {
|
||||
enabled: false,
|
||||
entitlement: { ...DEFAULT_AI_ENTITLEMENT },
|
||||
publicConsentVersion: null,
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
// Single source of truth for "this feature only exists on some platforms" —
|
||||
// mirrors the same-named module in vncmail-native (mobile), so the AI
|
||||
// Assistant capability contract (docs/AI-ASSISTANT-CONCEPT.md §3, §11)
|
||||
// reads identically across both clients.
|
||||
|
||||
import { isElectronShell } from '@/lib/electron-bridge';
|
||||
|
||||
/**
|
||||
* The `local` provider class (a loopback Ollama-compatible runtime) needs a
|
||||
* host process reachable on 127.0.0.1. Electron's main process can fetch
|
||||
* loopback directly, no CORS constraint. A browser page can too — localhost
|
||||
* is a trustworthy origin so mixed-content doesn't block it — but only if
|
||||
* the runtime's own CORS allowlist permits this origin (see
|
||||
* localLlmNeedsCorsSetup below). Mobile has neither the runtime nor the RAM
|
||||
* and is a separate codebase (vncmail-native), not reachable from here.
|
||||
*/
|
||||
export function supportsLocalLlm(): boolean {
|
||||
return true; // web or Electron — this codebase is never mobile
|
||||
}
|
||||
|
||||
/**
|
||||
* True only for the plain-browser case: Electron reaches loopback from its
|
||||
* main process with no CORS involved at all, so this is specifically the
|
||||
* "advise the user to set OLLAMA_ORIGINS" case (docs/AI-ASSISTANT-CONCEPT.md
|
||||
* §3's note under the platform matrix), not a general capability check.
|
||||
*/
|
||||
export function localLlmNeedsCorsSetup(): boolean {
|
||||
return !isElectronShell();
|
||||
}
|
||||
Reference in New Issue
Block a user