From 9ecaf9499450039d63d984039a3582f498246d3b Mon Sep 17 00:00:00 2001 From: Oleh Martsokha Date: Wed, 9 Sep 2026 04:56:31 +0200 Subject: [PATCH 1/4] Adopt SDK 0.49: migrate LLMs from connections to the new providers resource SDK 0.49 splits inference services (LLM/NER) out of connections into a dedicated `providers` resource, and reshapes connection sync fields. Providers - New `useProviders` composable over `client.providers.*` (list/create/update/ delete/verify), matching the connections colada + optimistic-list pattern. - New Providers tab in the integrations section, with ProvidersTable (name, enabled, type, updated; configure/test/delete) and ConfigureProviderDialog (name + active, opt-in credential replacement). Providers don't sync or transfer files, so the table omits those columns. - The explore-page LLM connect flow now routes to `createProvider` and lands on the Providers tab; ConnectLlmDialog emits `CreateProvider`. `useConnectProvider` splits its submit into storage (createConnection) and LLM (createProvider). - LLMs no longer appear in the connections table (not returned by listConnections). Breaking-change adoption - `Provider` file-service enum renamed to `FileServiceProvider`. - `connection.providerType` renamed to `connection.connectionType` (object_store | file_service). - Connection `sync.lastSynced` moved to the top-level `lastSyncedAt`. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JcYYAsbEf75Lke291j9AYU --- apps/desktop/package.json | 2 +- apps/web/package.json | 2 +- package-lock.json | 12 +- .../integrations/ConfigureProviderDialog.vue | 220 ++++++++++++++++ .../pages/integrations/ConnectLlmDialog.vue | 6 +- .../pages/integrations/ConnectionsTable.vue | 6 +- .../integrations/ExportToConnectionDialog.vue | 2 +- .../pages/integrations/ProvidersTable.vue | 131 ++++++++++ .../components/pages/integrations/index.ts | 4 + .../app/composables/useConnectProvider.ts | 34 ++- .../console/app/composables/useFileImport.ts | 2 +- .../console/app/composables/useProviders.ts | 103 ++++++++ .../console/app/composables/useSectionTabs.ts | 7 + .../w/[workspace]/integrations/explore.vue | 7 +- .../w/[workspace]/integrations/providers.vue | 234 ++++++++++++++++++ .../app/utils/connections/fileservice.ts | 6 +- packages/console/i18n/locales/de.json | 58 +++++ packages/console/i18n/locales/en.json | 58 +++++ packages/console/package.json | 2 +- 19 files changed, 867 insertions(+), 29 deletions(-) create mode 100644 packages/console/app/components/pages/integrations/ConfigureProviderDialog.vue create mode 100644 packages/console/app/components/pages/integrations/ProvidersTable.vue create mode 100644 packages/console/app/composables/useProviders.ts create mode 100644 packages/console/app/pages/w/[workspace]/integrations/providers.vue diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 410e8a77..cb35760f 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -30,7 +30,7 @@ "dependencies": { "@lucide/vue": "^1.41.0", "@nvisy/console": "*", - "@nvisy/sdk": "^0.48.0", + "@nvisy/sdk": "^0.49.0", "@tauri-apps/api": "^2", "@tauri-apps/plugin-deep-link": "^2", "@tauri-apps/plugin-http": "^2.6.0", diff --git a/apps/web/package.json b/apps/web/package.json index dee49372..21254b42 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -19,7 +19,7 @@ "dependencies": { "@nuxtjs/device": "^4.0.0", "@nvisy/console": "*", - "@nvisy/sdk": "^0.48.0", + "@nvisy/sdk": "^0.49.0", "jszip": "^3.10.1", "nuxt": "^4.5.2", "vue": "^3.5.42", diff --git a/package-lock.json b/package-lock.json index 5c35ccd5..af089257 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "dependencies": { "@lucide/vue": "^1.41.0", "@nvisy/console": "*", - "@nvisy/sdk": "^0.48.0", + "@nvisy/sdk": "^0.49.0", "@tauri-apps/api": "^2", "@tauri-apps/plugin-deep-link": "^2", "@tauri-apps/plugin-http": "^2.6.0", @@ -48,7 +48,7 @@ "dependencies": { "@nuxtjs/device": "^4.0.0", "@nvisy/console": "*", - "@nvisy/sdk": "^0.48.0", + "@nvisy/sdk": "^0.49.0", "jszip": "^3.10.1", "nuxt": "^4.5.2", "vue": "^3.5.42", @@ -3742,9 +3742,9 @@ "link": true }, "node_modules/@nvisy/sdk": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@nvisy/sdk/-/sdk-0.48.0.tgz", - "integrity": "sha512-82WkJU3NcOq7vF1vqmrLk6wbhnjz/4M9syuY4DyVXSpbXHcB5L17/1g5cCa2Xny38Sys6g+HZjdwmSPgbkckCA==", + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@nvisy/sdk/-/sdk-0.49.0.tgz", + "integrity": "sha512-yLD7JiXSunJ5K3wPS35TRcHA2TEz3K2Cu4N15OfSGtyMmrokRxninYPxCdASHbYLPcnCaBBBQr/G4NnqsoWvKA==", "license": "MIT", "dependencies": { "openapi-fetch": "^0.17.0" @@ -19033,7 +19033,7 @@ "@lucide/vue": "^1.41.0", "@nuxtjs/color-mode": "^4.0.0", "@nuxtjs/i18n": "^10.6.0", - "@nvisy/sdk": "^0.48.0", + "@nvisy/sdk": "^0.49.0", "@pinia/colada": "^1.4.4", "@pinia/colada-nuxt": "^1.1.0", "@pinia/nuxt": "^1.0.2", diff --git a/packages/console/app/components/pages/integrations/ConfigureProviderDialog.vue b/packages/console/app/components/pages/integrations/ConfigureProviderDialog.vue new file mode 100644 index 00000000..38937499 --- /dev/null +++ b/packages/console/app/components/pages/integrations/ConfigureProviderDialog.vue @@ -0,0 +1,220 @@ + + + diff --git a/packages/console/app/components/pages/integrations/ConnectLlmDialog.vue b/packages/console/app/components/pages/integrations/ConnectLlmDialog.vue index 49a9604d..465ada04 100644 --- a/packages/console/app/components/pages/integrations/ConnectLlmDialog.vue +++ b/packages/console/app/components/pages/integrations/ConnectLlmDialog.vue @@ -1,5 +1,5 @@ diff --git a/packages/console/app/components/pages/integrations/ConnectionsTable.vue b/packages/console/app/components/pages/integrations/ConnectionsTable.vue index 56f66e9f..83d3c565 100644 --- a/packages/console/app/components/pages/integrations/ConnectionsTable.vue +++ b/packages/console/app/components/pages/integrations/ConnectionsTable.vue @@ -81,8 +81,8 @@ const columns = computed[]>(() => [ width: "160px", cell: (c) => ({ type: "text", - value: c.sync?.lastSynced - ? relativeTime(c.sync.lastSynced) + value: c.lastSyncedAt + ? relativeTime(c.lastSyncedAt) : t("common.time.never"), muted: true, }), @@ -94,7 +94,7 @@ function rowActions(connection: Connection): RowAction[] { // picker nor a redacted-export target). Import needs an active connection with // a wired picker (the shared eligibility rule); export applies to any file // service. - const fileService = connection.providerType === "file_service"; + const fileService = connection.connectionType === "file_service"; const importAction: RowAction[] = isImportableConnection(connection, { dropboxAppKey, }) diff --git a/packages/console/app/components/pages/integrations/ExportToConnectionDialog.vue b/packages/console/app/components/pages/integrations/ExportToConnectionDialog.vue index 151c6f12..caedb670 100644 --- a/packages/console/app/components/pages/integrations/ExportToConnectionDialog.vue +++ b/packages/console/app/components/pages/integrations/ExportToConnectionDialog.vue @@ -41,7 +41,7 @@ const emit = defineEmits<{ // can't. Inactive connections are excluded - a paused connection won't sync. const targets = computed(() => props.connections.filter( - (c) => c.providerType === "file_service" && c.isActive, + (c) => c.connectionType === "file_service" && c.isActive, ), ); diff --git a/packages/console/app/components/pages/integrations/ProvidersTable.vue b/packages/console/app/components/pages/integrations/ProvidersTable.vue new file mode 100644 index 00000000..bdc1a61c --- /dev/null +++ b/packages/console/app/components/pages/integrations/ProvidersTable.vue @@ -0,0 +1,131 @@ + + + diff --git a/packages/console/app/components/pages/integrations/index.ts b/packages/console/app/components/pages/integrations/index.ts index 0ae11891..a8917cc8 100644 --- a/packages/console/app/components/pages/integrations/index.ts +++ b/packages/console/app/components/pages/integrations/index.ts @@ -8,6 +8,10 @@ export { default as ConnectFileServiceDialog } from "./ConnectFileServiceDialog. export { default as ExportToConnectionDialog } from "./ExportToConnectionDialog.vue"; export { default as ImportFromConnectionDialog } from "./ImportFromConnectionDialog.vue"; +// Providers (inference) +export { default as ProvidersTable } from "./ProvidersTable.vue"; +export { default as ConfigureProviderDialog } from "./ConfigureProviderDialog.vue"; + // Webhooks export { default as WebhooksTable } from "./WebhooksTable.vue"; export { default as WebhookSheet } from "./WebhookSheet.vue"; diff --git a/packages/console/app/composables/useConnectProvider.ts b/packages/console/app/composables/useConnectProvider.ts index 6dd888fd..e408e255 100644 --- a/packages/console/app/composables/useConnectProvider.ts +++ b/packages/console/app/composables/useConnectProvider.ts @@ -1,5 +1,6 @@ import type { CreateConnection, + CreateProvider, StartFileServiceOAuth, } from "@nvisy/sdk/datatypes"; import type { @@ -21,7 +22,8 @@ import { toast } from "vue-sonner"; * * Three connectable families connect three different ways: * - object stores (S3, Azure, GCS): a credential dialog -> `createConnection`. - * - LLMs (OpenAI, Anthropic, Ollama): a different credential dialog -> same. + * - LLMs (OpenAI, Anthropic, Ollama): a credential dialog -> `createProvider` + * (inference providers are a separate resource from connections). * - cloud file services (Drive, Dropbox, OneDrive, Box): OAuth - a short dialog * collects a name/folder, then we redirect to the provider's consent screen. * @@ -33,10 +35,17 @@ export function useConnectProvider() { const { wLink } = useWorkspaceLink(); const { createConnectionAsync, - isCreating, + isCreating: isCreatingConnection, startFileServiceOAuthAsync, isStartingOAuth, } = useConnections(); + const { createProviderAsync, isCreating: isCreatingProvider } = + useProviders(); + + // Either create-path may be in flight; the dialogs share one busy flag. + const isCreating = computed( + () => isCreatingConnection.value || isCreatingProvider.value, + ); // Shared header for whichever dialog opens (name + icon of the clicked card). const seed = ref({ name: "", icon: "" }); @@ -76,12 +85,11 @@ export function useConnectProvider() { } } - // Storage + LLM: create the connection from the dialog's payload. - async function submitCredentials(connection: CreateConnection) { + // Storage: create the object-store connection from the dialog's payload. + async function submitStorage(connection: CreateConnection) { try { await createConnectionAsync(connection); storageOpen.value = false; - llmOpen.value = false; toast.success(t("connections.dialogs.connect.success")); await navigateTo(wLink("/integrations")); } catch { @@ -89,6 +97,19 @@ export function useConnectProvider() { } } + // LLM: create the inference provider from the dialog's payload, and land on + // the providers tab where it now lives. + async function submitLlm(provider: CreateProvider) { + try { + await createProviderAsync(provider); + llmOpen.value = false; + toast.success(t("connections.dialogs.connect.success")); + await navigateTo(wLink("/integrations/providers")); + } catch { + toast.error(t("connections.dialogs.connect.error")); + } + } + // File service: ask the server for the authorize URL and hand the browser to // the consent screen. The connection is created by the server-side callback // once the user grants access, which then redirects back to /integrations @@ -121,7 +142,8 @@ export function useConnectProvider() { fileServiceOpen, fileServiceProvider, // Submits + loading - submitCredentials, + submitStorage, + submitLlm, submitOAuth, isCreating, isStartingOAuth, diff --git a/packages/console/app/composables/useFileImport.ts b/packages/console/app/composables/useFileImport.ts index 23e97b55..916c17ac 100644 --- a/packages/console/app/composables/useFileImport.ts +++ b/packages/console/app/composables/useFileImport.ts @@ -61,7 +61,7 @@ export function isImportableConnection( connection: Connection, config: PickerAvailability, ): boolean { - if (connection.providerType !== "file_service" || !connection.isActive) { + if (connection.connectionType !== "file_service" || !connection.isActive) { return false; } return PICKERS[connection.provider]?.available(config) ?? false; diff --git a/packages/console/app/composables/useProviders.ts b/packages/console/app/composables/useProviders.ts new file mode 100644 index 00000000..e02566aa --- /dev/null +++ b/packages/console/app/composables/useProviders.ts @@ -0,0 +1,103 @@ +import type { + CreateProvider, + Provider, + UpdateProvider, +} from "@nvisy/sdk/datatypes"; + +/** + * Composable for inference-provider operations — the LLM / NER services the + * platform calls. Providers are a separate resource from connections: they carry + * only inference config (an API key + model), never sync or file transfer. + */ +export function useProviders() { + const providersQuery = workspaceQuery( + "providers", + async ({ client, workspaceSlug }) => { + const result = await client.providers.listProviders(workspaceSlug); + return result.items; + }, + ); + + // Reflect updates on a row immediately, reconciling once settled. + const optimistic = useOptimisticList>( + providersQuery.data, + (p) => p.id, + ); + + const createProviderMutation = workspaceMutation( + ({ client, workspaceSlug }, provider: CreateProvider) => + client.providers.createProvider(workspaceSlug, provider), + { invalidates: "providers" }, + ); + + const updateProviderMutation = workspaceMutation( + ( + { client, workspaceSlug }, + { providerId, updates }: { providerId: string; updates: UpdateProvider }, + ) => client.providers.updateProvider(workspaceSlug, providerId, updates), + { + onMutate({ providerId, updates }) { + // Optimistically reflect only the fields that render on the row and are + // read-shape compatible. `config` uses a write-only input type, so it + // can't merge into a `Provider`; settle()/refresh reconciles it. + const patch: Partial = {}; + if (updates.displayName !== undefined) + patch.displayName = updates.displayName; + if (updates.isActive !== undefined) patch.isActive = updates.isActive; + optimistic.apply(providerId, patch); + }, + onSettled(data, _error, { providerId }) { + optimistic.settle(providerId, data as Partial | undefined); + providersQuery.refresh(); + }, + }, + ); + + const deleteProviderMutation = workspaceMutation( + ({ client, workspaceSlug }, providerId: string) => + client.providers.deleteProvider(workspaceSlug, providerId), + { + invalidates: "providers", + onMutate: (providerId) => optimistic.remove(providerId), + onError: (_error, providerId) => optimistic.restore(providerId), + }, + ); + + // Verify a provider is reachable with its stored credentials. + const verifyProviderMutation = workspaceMutation( + ({ client, workspaceSlug }, providerId: string) => + client.providers.verifyProvider(workspaceSlug, providerId), + ); + + return { + // Query state + providers: optimistic.items, + isLoading: providersQuery.isLoading, + error: providersQuery.error, + refresh: providersQuery.refresh, + + // Create + createProvider: createProviderMutation.mutate, + createProviderAsync: createProviderMutation.mutateAsync, + isCreating: createProviderMutation.isLoading, + createError: createProviderMutation.error, + + // Update + updateProvider: updateProviderMutation.mutate, + updateProviderAsync: updateProviderMutation.mutateAsync, + isUpdating: updateProviderMutation.isLoading, + updateError: updateProviderMutation.error, + + // Delete + deleteProvider: deleteProviderMutation.mutate, + deleteProviderAsync: deleteProviderMutation.mutateAsync, + isDeleting: deleteProviderMutation.isLoading, + deleteError: deleteProviderMutation.error, + + // Verify (test) provider + verifyProvider: verifyProviderMutation.mutate, + verifyProviderAsync: verifyProviderMutation.mutateAsync, + isVerifying: verifyProviderMutation.isLoading, + verifyError: verifyProviderMutation.error, + }; +} diff --git a/packages/console/app/composables/useSectionTabs.ts b/packages/console/app/composables/useSectionTabs.ts index de9485b9..2a253e2c 100644 --- a/packages/console/app/composables/useSectionTabs.ts +++ b/packages/console/app/composables/useSectionTabs.ts @@ -1,6 +1,7 @@ import { Plug, Webhook as WebhookIcon, + Bot, Compass, History, Workflow, @@ -63,6 +64,12 @@ export function useSectionTabs() { icon: Plug, to: "/integrations", }, + { + value: "providers", + label: t("header.tabs.connections.providers"), + icon: Bot, + to: "/integrations/providers", + }, { value: "webhooks", label: t("header.tabs.connections.webhooks"), diff --git a/packages/console/app/pages/w/[workspace]/integrations/explore.vue b/packages/console/app/pages/w/[workspace]/integrations/explore.vue index 2bec33a3..ec4e7b03 100644 --- a/packages/console/app/pages/w/[workspace]/integrations/explore.vue +++ b/packages/console/app/pages/w/[workspace]/integrations/explore.vue @@ -53,7 +53,8 @@ const { llmProvider, fileServiceOpen, fileServiceProvider, - submitCredentials, + submitStorage, + submitLlm, submitOAuth, isCreating, isStartingOAuth, @@ -227,7 +228,7 @@ const { :provider-name="seed.name" :provider-icon="seed.icon" :is-loading="isCreating" - @connect="submitCredentials" + @connect="submitStorage" /> @@ -235,7 +236,7 @@ const { v-model:open="llmOpen" :provider="llmProvider" :is-loading="isCreating" - @connect="submitCredentials" + @connect="submitLlm" /> diff --git a/packages/console/app/pages/w/[workspace]/integrations/providers.vue b/packages/console/app/pages/w/[workspace]/integrations/providers.vue new file mode 100644 index 00000000..4d5bdcfa --- /dev/null +++ b/packages/console/app/pages/w/[workspace]/integrations/providers.vue @@ -0,0 +1,234 @@ + + + diff --git a/packages/console/app/utils/connections/fileservice.ts b/packages/console/app/utils/connections/fileservice.ts index 8fc5ebcb..236b6308 100644 --- a/packages/console/app/utils/connections/fileservice.ts +++ b/packages/console/app/utils/connections/fileservice.ts @@ -1,13 +1,13 @@ -import type { FileProviders, Provider } from "@nvisy/sdk/datatypes"; +import type { FileProviders, FileServiceProvider } from "@nvisy/sdk/datatypes"; /** * Cloud file-service providers connected over OAuth (Google Drive, Dropbox, * OneDrive, Box). These differ from object stores and LLMs: they carry no * user-entered credentials, so they are connected by redirecting the user to * the provider's `authorizeUrl` (see `startFileServiceOAuth`) rather than - * through a credential form. Derived from the SDK's `Provider` type. + * through a credential form. Derived from the SDK's `FileServiceProvider` type. */ -export type FileProvider = Provider; +export type FileProvider = FileServiceProvider; /** * Maps an OAuth file-service provider to its key on the catalog's diff --git a/packages/console/i18n/locales/de.json b/packages/console/i18n/locales/de.json index ea34a93e..3c59af02 100644 --- a/packages/console/i18n/locales/de.json +++ b/packages/console/i18n/locales/de.json @@ -3,6 +3,7 @@ "tabs": { "connections": { "connections": "Verbindungen", + "providers": "Anbieter", "webhooks": "Webhooks", "explore": "Erkunden", "runs": "Ausführungen" @@ -1250,6 +1251,63 @@ "failed": "Verbindung konnte nicht abgeschlossen werden. Bitte erneut versuchen." } }, + "providers": { + "description": "{count} Anbieter konfiguriert | {count} Anbieter konfiguriert", + "types": { + "llm": "Sprachmodell", + "ner": "Entitätserkennung" + }, + "empty": { + "title": "Keine Inferenz-Anbieter", + "description": "Verbinden Sie ein Sprachmodell (OpenAI, Anthropic, Ollama) für Erkennung und Schwärzung." + }, + "table": { + "headers": { + "name": "Name", + "enabled": "Aktiviert", + "type": "Typ", + "updated": "Aktualisiert" + }, + "actions": { + "menu": "Aktionen", + "configure": "Konfigurieren", + "test": "Testen", + "delete": "Entfernen" + } + }, + "dialogs": { + "configure": { + "title": "{name} konfigurieren", + "description": "Aktualisieren Sie die Einstellungen des Anbieters.", + "nameLabel": "Anzeigename", + "namePlaceholder": "z. B. Produktion OpenAI", + "updateCredentials": "Zugangsdaten aktualisieren", + "credentialsHint": "Geschlossen lassen, um die gespeicherten Zugangsdaten beizubehalten.", + "cancel": "Abbrechen", + "save": "Änderungen speichern" + }, + "delete": { + "title": "{name} entfernen?", + "description": "Dieser Anbieter steht dann nicht mehr für Erkennung und Schwärzung zur Verfügung. Dies kann nicht rückgängig gemacht werden.", + "confirm": "Entfernen", + "cancel": "Abbrechen" + } + }, + "toast": { + "updated": "Anbieter aktualisiert", + "updateFailed": "Anbieter konnte nicht aktualisiert werden", + "deleted": "Anbieter entfernt", + "deleteFailed": "Anbieter konnte nicht entfernt werden", + "enabled": "Anbieter aktiviert", + "disabled": "Anbieter deaktiviert", + "testReachable": "Anbieter ist erreichbar", + "testUnreachable": "Anbieter ist nicht erreichbar", + "testFailed": "Anbieter konnte nicht getestet werden" + }, + "errors": { + "loadFailed": "Anbieter konnten nicht geladen werden" + } + }, "sidebar": { "overview": "Übersicht", "workspace": "Arbeitsbereich", diff --git a/packages/console/i18n/locales/en.json b/packages/console/i18n/locales/en.json index 66d2a0cb..2988c3eb 100644 --- a/packages/console/i18n/locales/en.json +++ b/packages/console/i18n/locales/en.json @@ -3,6 +3,7 @@ "tabs": { "connections": { "connections": "Connections", + "providers": "Providers", "webhooks": "Webhooks", "explore": "Explore", "runs": "Runs" @@ -1250,6 +1251,63 @@ "failed": "Couldn't finish connecting. Please try again." } }, + "providers": { + "description": "{count} provider configured | {count} providers configured", + "types": { + "llm": "Language model", + "ner": "Entity recognition" + }, + "empty": { + "title": "No inference providers", + "description": "Connect a language model (OpenAI, Anthropic, Ollama) to power detection and redaction." + }, + "table": { + "headers": { + "name": "Name", + "enabled": "Enabled", + "type": "Type", + "updated": "Updated" + }, + "actions": { + "menu": "Actions", + "configure": "Configure", + "test": "Test", + "delete": "Remove" + } + }, + "dialogs": { + "configure": { + "title": "Configure {name}", + "description": "Update the provider's settings.", + "nameLabel": "Display name", + "namePlaceholder": "e.g. Production OpenAI", + "updateCredentials": "Update credentials", + "credentialsHint": "Leave closed to keep the stored credentials.", + "cancel": "Cancel", + "save": "Save changes" + }, + "delete": { + "title": "Remove {name}?", + "description": "This provider will no longer be available for detection and redaction. This can't be undone.", + "confirm": "Remove", + "cancel": "Cancel" + } + }, + "toast": { + "updated": "Provider updated", + "updateFailed": "Failed to update provider", + "deleted": "Provider removed", + "deleteFailed": "Failed to remove provider", + "enabled": "Provider enabled", + "disabled": "Provider disabled", + "testReachable": "Provider is reachable", + "testUnreachable": "Provider is not reachable", + "testFailed": "Failed to test provider" + }, + "errors": { + "loadFailed": "Failed to load providers" + } + }, "sidebar": { "overview": "Overview", "workspace": "Workspace", diff --git a/packages/console/package.json b/packages/console/package.json index 76c733bd..0d0ecbfb 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -15,7 +15,7 @@ "@lucide/vue": "^1.41.0", "@nuxtjs/color-mode": "^4.0.0", "@nuxtjs/i18n": "^10.6.0", - "@nvisy/sdk": "^0.48.0", + "@nvisy/sdk": "^0.49.0", "@pinia/colada": "^1.4.4", "@pinia/colada-nuxt": "^1.1.0", "@pinia/nuxt": "^1.0.2", From 8ccd6c7a1583d1dc9b3498ab42f81ca379ca8f1a Mon Sep 17 00:00:00 2001 From: Oleh Martsokha Date: Wed, 9 Sep 2026 07:45:14 +0200 Subject: [PATCH 2/4] Global chat, studio unification, sidebar/integrations reorg, OneDrive resource token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Global workspace chat - A collapsible right-rail AI chat, mounted once in the shell layout and on every page, backed by the SDK's `client.chat.*` (sessions + streamed SSE replies). New `useChat` (sessions/history/streaming, workspace-switch reset in a detached scope, 409 → "connect a provider" CTA) and `useChatPanel` (open state + Cmd/Ctrl+J). - Extends the shell grid with a `[chat]` track (`--chat-w`) that reflows content; a mobile Sheet overlay. Header Sparkles toggle. - Studio unified onto it: dropped the stubbed studio chat panel; the studio inspector is now audit-only, and the wide full-screen audit gets a floating controls toolbar (pipeline + redaction) since the inspector collapses there. OneDrive import - Thread the picker's requested `resource` through to `getPickerToken` (SDK 0.49), minting a per-resource token so consumer OneDrive's two resources authenticate. Sidebar / integrations - Webhooks becomes its own Observability item (Webhooks · Analytics · Logs, tabs too), moved to `/webhooks`. Providers stays a tab under Integrations. - Runs folds into a wide "View runs" dialog on the Connections page (route removed); sync notifications deep-link to `/integrations?runs=1`. Fixes - Account set-a-password card: bottom-padding when the footer is hidden (no more clipping) and the button moved right. - Studio file tabs no longer use a fake "Loading..." string as the display name (it leaked into the redacted-file name); the loading state drives a proper label. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JcYYAsbEf75Lke291j9AYU --- .../console/app/components/layout/AppChat.vue | 61 +++ .../app/components/layout/AppHeader.vue | 15 + .../components/layout/chat/AppChatPanel.vue | 210 ++++++++++ .../app/components/layout/chat/ChatInput.vue | 54 +++ .../components/layout/chat/ChatMessage.vue | 104 +++++ .../layout/header/StudioFileTab.vue | 6 +- .../components/layout/sidebar/AppSidebar.vue | 7 + .../integrations/ConnectionRunsPanel.vue | 293 ++++++++++++++ .../components/pages/integrations/index.ts | 1 + .../pages/studio/chat/ChatMessage.vue | 165 -------- .../pages/studio/chat/MessageInput.vue | 112 ------ .../pages/studio/chat/StudioChatPanel.vue | 377 ------------------ .../app/components/pages/studio/chat/index.ts | 3 - .../studio/entities/StudioAuditPanel.vue | 87 +--- .../studio/entities/StudioAuditToolbar.vue | 149 +++++++ .../studio/entities/StudioDetectionBar.vue | 2 +- .../studio/entities/StudioRedactionFooter.vue | 103 +++++ .../components/pages/studio/entities/index.ts | 2 + .../app/components/pages/studio/index.ts | 1 - .../components/ui/sidebar/SidebarInset.vue | 4 +- .../components/ui/sidebar/SidebarProvider.vue | 44 +- .../app/components/ui/sidebar/utils.ts | 2 + packages/console/app/composables/useChat.ts | 265 ++++++++++++ .../console/app/composables/useChatPanel.ts | 43 ++ .../console/app/composables/useConnections.ts | 15 +- .../console/app/composables/useFileImport.ts | 16 +- .../console/app/composables/useSectionTabs.ts | 20 +- .../console/app/composables/useStudioFiles.ts | 5 +- .../app/composables/useStudioRedaction.ts | 3 +- packages/console/app/layouts/default.vue | 4 + .../console/app/pages/account/general.vue | 45 ++- .../w/[workspace]/integrations/index.vue | 43 +- .../pages/w/[workspace]/integrations/runs.vue | 305 -------------- .../app/pages/w/[workspace]/studio/index.vue | 122 +++--- .../{integrations => }/webhooks.vue | 4 +- .../app/utils/connections/pickers/onedrive.ts | 44 +- packages/console/app/utils/notifications.ts | 4 +- packages/console/i18n/locales/de.json | 81 ++-- packages/console/i18n/locales/en.json | 81 ++-- 39 files changed, 1640 insertions(+), 1262 deletions(-) create mode 100644 packages/console/app/components/layout/AppChat.vue create mode 100644 packages/console/app/components/layout/chat/AppChatPanel.vue create mode 100644 packages/console/app/components/layout/chat/ChatInput.vue create mode 100644 packages/console/app/components/layout/chat/ChatMessage.vue create mode 100644 packages/console/app/components/pages/integrations/ConnectionRunsPanel.vue delete mode 100644 packages/console/app/components/pages/studio/chat/ChatMessage.vue delete mode 100644 packages/console/app/components/pages/studio/chat/MessageInput.vue delete mode 100644 packages/console/app/components/pages/studio/chat/StudioChatPanel.vue delete mode 100644 packages/console/app/components/pages/studio/chat/index.ts create mode 100644 packages/console/app/components/pages/studio/entities/StudioAuditToolbar.vue create mode 100644 packages/console/app/components/pages/studio/entities/StudioRedactionFooter.vue create mode 100644 packages/console/app/composables/useChat.ts create mode 100644 packages/console/app/composables/useChatPanel.ts delete mode 100644 packages/console/app/pages/w/[workspace]/integrations/runs.vue rename packages/console/app/pages/w/[workspace]/{integrations => }/webhooks.vue (98%) diff --git a/packages/console/app/components/layout/AppChat.vue b/packages/console/app/components/layout/AppChat.vue new file mode 100644 index 00000000..c4142ea0 --- /dev/null +++ b/packages/console/app/components/layout/AppChat.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/packages/console/app/components/layout/AppHeader.vue b/packages/console/app/components/layout/AppHeader.vue index ee81f43f..8a429925 100644 --- a/packages/console/app/components/layout/AppHeader.vue +++ b/packages/console/app/components/layout/AppHeader.vue @@ -1,6 +1,8 @@ + + diff --git a/packages/console/app/components/layout/chat/ChatInput.vue b/packages/console/app/components/layout/chat/ChatInput.vue new file mode 100644 index 00000000..64e3e022 --- /dev/null +++ b/packages/console/app/components/layout/chat/ChatInput.vue @@ -0,0 +1,54 @@ + + +