add /settings/ai route with role-aware model management

Two-tier model policy: system policy (admin, allowedModels) controls
member access, per-user hiddenModels controls personal visibility.
All model selectors now use useUserVisibleModels. Moved providers
and model visibility out of system settings, AI models out of profile.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 21:36:58 +00:00
co-authored by Claude Opus 4.6
parent d5fd3f58b1
commit 188e91920a
13 changed files with 606 additions and 413 deletions
@@ -1,6 +1,6 @@
import type { KeyboardEvent } from 'react';
import { useState, useRef, useEffect } from 'react';
import { useVisiblePiModels } from 'state/useModels';
import { useUserVisibleModels } from 'state/useModels';
import { usePiChat, type UsePiChatType } from '../../../hooks/usePiChat';
import { useAttachments } from '../useAttachments';
import { useSlashCommands } from '../useSlashCommands';
@@ -47,7 +47,7 @@ export function useEmbeddableChat(params: UseEmbeddableChatParams, onMessageComp
stopGeneration,
} = chat;
const availableModels = useVisiblePiModels();
const availableModels = useUserVisibleModels();
const attachmentManager = useAttachments({ sessionId });
const slashCommands = useSlashCommands({ sessionId });
@@ -6,7 +6,7 @@ import { cardStyle } from '@/components/Card';
import type { TaskInfo, ChatMessage } from '../../../Chat';
import { usePiChat, MessageBubble, StreamingBubble, ModelSelector } from '../../../Chat';
import { useSettings } from 'state/useSettings';
import { useVisiblePiModels } from 'state/useModels';
import { useUserVisibleModels } from 'state/useModels';
import type { TaskSummary } from '../../useTasks';
const playDing = () => {
@@ -46,7 +46,7 @@ type PiMonoInnerProps = {
const PiMonoInner = ({ defaultInput, cwd, initialModel, taskInfo, sandboxed }: PiMonoInnerProps) => {
const [phase, setPhase] = useState<Phase>('ready');
const chat = usePiChat(undefined, initialModel, { replaceUrl: false, taskInfo });
const availableModels = useVisiblePiModels();
const availableModels = useUserVisibleModels();
// --- Independent message accumulator (never loses messages) ---
const accRef = useRef<ChatMessage[]>([]);
+1 -1
View File
@@ -2,7 +2,7 @@ export { useSettings, DEFAULT_SETTINGS } from './useSettings';
export type { UseSettingsType, UserSettings, UserState } from './useSettings';
export { useUserState } from './useUserState';
export { useDashboardState } from './useDashboardState';
export { usePiModels, useVisiblePiModels, useEnabledPiModels, modelKey } from './useModels';
export { usePiModels, useVisiblePiModels, useUserVisibleModels, useEnabledPiModels, modelKey } from './useModels';
export type { ModelOption } from './useModels';
export { useAccessPolicy } from './useAccessPolicy';
export { useRecentModels } from './useRecentModels';
+20 -3
View File
@@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query';
import { useClient } from 'hooks/useClient';
import { useAuth } from 'hooks/useAuth';
import { useAccessPolicy } from './useAccessPolicy';
import { useSettings } from './useSettings';
import type { ModelOption } from 'officerdev';
export type { ModelOption };
@@ -52,14 +53,13 @@ export function usePiModels() {
return models;
}
/** Filter models by system-wide access policy. Super Admin sees all. New providers pass through. */
/** Filter models by system-wide access policy. New providers pass through. */
export function useVisiblePiModels() {
const models = usePiModels();
const { user } = useAuth();
const { policy } = useAccessPolicy();
const allowed = policy.allowedModels;
if (user?.role === 'Super Admin' || allowed.length === 0) return models;
if (allowed.length === 0) return models;
const allowedSet = new Set(allowed);
const allowedProviderSet = new Set(allowed.map((key) => key.split(':')[0]));
@@ -72,6 +72,23 @@ export function useVisiblePiModels() {
});
}
/** Models visible to the current user: system policy (members) or all (admins), minus per-user hidden. */
export function useUserVisibleModels() {
const allModels = usePiModels();
const policyModels = useVisiblePiModels();
const { user } = useAuth();
const { settings } = useSettings();
const isAdmin = user?.role !== 'Member';
const base = isAdmin ? allModels : policyModels;
const hidden = settings.chat.hiddenModels;
if (!hidden || hidden.length === 0) return base;
const hiddenSet = new Set(hidden);
return base.filter((m) => !hiddenSet.has(modelKey(m)));
}
/** Strict filtering — only explicitly allowed models, no "new provider" passthrough. */
export function useEnabledPiModels() {
const models = usePiModels();
+1
View File
@@ -55,6 +55,7 @@ export type UserSettings = {
systemPrompt: string;
temperature: number;
defaultPwd: string;
hiddenModels?: string[];
};
ai: {
enabledModels: string[];