mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-25 00:13:00 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8438d5544b |
@@ -65,7 +65,7 @@
|
||||
},
|
||||
{
|
||||
"feature": "AI assistant chat",
|
||||
"hooks": ["src/components/Assistant/useAssistantThread.ts"],
|
||||
"hooks": ["src/components/Assistant/useAssistantThread.ts", "src/components/Assistant/useAssistantStatus.ts"],
|
||||
"desktop": ["src/components/Assistant/AssistantWindow.tsx"],
|
||||
"mobile": ["src/mobile/screens/trip/assistant/MAssistantScreen.tsx"]
|
||||
},
|
||||
|
||||
@@ -5,7 +5,9 @@ import { MASKED_SETTING_VALUE } from '@trek/shared';
|
||||
import { createElement, type ReactNode } from 'react';
|
||||
import { MemoryRouter } from 'react-router';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { seedStore } from '../../../../tests/helpers/store';
|
||||
import { TranslationProvider } from '../../../i18n/TranslationContext';
|
||||
import { useAddonStore } from '../../../store/addonStore';
|
||||
import { useAssistantStore } from '../../../store/assistantStore';
|
||||
|
||||
const repo = vi.hoisted(() => ({
|
||||
@@ -59,6 +61,8 @@ async function renderLoaded() {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useAssistantStore.setState({ status: null });
|
||||
// The settings show while the assistant is on, and only then is its status asked for.
|
||||
seedStore(useAddonStore, { loaded: true, addons: [{ id: 'ai_assistant', enabled: true }] });
|
||||
repo.status.mockRejectedValue(new Error('not needed'));
|
||||
repo.adminSettings.mockResolvedValue(SETTINGS);
|
||||
repo.saveAdminSettings.mockImplementation(async (next: AiAdminSettings) => next);
|
||||
|
||||
@@ -17,6 +17,7 @@ import type { AssistantChatTrip } from './assistantViewContext';
|
||||
import type { EntityTarget } from './cards/cardTypes';
|
||||
import { chatPagePath, useAssistantHome, useRouteJournalChat } from './useAssistantHome';
|
||||
import { useAssistantHotkey } from './useAssistantHotkey';
|
||||
import { useAssistantStatus } from './useAssistantStatus';
|
||||
import { useAssistantThread } from './useAssistantThread';
|
||||
|
||||
/**
|
||||
@@ -202,15 +203,12 @@ export default function AssistantWindow({ trip, onOpen, onFocusDay, onRevealMap
|
||||
const available = useAssistantStore((s) => s.status?.available === true);
|
||||
const open = useAssistantStore((s) => s.window.open);
|
||||
const minimized = useAssistantStore((s) => s.window.minimized);
|
||||
const loadStatus = useAssistantStore((s) => s.loadStatus);
|
||||
const toggle = useAssistantStore((s) => s.toggle);
|
||||
const [picked, setPicked] = useState<WindowTrip | null>(null);
|
||||
const [wasOpen, setWasOpen] = useState(open);
|
||||
const home = useAssistantHome(setPicked);
|
||||
|
||||
useEffect(() => {
|
||||
void loadStatus();
|
||||
}, [loadStatus]);
|
||||
useAssistantStatus();
|
||||
useAssistantHotkey(available, toggle);
|
||||
|
||||
// A trip picked outside the planner is only for this opening; the next one opens on the home chat again.
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* FE-AI-STATUS-001 to FE-AI-STATUS-003
|
||||
*
|
||||
* The views of the assistant ask for its status through this hook: not while
|
||||
* both addons that open /api/ai are off, where the server answers 404, and
|
||||
* as soon as one comes on, which the addon list can say after the view
|
||||
* mounted (the first load, an admin's switch). The assistant's own addon
|
||||
* switching asks again while AI parsing keeps /api/ai open.
|
||||
*/
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import type { AiStatusResponse } from '@trek/shared';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { resetAllStores, seedStore } from '../../../tests/helpers/store';
|
||||
import { aiConnectionsRepo } from '../../repo/aiConnectionsRepo';
|
||||
import { useAddonStore } from '../../store/addonStore';
|
||||
import { useAssistantStore } from '../../store/assistantStore';
|
||||
import { useAssistantStatus } from './useAssistantStatus';
|
||||
|
||||
const INITIAL = useAssistantStore.getState();
|
||||
const STATUS = { available: true, retention: { mode: 'days', days: 90 } } as AiStatusResponse;
|
||||
|
||||
/** The addon list as it lands from the server, with these addons on. */
|
||||
const addonsOn = (...ids: string[]) =>
|
||||
seedStore(useAddonStore, { loaded: true, addons: ids.map((id) => ({ id, enabled: true })) });
|
||||
|
||||
beforeEach(() => {
|
||||
resetAllStores();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
useAssistantStore.setState(INITIAL, true);
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('useAssistantStatus', () => {
|
||||
it('FE-AI-STATUS-001: asks nothing before the addon list or with both addons off, and asks once one comes on', async () => {
|
||||
const status = vi.spyOn(aiConnectionsRepo, 'status').mockResolvedValue(STATUS);
|
||||
renderHook(() => useAssistantStatus());
|
||||
act(() => addonsOn('mcp'));
|
||||
expect(status).not.toHaveBeenCalled();
|
||||
expect(useAssistantStore.getState().status).toBeNull();
|
||||
|
||||
act(() => addonsOn('ai_assistant'));
|
||||
await waitFor(() => expect(useAssistantStore.getState().status).toEqual(STATUS));
|
||||
expect(status).toHaveBeenCalledTimes(1);
|
||||
|
||||
// The list read again with the assistant still on asks nothing more.
|
||||
act(() => addonsOn('ai_assistant', 'mcp'));
|
||||
expect(status).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('FE-AI-STATUS-002: with AI parsing on it asks at once, and the last addon going off hides the assistant', async () => {
|
||||
const status = vi.spyOn(aiConnectionsRepo, 'status').mockResolvedValue(STATUS);
|
||||
addonsOn('llm_parsing');
|
||||
renderHook(() => useAssistantStatus());
|
||||
await waitFor(() => expect(useAssistantStore.getState().status).toEqual(STATUS));
|
||||
expect(status).toHaveBeenCalledTimes(1);
|
||||
|
||||
act(() => addonsOn());
|
||||
expect(useAssistantStore.getState().status).toBeNull();
|
||||
expect(status).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('FE-AI-STATUS-003: with AI parsing on, the assistant switched on or off asks again', async () => {
|
||||
const status = vi.spyOn(aiConnectionsRepo, 'status').mockResolvedValue(STATUS);
|
||||
addonsOn('llm_parsing');
|
||||
renderHook(() => useAssistantStatus());
|
||||
await waitFor(() => expect(status).toHaveBeenCalledTimes(1));
|
||||
|
||||
act(() => addonsOn('llm_parsing', 'ai_assistant'));
|
||||
await waitFor(() => expect(status).toHaveBeenCalledTimes(2));
|
||||
|
||||
act(() => addonsOn('llm_parsing'));
|
||||
await waitFor(() => expect(status).toHaveBeenCalledTimes(3));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useAddonStore } from '../../store/addonStore';
|
||||
import { aiRoutesOpen, useAssistantStore } from '../../store/assistantStore';
|
||||
|
||||
/**
|
||||
* Asks for the assistant's status when a view of it mounts, and again when
|
||||
* either addon that opens /api/ai is switched: the addon list can land after
|
||||
* the view, and an admin can switch either addon while it is open. The
|
||||
* assistant's own addon counts on its own, since the status says it runs only
|
||||
* while that one is on, even with AI parsing keeping /api/ai open. The store
|
||||
* asks the server only while one is on (aiRoutesOpen).
|
||||
*/
|
||||
export function useAssistantStatus(): void {
|
||||
const loadStatus = useAssistantStore((s) => s.loadStatus);
|
||||
const routesOpen = useAddonStore(aiRoutesOpen);
|
||||
const assistantOn = useAddonStore((s) => s.isEnabled('ai_assistant'));
|
||||
|
||||
useEffect(() => {
|
||||
void loadStatus();
|
||||
}, [routesOpen, assistantOn, loadStatus]);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { buildUser } from '../../../../tests/helpers/factories';
|
||||
import { render, screen } from '../../../../tests/helpers/render';
|
||||
import { resetAllStores, seedStore } from '../../../../tests/helpers/store';
|
||||
import { useAddonStore } from '../../../store/addonStore';
|
||||
import { useAssistantStore } from '../../../store/assistantStore';
|
||||
import { useAuthStore } from '../../../store/authStore';
|
||||
import { useSettingsStore } from '../../../store/settingsStore';
|
||||
@@ -33,6 +34,8 @@ beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useAssistantStore.setState({ status: null });
|
||||
seedStore(useAuthStore, { user: buildUser(), isAuthenticated: true });
|
||||
// The card shows while the assistant is on, and only then is its status asked for.
|
||||
seedStore(useAddonStore, { loaded: true, addons: [{ id: 'ai_assistant', enabled: true }] });
|
||||
repo.models.mockResolvedValue([]);
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { buildAdmin, buildUser } from '../../../../tests/helpers/factories';
|
||||
import { render, screen, waitFor } from '../../../../tests/helpers/render';
|
||||
import { resetAllStores, seedStore } from '../../../../tests/helpers/store';
|
||||
import { useAddonStore } from '../../../store/addonStore';
|
||||
import { useAssistantStore } from '../../../store/assistantStore';
|
||||
import { useAuthStore } from '../../../store/authStore';
|
||||
|
||||
@@ -51,6 +52,8 @@ beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useAssistantStore.setState({ status: null });
|
||||
seedStore(useAuthStore, { user: buildUser(), isAuthenticated: true });
|
||||
// The card shows while the assistant is on, and only then is its status asked for.
|
||||
seedStore(useAddonStore, { loaded: true, addons: [{ id: 'ai_assistant', enabled: true }] });
|
||||
repo.models.mockResolvedValue([{ id: 'claude-sonnet', label: 'Claude Sonnet', supportsTools: true }]);
|
||||
repo.disconnect.mockResolvedValue(undefined);
|
||||
repo.chatgptStart.mockResolvedValue({
|
||||
|
||||
@@ -4,7 +4,9 @@ import type { AiConnectionView, AiStatusResponse } from '@trek/shared';
|
||||
import { createElement, type ReactNode } from 'react';
|
||||
import { MemoryRouter, useLocation } from 'react-router';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { seedStore } from '../../../../tests/helpers/store';
|
||||
import { TranslationProvider } from '../../../i18n/TranslationContext';
|
||||
import { useAddonStore } from '../../../store/addonStore';
|
||||
import { useAssistantStore } from '../../../store/assistantStore';
|
||||
|
||||
const repo = vi.hoisted(() => ({
|
||||
@@ -61,6 +63,8 @@ function renderConnections(phone = false) {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useAssistantStore.setState({ status: null, window: { open: false, minimized: false } });
|
||||
// The card shows while the assistant is on, and only then is its status asked for.
|
||||
seedStore(useAddonStore, { loaded: true, addons: [{ id: 'ai_assistant', enabled: true }] });
|
||||
repo.status.mockResolvedValue(status());
|
||||
repo.models.mockResolvedValue([
|
||||
{ id: 'claude-sonnet', label: 'Claude Sonnet', supportsTools: true },
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useMatch, useNavigate } from 'react-router';
|
||||
|
||||
import type { AssistantChatTrip } from '../../../components/Assistant/assistantViewContext';
|
||||
import type { EntityTarget } from '../../../components/Assistant/cards/cardTypes';
|
||||
import { chatPagePath, useAssistantHome, useRouteJournalChat } from '../../../components/Assistant/useAssistantHome';
|
||||
import { useAssistantStatus } from '../../../components/Assistant/useAssistantStatus';
|
||||
import { useAssistantStore } from '../../../store/assistantStore';
|
||||
import { AssistantScreen, type MapHops } from '../trip/assistant/MAssistantScreen';
|
||||
|
||||
@@ -26,7 +27,6 @@ export default function MAssistantHomeScreen() {
|
||||
const available = useAssistantStore((s) => s.status?.available === true);
|
||||
const open = useAssistantStore((s) => s.window.open);
|
||||
const minimized = useAssistantStore((s) => s.window.minimized);
|
||||
const loadStatus = useAssistantStore((s) => s.loadStatus);
|
||||
const close = useAssistantStore((s) => s.close);
|
||||
|
||||
// The trip's screen (or the journal's) takes over with the window still open, so its chat comes up there.
|
||||
@@ -46,10 +46,8 @@ export default function MAssistantHomeScreen() {
|
||||
[close, navigate]
|
||||
);
|
||||
|
||||
// The desktop window loads this itself; on the phone the pages outside a trip ask once.
|
||||
useEffect(() => {
|
||||
void loadStatus();
|
||||
}, [loadStatus]);
|
||||
// The desktop window loads this itself; on the phone the pages outside a trip ask here.
|
||||
useAssistantStatus();
|
||||
|
||||
if (inTrip || !available || !open || minimized) return null;
|
||||
return (
|
||||
|
||||
@@ -5,6 +5,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { buildUser } from '../../../../tests/helpers/factories';
|
||||
import { render, screen, waitFor } from '../../../../tests/helpers/render';
|
||||
import { seedStore } from '../../../../tests/helpers/store';
|
||||
import { useAddonStore } from '../../../store/addonStore';
|
||||
import { useAssistantStore } from '../../../store/assistantStore';
|
||||
import { useAuthStore } from '../../../store/authStore';
|
||||
|
||||
@@ -47,6 +48,8 @@ function status(over: Partial<AiStatusResponse> = {}): AiStatusResponse {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useAssistantStore.setState({ status: null });
|
||||
// The card shows while the assistant is on, and only then is its status asked for.
|
||||
seedStore(useAddonStore, { loaded: true, addons: [{ id: 'ai_assistant', enabled: true }] });
|
||||
repo.models.mockResolvedValue([
|
||||
{ id: 'gemini-flash', label: 'Gemini Flash', supportsTools: true, free: true },
|
||||
{ id: 'gemini-pro', label: 'Gemini Pro', supportsTools: true },
|
||||
|
||||
@@ -23,6 +23,7 @@ import RoutePeekLazy from '../../../../components/Assistant/map/RoutePeekLazy';
|
||||
import { moveRoutePeek } from '../../../../components/Assistant/map/routePeekControl';
|
||||
import { useAssistantHintsSetting } from '../../../../components/Assistant/useAssistantHints';
|
||||
import { useAssistantLiveStep } from '../../../../components/Assistant/useAssistantLiveStep';
|
||||
import { useAssistantStatus } from '../../../../components/Assistant/useAssistantStatus';
|
||||
import { useAssistantThread } from '../../../../components/Assistant/useAssistantThread';
|
||||
import { useTripMemory } from '../../../../components/Assistant/useTripMemory';
|
||||
import { useKeyboardInset } from '../../../../hooks/useKeyboardInset';
|
||||
@@ -395,13 +396,10 @@ export default function MAssistantScreen({ planner, shell }: MTripSheetsProps) {
|
||||
const minimized = useAssistantStore((s) => s.window.minimized);
|
||||
const peeking = useAssistantStore((s) => s.routePeek?.tripId === planner.tripId);
|
||||
const running = useAssistantStore((s) => s.byTrip[planner.tripId]?.isRunning === true);
|
||||
const loadStatus = useAssistantStore((s) => s.loadStatus);
|
||||
const hops = useMapHops({ planner, shell });
|
||||
|
||||
// The desktop window loads this itself; on the phone the trip screen asks once.
|
||||
useEffect(() => {
|
||||
void loadStatus();
|
||||
}, [loadStatus]);
|
||||
// The desktop window loads this itself; on the phone the trip screen asks here.
|
||||
useAssistantStatus();
|
||||
|
||||
if (!available || !open || !planner.trip) return null;
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* FE-AI-STORE-001 to FE-AI-STORE-012
|
||||
* FE-AI-STORE-001 to FE-AI-STORE-014
|
||||
*
|
||||
* The store runs a turn end to end against a faked repo: the request it
|
||||
* sends, how the stream lands in the chat, and what each way of failing
|
||||
@@ -9,11 +9,14 @@
|
||||
*/
|
||||
import type { AiStatusResponse, AssistantMessage, AssistantThreadResponse, UiStreamChunk } from '@trek/shared';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { seedStore } from '../../tests/helpers/store';
|
||||
import { AssistantRequestError } from '../api/assistant';
|
||||
import { aiConnectionsRepo } from '../repo/aiConnectionsRepo';
|
||||
import { assistantRepo } from '../repo/assistantRepo';
|
||||
import { OnlineOnlyError } from '../repo/onlineOnly';
|
||||
import { useAddonStore } from './addonStore';
|
||||
import { chatOf, useAssistantStore } from './assistantStore';
|
||||
import { useAuthStore } from './authStore';
|
||||
import { useSettingsStore } from './settingsStore';
|
||||
|
||||
const TRIP = 4;
|
||||
@@ -60,6 +63,10 @@ async function* hangingStream(first: UiStreamChunk[], signal: AbortSignal): Asyn
|
||||
});
|
||||
}
|
||||
|
||||
/** The addon list as it lands from the server, with these addons on. */
|
||||
const addonsOn = (...ids: string[]) =>
|
||||
seedStore(useAddonStore, { loaded: true, addons: ids.map((id) => ({ id, enabled: true })) });
|
||||
|
||||
const chat = () => chatOf(useAssistantStore.getState(), TRIP);
|
||||
const store = () => useAssistantStore.getState();
|
||||
|
||||
@@ -76,6 +83,7 @@ beforeEach(() => {
|
||||
lastApplied: null,
|
||||
});
|
||||
useSettingsStore.setState((s) => ({ settings: { ...s.settings, language: 'de' } }));
|
||||
useAuthStore.setState({ managed: false });
|
||||
});
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
@@ -311,6 +319,7 @@ describe('assistantStore: threads, window and status', () => {
|
||||
});
|
||||
|
||||
it('FE-AI-STORE-012: status fails closed, offline keeps the last answer, and the privacy notice is acknowledged', async () => {
|
||||
addonsOn('ai_assistant');
|
||||
vi.spyOn(aiConnectionsRepo, 'status').mockResolvedValueOnce(STATUS);
|
||||
await store().loadStatus();
|
||||
expect(store().status).toEqual(STATUS);
|
||||
@@ -328,4 +337,39 @@ describe('assistantStore: threads, window and status', () => {
|
||||
await store().loadStatus();
|
||||
expect(store().status).toBeNull();
|
||||
});
|
||||
|
||||
it('FE-AI-STORE-013: the status is asked for only while the assistant or AI parsing is on', async () => {
|
||||
const status = vi.spyOn(aiConnectionsRepo, 'status').mockResolvedValue(STATUS);
|
||||
useAddonStore.setState({ addons: [], loaded: false });
|
||||
await store().loadStatus();
|
||||
addonsOn('mcp');
|
||||
await store().loadStatus();
|
||||
expect(status).not.toHaveBeenCalled();
|
||||
expect(store().status).toBeNull();
|
||||
|
||||
addonsOn('llm_parsing');
|
||||
await store().loadStatus();
|
||||
expect(status).toHaveBeenCalledTimes(1);
|
||||
expect(store().status).toEqual(STATUS);
|
||||
|
||||
addonsOn('ai_assistant');
|
||||
await store().loadStatus();
|
||||
expect(status).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Switched off again, the assistant goes away without a request.
|
||||
addonsOn();
|
||||
await store().loadStatus();
|
||||
expect(status).toHaveBeenCalledTimes(2);
|
||||
expect(store().status).toBeNull();
|
||||
});
|
||||
|
||||
it('FE-AI-STORE-014: a managed install, which refuses /api/ai, is not asked whatever its addons say', async () => {
|
||||
const status = vi.spyOn(aiConnectionsRepo, 'status').mockResolvedValue(STATUS);
|
||||
useAssistantStore.setState({ status: STATUS });
|
||||
useAuthStore.setState({ managed: true });
|
||||
addonsOn('ai_assistant', 'llm_parsing');
|
||||
await store().loadStatus();
|
||||
expect(status).not.toHaveBeenCalled();
|
||||
expect(store().status).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ import type { Assignment } from '../types';
|
||||
import { browserTimeZone } from '../utils/localDate';
|
||||
import { randomId } from '../utils/randomId';
|
||||
import type { RouteProfileFocus } from '../utils/routeGeometry';
|
||||
import { useAddonStore } from './addonStore';
|
||||
import { assistantScopeOf, assistantTargetOf, type AssistantChatKey } from './assistantChatKey';
|
||||
import {
|
||||
blockDays,
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
type WindowRect,
|
||||
} from './assistantMapModel';
|
||||
import { applyChunk, closeUndo, markDecisions } from './assistantMessages';
|
||||
import { useAuthStore } from './authStore';
|
||||
import { useSettingsStore } from './settingsStore';
|
||||
import { useTripStore } from './tripStore';
|
||||
|
||||
@@ -120,7 +122,10 @@ interface UndoBlockResult {
|
||||
}
|
||||
|
||||
interface AssistantState {
|
||||
/** What the assistant runs on for this user; null until loaded, and the assistant stays hidden until then. */
|
||||
/**
|
||||
* What the assistant runs on for this user; null until loaded, while no addon
|
||||
* opens /api/ai (aiRoutesOpen) and on a managed install, and the assistant stays hidden then.
|
||||
*/
|
||||
status: AiStatusResponse | null;
|
||||
window: { open: boolean; minimized: boolean };
|
||||
/** The chats by `AssistantChatKey`: a trip's id, or a journey's key. */
|
||||
@@ -316,6 +321,19 @@ export function chatOf(state: Pick<AssistantState, 'byTrip'>, key: AssistantChat
|
||||
return state.byTrip[key] ?? EMPTY_CHAT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the addons open /api/ai: the server opens it while the assistant or
|
||||
* AI parsing is on, the two sharing one connection, and answers 404 while
|
||||
* both are off. Closed until the addon list is in. A selector on the addon
|
||||
* store; a managed install refuses /api/ai whatever it says (loadStatus).
|
||||
*/
|
||||
export function aiRoutesOpen({
|
||||
loaded,
|
||||
isEnabled,
|
||||
}: Pick<ReturnType<typeof useAddonStore.getState>, 'loaded' | 'isEnabled'>): boolean {
|
||||
return loaded && (isEnabled('ai_assistant') || isEnabled('llm_parsing'));
|
||||
}
|
||||
|
||||
function httpStatus(err: unknown): number | undefined {
|
||||
return (err as { response?: { status?: number } } | null)?.response?.status;
|
||||
}
|
||||
@@ -568,6 +586,12 @@ function backToAfter(byTrip: Record<number, AssistantTripChat>): Record<number,
|
||||
function shellActions(ctx: Ctx) {
|
||||
return {
|
||||
loadStatus: async () => {
|
||||
// While /api/ai is closed the request could only get a 404, or the 403 of a managed install: nothing is asked,
|
||||
// and the assistant stays hidden. The managed flag comes with the app config, which can land after the addons.
|
||||
if (useAuthStore.getState().managed || !aiRoutesOpen(useAddonStore.getState())) {
|
||||
ctx.set({ status: null });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ctx.set({ status: await aiConnectionsRepo.status() });
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user