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.
77 lines
2.9 KiB
TypeScript
77 lines
2.9 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { Sparkles, Loader2 } from 'lucide-react';
|
|
import { SettingsSection, SettingItem } from './settings-section';
|
|
import { apiFetch } from '@/lib/browser-navigation';
|
|
import { DEFAULT_AI_POLICY, type AiPolicy } from '@/lib/ai/types';
|
|
import { supportsLocalLlm, localLlmNeedsCorsSetup } from '@/lib/platform-capabilities';
|
|
|
|
/**
|
|
* P0 scope only (docs/AI-ASSISTANT-CONCEPT.md §12): proves capability
|
|
* gating and the policy-fetch round trip. No provider is called from here —
|
|
* that's P1 (server class) onward. Once entitlement is real (P2), this pane
|
|
* grows the Model/Scope/Index sections from §4.
|
|
*/
|
|
export function AiAssistantSettings() {
|
|
const [policy, setPolicy] = useState<AiPolicy>(DEFAULT_AI_POLICY);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
(async () => {
|
|
try {
|
|
const res = await apiFetch('/api/ai/policy');
|
|
if (res.ok && !cancelled) {
|
|
setPolicy(await res.json());
|
|
}
|
|
} finally {
|
|
if (!cancelled) setLoading(false);
|
|
}
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<SettingsSection
|
|
title="AI Assistant"
|
|
description="Ask questions about your mail, answered by a model you or your admin choose. In preview — see below."
|
|
>
|
|
<SettingItem label="Status">
|
|
{loading ? (
|
|
<span className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<Loader2 className="w-3.5 h-3.5 animate-spin" /> Checking availability…
|
|
</span>
|
|
) : policy.entitlement.licensed ? (
|
|
<span className="text-sm text-muted-foreground">
|
|
Licensed ({policy.entitlement.tier}) — no model provider is configured yet.
|
|
</span>
|
|
) : (
|
|
<span className="text-sm text-muted-foreground">Not yet licensed for this account.</span>
|
|
)}
|
|
</SettingItem>
|
|
</SettingsSection>
|
|
|
|
<SettingsSection
|
|
title="What's coming"
|
|
description="This tab exists ahead of the feature so the settings surface and platform gating are proven before any model is wired in."
|
|
>
|
|
<div className="flex items-start gap-3 rounded-lg border border-border p-4">
|
|
<Sparkles className="w-4 h-4 mt-0.5 text-muted-foreground shrink-0" />
|
|
<p className="text-sm text-muted-foreground">
|
|
Local, VNC-hosted, and bring-your-own-key providers are planned (see the AI Assistant
|
|
concept doc). {supportsLocalLlm()
|
|
? localLlmNeedsCorsSetup()
|
|
? 'A local runtime will need its CORS setting adjusted to allow this browser origin.'
|
|
: 'This desktop app can reach a local runtime with no extra setup.'
|
|
: null}
|
|
</p>
|
|
</div>
|
|
</SettingsSection>
|
|
</div>
|
|
);
|
|
}
|