mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
feat(desktop): show the AI assistant in SaaS mode via the cloud kill switch (#6666)
## What & why Chained on top of #6649 (the `cloud/` refactor). The AI assistant was effectively dead on desktop: 1. **Hidden** — `ChatFAB` gates on `aiEngineEnabled`, which desktop reads from the **local** bundled backend's `/api/v1/config/app-config`. The local backend has no AI engine, so the flag is always `false` and the FAB never renders. 2. **Mis-routed** — even if shown, AI calls used `getApiBaseUrl()`, which is empty/local on desktop, so the orchestrate stream and AI result-file download missed the engine (which only runs in the cloud). This PR wires AI properly **without hardcoding it on**, so the cloud keeps the kill switch: flip `aiEngineEnabled` server-side and the desktop FAB disappears on the next load — no desktop release required. (Deliberately *not* assume-on, so a future "turn AI off" doesn't strand shipped versions.) ## Changes **General SaaS app-config service** (reusable for any cloud flag, not just AI): - `desktop/services/saasAppConfigService.ts` — SaaS-mode-only fetch + 5-min cache of the **public** `/api/v1/config/app-config` from the **SaaS** backend over native HTTP (`@tauri-apps/plugin-http`, no CORS). Returns `null` outside SaaS mode. - `desktop/hooks/useSaasAppConfig.ts` — hook over it; reloads on connection-mode change. **AI gating + routing seams:** - `useAiEngineEnabled()` — core reads `useAppConfig()` (web), desktop reads `useSaasAppConfig()`. `ChatFAB` consumes it. - `getAiBaseUrl()` — core uses the normal API base (web), desktop points AI calls at the SaaS backend. `ChatContext` uses it for the orchestrate stream + result-file download. - `operationRouter` — route `/api/v1/ai/*` to the SaaS backend (cloud-only prefix). **Docs:** AGENTS.md gains a short "cloud feature flags on desktop" note so the pattern is maintained. ## Verification - `tsc --noEmit` green for saas / desktop / cloud flavors - `eslint --max-warnings=0` clean (cloud-layer guardrail respected — the platform-coupled bits live in `desktop/`) - New `saasAppConfigService.test.ts` (3 tests) + existing `operationRouter` / `tauriHttpClient` / `httpErrorHandler` suites green - 0 stray compiled artifacts ## Not headlessly verifiable — needs a live Tauri smoke The orchestrate **SSE stream** uses the webview's global `fetch` (native HTTP can't stream the body the same way), so it's subject to browser CORS to the SaaS backend. The `SupabaseSecurityConfig` tauri-origin allowance (from #6649) covers it, but please confirm on a real build: open the FAB in SaaS mode, run an agent task, watch the stream + a result-file download succeed.
This commit is contained in:
@@ -10,7 +10,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { generateId } from "@app/utils/generateId";
|
||||
import { useAllFiles, useFileActions } from "@app/contexts/FileContext";
|
||||
import apiClient from "@app/services/apiClient";
|
||||
import { getApiBaseUrl } from "@app/services/apiClientConfig";
|
||||
import { getAiBaseUrl } from "@app/services/aiBaseUrl";
|
||||
import { getAuthHeaders } from "@app/services/apiClientSetup";
|
||||
import { dispatchPaygLimitReached } from "@app/services/usageLimitBridge";
|
||||
import { createChildStub } from "@app/contexts/file/fileActions";
|
||||
@@ -407,8 +407,10 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
||||
// Download a File from the Stirling files endpoint.
|
||||
const downloadFile = useCallback(
|
||||
async (descriptor: AiWorkflowResultFile): Promise<File> => {
|
||||
// AI result files live on the backend that ran the workflow (the SaaS
|
||||
// engine on desktop), so fetch from the AI base, not the local backend.
|
||||
const response = await apiClient.get<Blob>(
|
||||
`/api/v1/general/files/${descriptor.fileId}`,
|
||||
`${getAiBaseUrl()}/api/v1/general/files/${descriptor.fileId}`,
|
||||
{ responseType: "blob" },
|
||||
);
|
||||
return new File([response.data], descriptor.fileName, {
|
||||
@@ -520,7 +522,7 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
||||
formData.append(`conversationHistory[${i}].content`, message.content);
|
||||
});
|
||||
const response = await fetch(
|
||||
`${getApiBaseUrl()}/api/v1/ai/orchestrate/stream`,
|
||||
`${getAiBaseUrl()}/api/v1/ai/orchestrate/stream`,
|
||||
{
|
||||
method: "POST",
|
||||
body: formData,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ChatFABButton } from "@shared/components/ChatFABButton";
|
||||
import { ChatFABWindow } from "@shared/components/ChatFABWindow";
|
||||
import { ChatPanel } from "@app/components/chat/ChatPanel";
|
||||
import { useChat } from "@app/components/chat/ChatContext";
|
||||
import { useAppConfig } from "@app/contexts/AppConfigContext";
|
||||
import { useAiEngineEnabled } from "@app/hooks/useAiEngineEnabled";
|
||||
import { Z_INDEX_CHAT_FAB_OVERLAY } from "@app/styles/zIndex";
|
||||
import "@app/components/chat/ChatFAB.css";
|
||||
|
||||
@@ -53,8 +53,9 @@ export function ChatFAB() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [hasUnviewedResult, setHasUnviewedResult] = useState(false);
|
||||
const { isLoading } = useChat();
|
||||
const { config } = useAppConfig();
|
||||
const enabled = Boolean(config?.aiEngineEnabled);
|
||||
// Desktop sources this from the SaaS backend (cloud kill switch); web reads it
|
||||
// from the local app-config. Either way the AI engine drives FAB visibility.
|
||||
const enabled = useAiEngineEnabled();
|
||||
|
||||
// Detect loading → done transition. If the FAB is closed when the agent
|
||||
// finishes, show the tick badge until the user opens the panel.
|
||||
|
||||
Reference in New Issue
Block a user