diff --git a/src/pages/views/trustee/TrusteeAccountingSettingsView.tsx b/src/pages/views/trustee/TrusteeAccountingSettingsView.tsx index 66927da..b145a0d 100644 --- a/src/pages/views/trustee/TrusteeAccountingSettingsView.tsx +++ b/src/pages/views/trustee/TrusteeAccountingSettingsView.tsx @@ -6,7 +6,7 @@ * testing the connection, and removing the integration. */ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useCurrentInstance } from '../../../hooks/useCurrentInstance'; import { useApiRequest } from '../../../hooks/useApi'; import { useToast } from '../../../contexts/ToastContext'; @@ -35,6 +35,19 @@ export const TrusteeAccountingSettingsView: React.FC = () => { const [saving, setSaving] = useState(false); const [testing, setTesting] = useState(false); const [testResult, setTestResult] = useState<{ success: boolean; message?: string } | null>(null); + const [importing, setImporting] = useState(false); + const [importDone, setImportDone] = useState(false); + const [importResult, setImportResult] = useState | null>(null); + const [importStatus, setImportStatus] = useState | null>(null); + const [dateFrom, setDateFrom] = useState(''); + const [dateTo, setDateTo] = useState(''); + const mountedRef = useRef(true); + + useEffect(() => { + if (!importDone) return; + const t = setTimeout(() => { setImporting(false); setImportDone(false); }, 5000); + return () => clearTimeout(t); + }, [importDone]); const loadData = useCallback(async () => { if (!instanceId) return; @@ -62,8 +75,21 @@ export const TrusteeAccountingSettingsView: React.FC = () => { useEffect(() => { loadData(); + return () => { mountedRef.current = false; }; }, [loadData]); + const _loadImportStatus = useCallback(async () => { + if (!instanceId) return; + try { + const res = await request({ url: `/api/trustee/${instanceId}/accounting/import-status`, method: 'get' }); + if (mountedRef.current) setImportStatus(res.data); + } catch { /* ignore */ } + }, [instanceId, request]); + + useEffect(() => { + if (existingConfig?.configured) _loadImportStatus(); + }, [existingConfig, _loadImportStatus]); + const _getSelectedConnector = (): AccountingConnectorInfo | undefined => { return connectors.find(c => c.connectorType === selectedType); }; @@ -291,6 +317,109 @@ export const TrusteeAccountingSettingsView: React.FC = () => { )} + + {/* Step 4: Import Accounting Data */} + {existingConfig?.configured && ( +
+
4
+
+

Buchhaltungsdaten importieren

+

+ Kontenplan, Buchungen, Kontakte und Salden aus dem Buchhaltungssystem einlesen. + Diese Daten stehen anschliessend im AI Workspace fuer Analysen zur Verfuegung. +

+ +
+
+ + setDateFrom(e.target.value)} style={{ width: '160px' }} /> +
+
+ + setDateTo(e.target.value)} style={{ width: '160px' }} /> +
+
+
+ {[ + { label: 'YTD', from: `${new Date().getFullYear()}-01-01`, to: new Date().toISOString().slice(0, 10) }, + { + label: 'Letztes Jahr', + from: `${new Date().getFullYear() - 1}-01-01`, + to: `${new Date().getFullYear() - 1}-12-31`, + }, + { + label: 'Letzter Monat', + from: (() => { const d = new Date(); d.setDate(1); d.setMonth(d.getMonth() - 1); return d.toISOString().slice(0, 10); })(), + to: (() => { const d = new Date(); d.setDate(0); return d.toISOString().slice(0, 10); })(), + }, + ].map(s => ( + + ))} +
+ + + + {importResult && !importResult.errors?.length && ( +
+ Import abgeschlossen in {importResult.durationSeconds}s: + {' '}{importResult.accounts} Konten, {importResult.journalEntries} Buchungen ({importResult.journalLines} Zeilen), + {' '}{importResult.contacts} Kontakte, {importResult.accountBalances} Salden +
+ )} + + {importStatus && (importStatus.accounts > 0 || importStatus.journalEntries > 0) && ( +
+ Aktueller Datenbestand:{' '} + {importStatus.accounts} Konten, {importStatus.journalEntries} Buchungen, + {' '}{importStatus.journalLines} Zeilen, {importStatus.contacts} Kontakte, + {' '}{importStatus.accountBalances} Salden + {importStatus.lastSyncAt && ( + <> · Letzter Import: {new Date(importStatus.lastSyncAt * 1000).toLocaleString()} + )} +
+ )} +
+
+ )} ); diff --git a/src/pages/views/workspace/DataSourcePanel.tsx b/src/pages/views/workspace/DataSourcePanel.tsx index d5be787..656472f 100644 --- a/src/pages/views/workspace/DataSourcePanel.tsx +++ b/src/pages/views/workspace/DataSourcePanel.tsx @@ -11,7 +11,8 @@ import React, { useEffect, useState, useCallback, useRef } from 'react'; import api from '../../../api'; -import type { DataSource } from './useWorkspace'; +import { getPageIcon } from '../../../config/pageRegistry'; +import type { DataSource, FeatureDataSource } from './useWorkspace'; /* ─── Types ─────────────────────────────────────────────────────────── */ @@ -29,10 +30,30 @@ interface TreeNode { authority?: string; } +interface FeatureConnectionNode { + featureInstanceId: string; + featureCode: string; + label: string; + icon: string; + tableCount: number; + expanded: boolean; + loading: boolean; + tables: FeatureTableNode[] | null; +} + +interface FeatureTableNode { + objectKey: string; + tableName: string; + label: Record; + fields: string[]; +} + interface DataSourcePanelProps { instanceId: string; dataSources: DataSource[]; + featureDataSources: FeatureDataSource[]; onRefresh: () => void; + onRefreshFeatureDataSources: () => void; } /* ─── Icons ─────────────────────────────────────────────────────────── */ @@ -86,11 +107,16 @@ function _getSourceIcon(sourceType: string): string { export const DataSourcePanel: React.FC = ({ instanceId, dataSources, + featureDataSources, onRefresh, + onRefreshFeatureDataSources, }) => { const [tree, setTree] = useState([]); const [loadingRoot, setLoadingRoot] = useState(false); const [addingPath, setAddingPath] = useState(null); + const [featureTree, setFeatureTree] = useState([]); + const [loadingFeatures, setLoadingFeatures] = useState(false); + const [addingFeatureKey, setAddingFeatureKey] = useState(null); const mountedRef = useRef(true); useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); @@ -205,6 +231,110 @@ export const DataSourcePanel: React.FC = ({ ); }, [dataSources]); + /* ── Feature Connections: Load Level 1 ── */ + const _loadFeatureConnections = useCallback(() => { + if (!instanceId) return; + setLoadingFeatures(true); + api.get(`/api/workspace/${instanceId}/feature-connections`) + .then(res => { + if (!mountedRef.current) return; + const conns = res.data.featureConnections || []; + setFeatureTree(conns.map((c: any) => ({ + featureInstanceId: c.featureInstanceId, + featureCode: c.featureCode, + label: c.label, + icon: c.icon || '\uD83D\uDDC3\uFE0F', + tableCount: c.tableCount || 0, + expanded: false, + loading: false, + tables: null, + }))); + }) + .catch(() => { if (mountedRef.current) setFeatureTree([]); }) + .finally(() => { if (mountedRef.current) setLoadingFeatures(false); }); + }, [instanceId]); + + useEffect(() => { _loadFeatureConnections(); }, [_loadFeatureConnections]); + + /* ── Feature Connections: Toggle expand ── */ + const _toggleFeatureNode = useCallback(async (node: FeatureConnectionNode) => { + if (node.expanded) { + setFeatureTree(prev => prev.map(n => + n.featureInstanceId === node.featureInstanceId ? { ...n, expanded: false } : n + )); + return; + } + + if (node.tables !== null) { + setFeatureTree(prev => prev.map(n => + n.featureInstanceId === node.featureInstanceId ? { ...n, expanded: true } : n + )); + return; + } + + setFeatureTree(prev => prev.map(n => + n.featureInstanceId === node.featureInstanceId ? { ...n, loading: true, expanded: true } : n + )); + + try { + const res = await api.get(`/api/workspace/${instanceId}/feature-connections/${node.featureInstanceId}/tables`); + const tables: FeatureTableNode[] = (res.data.tables || []).map((t: any) => ({ + objectKey: t.objectKey, + tableName: t.tableName, + label: t.label || {}, + fields: t.fields || [], + })); + if (mountedRef.current) { + setFeatureTree(prev => prev.map(n => + n.featureInstanceId === node.featureInstanceId ? { ...n, loading: false, tables } : n + )); + } + } catch { + if (mountedRef.current) { + setFeatureTree(prev => prev.map(n => + n.featureInstanceId === node.featureInstanceId ? { ...n, loading: false, tables: [] } : n + )); + } + } + }, [instanceId]); + + /* ── Feature: Add table as FeatureDataSource ── */ + const _addFeatureTable = useCallback(async (node: FeatureConnectionNode, table: FeatureTableNode) => { + const key = `${node.featureInstanceId}-${table.tableName}`; + setAddingFeatureKey(key); + try { + await api.post(`/api/workspace/${instanceId}/feature-datasources`, { + featureInstanceId: node.featureInstanceId, + featureCode: node.featureCode, + tableName: table.tableName, + objectKey: table.objectKey, + label: table.label?.en || table.label?.de || table.tableName, + }); + onRefreshFeatureDataSources(); + } catch (err) { + console.error('Failed to add feature data source:', err); + } finally { + if (mountedRef.current) setAddingFeatureKey(null); + } + }, [instanceId, onRefreshFeatureDataSources]); + + /* ── Feature: Remove FeatureDataSource ── */ + const _removeFeatureDataSource = useCallback(async (fdsId: string) => { + try { + await api.delete(`/api/workspace/${instanceId}/feature-datasources/${fdsId}`); + onRefreshFeatureDataSources(); + } catch (err) { + console.error('Failed to remove feature data source:', err); + } + }, [instanceId, onRefreshFeatureDataSources]); + + /* ── Feature: check if table already added ── */ + const _isFeatureTableAdded = useCallback((featureInstanceId: string, tableName: string): boolean => { + return featureDataSources.some(fds => + fds.featureInstanceId === featureInstanceId && fds.tableName === tableName, + ); + }, [featureDataSources]); + return (
{/* Active DataSources */} @@ -217,7 +347,7 @@ export const DataSourcePanel: React.FC = ({ const connColor = _getSourceColor(ds.sourceType); const connNode = tree.find(n => n.connectionId === ds.connectionId); const connLabel = connNode?.label || ds.connectionId; - const fullPath = `${connLabel} › ${ds.sourceType} › ${ds.path}`; + const folder = ds.label || ds.path || ds.id; return (
= ({ background: `${connColor}18`, borderLeft: `3px solid ${connColor}`, fontSize: 12, - }} title={fullPath}> + }} title={`${connLabel} – ${ds.path || ds.label}`}> {_getSourceIcon(ds.sourceType)} - {ds.label} + {connLabel} – {folder} +
+ ); })} +
+
+ )} + + {/* Feature Connections Tree */} +
+ + Feature Data + + +
+ + {loadingFeatures && featureTree.length === 0 && ( +
+ Loading feature instances... +
+ )} + + {!loadingFeatures && featureTree.length === 0 && ( +
+ No feature instances found. +
+ )} + + {featureTree.map(fNode => ( + <_FeatureNodeView + key={fNode.featureInstanceId} + node={fNode} + onToggle={_toggleFeatureNode} + onAddTable={_addFeatureTable} + isTableAdded={_isFeatureTableAdded} + addingKey={addingFeatureKey} + /> + ))}
); }; @@ -391,6 +596,129 @@ const _TreeNodeView: React.FC = ({ ); }; +/* ─── FeatureNodeView (feature instance + tables) ─────────────────── */ + +interface FeatureNodeViewProps { + node: FeatureConnectionNode; + onToggle: (node: FeatureConnectionNode) => void; + onAddTable: (node: FeatureConnectionNode, table: FeatureTableNode) => void; + isTableAdded: (featureInstanceId: string, tableName: string) => boolean; + addingKey: string | null; +} + +const _FeatureNodeView: React.FC = ({ + node, onToggle, onAddTable, isTableAdded, addingKey, +}) => { + const [hovered, setHovered] = useState(false); + const chevron = node.expanded ? '\u25BE' : '\u25B8'; + + return ( +
+
onToggle(node)} + onMouseEnter={() => setHovered(true)} + onMouseLeave={() => setHovered(false)} + style={{ + display: 'flex', alignItems: 'center', gap: 4, + paddingLeft: 4, paddingRight: 4, paddingTop: 3, paddingBottom: 3, + cursor: 'pointer', borderRadius: 3, + background: hovered ? 'var(--hover-bg, #f5f5f5)' : 'transparent', + transition: 'background 0.1s', userSelect: 'none', + }} + > + + {node.loading ? _Spinner() : chevron} + + + {getPageIcon(`feature.${node.featureCode}`) || '\uD83D\uDDC3\uFE0F'} + + + {node.label} + + + {node.tableCount} tables + +
+ + {node.expanded && node.tables && node.tables.length > 0 && ( +
+ {node.tables.map(table => ( + <_FeatureTableRow + key={table.objectKey} + featureNode={node} + table={table} + onAdd={onAddTable} + isAdded={isTableAdded(node.featureInstanceId, table.tableName)} + isAdding={addingKey === `${node.featureInstanceId}-${table.tableName}`} + /> + ))} +
+ )} + + {node.expanded && node.tables && node.tables.length === 0 && !node.loading && ( +
+ (no tables) +
+ )} +
+ ); +}; + +interface FeatureTableRowProps { + featureNode: FeatureConnectionNode; + table: FeatureTableNode; + onAdd: (node: FeatureConnectionNode, table: FeatureTableNode) => void; + isAdded: boolean; + isAdding: boolean; +} + +const _FeatureTableRow: React.FC = ({ + featureNode, table, onAdd, isAdded, isAdding, +}) => { + const [hovered, setHovered] = useState(false); + const tableLabel = table.label?.en || table.label?.de || table.tableName; + + return ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + style={{ + display: 'flex', alignItems: 'center', gap: 4, + paddingLeft: 36, paddingRight: 4, paddingTop: 3, paddingBottom: 3, + borderRadius: 3, + background: hovered ? 'var(--hover-bg, #f5f5f5)' : 'transparent', + transition: 'background 0.1s', userSelect: 'none', + }} + title={`${table.tableName}: ${table.fields.join(', ')}`} + > + {'\uD83D\uDCC1'} + + {tableLabel} + + {hovered && !isAdded && ( + + )} + {isAdded && ( + + {'\u2713'} + + )} +
+ ); +}; + /* ─── Spinner (inline) ──────────────────────────────────────────────── */ function _Spinner(): React.ReactElement { diff --git a/src/pages/views/workspace/WorkspaceInput.tsx b/src/pages/views/workspace/WorkspaceInput.tsx index 56bb812..0a091c4 100644 --- a/src/pages/views/workspace/WorkspaceInput.tsx +++ b/src/pages/views/workspace/WorkspaceInput.tsx @@ -5,8 +5,9 @@ import React, { useState, useCallback, useRef, useEffect } from 'react'; import { ProviderMultiSelect } from '../../../components/ProviderSelector'; +import { getPageIcon } from '../../../config/pageRegistry'; import { useVoiceStream } from '../../../hooks/useSpeechAudioCapture'; -import type { WorkspaceFile, DataSource } from './useWorkspace'; +import type { WorkspaceFile, DataSource, FeatureDataSource } from './useWorkspace'; const _STT_LANGUAGES = [ { code: 'de-DE', label: 'Deutsch' }, @@ -37,11 +38,12 @@ interface TreeItemDrop { interface WorkspaceInputProps { instanceId: string; - onSend: (prompt: string, fileIds?: string[], dataSourceIds?: string[]) => void; + onSend: (prompt: string, fileIds?: string[], dataSourceIds?: string[], featureDataSourceIds?: string[]) => void; isProcessing: boolean; onStop: () => void; files: WorkspaceFile[]; dataSources: DataSource[]; + featureDataSources?: FeatureDataSource[]; pendingFiles?: PendingFile[]; onRemovePendingFile?: (fileId: string) => void; onFileUploadClick?: () => void; @@ -60,6 +62,7 @@ export const WorkspaceInput: React.FC = ({ onStop, files, dataSources, + featureDataSources = [], pendingFiles = [], onRemovePendingFile, onFileUploadClick, @@ -79,6 +82,7 @@ export const WorkspaceInput: React.FC = ({ const [showLangPicker, setShowLangPicker] = useState(false); const [attachedFileIds, setAttachedFileIds] = useState([]); const [attachedDataSourceIds, setAttachedDataSourceIds] = useState([]); + const [attachedFeatureDataSourceIds, setAttachedFeatureDataSourceIds] = useState([]); const textareaRef = useRef(null); const promptBeforeVoiceRef = useRef(''); const finalizedTextRef = useRef(''); @@ -112,12 +116,12 @@ export const WorkspaceInput: React.FC = ({ if (!trimmed || isProcessing) return; const inlineFileIds = _extractFileRefs(trimmed); const allFileIds = [...new Set([...attachedFileIds, ...inlineFileIds])]; - onSend(trimmed, allFileIds, attachedDataSourceIds); + onSend(trimmed, allFileIds, attachedDataSourceIds, attachedFeatureDataSourceIds); setPrompt(''); setShowAutocomplete(false); setShowSourcePicker(false); setAttachedFileIds([]); - }, [prompt, isProcessing, _extractFileRefs, attachedFileIds, attachedDataSourceIds, onSend]); + }, [prompt, isProcessing, _extractFileRefs, attachedFileIds, attachedDataSourceIds, attachedFeatureDataSourceIds, onSend]); const _handleKeyDown = useCallback( (e: React.KeyboardEvent) => { @@ -178,6 +182,12 @@ export const WorkspaceInput: React.FC = ({ ); }, []); + const _toggleFeatureDataSource = useCallback((fdsId: string) => { + setAttachedFeatureDataSourceIds(prev => + prev.includes(fdsId) ? prev.filter(id => id !== fdsId) : [...prev, fdsId], + ); + }, []); + const _buildPromptFromRefs = useCallback(() => { const parts = [ promptBeforeVoiceRef.current, @@ -237,7 +247,7 @@ export const WorkspaceInput: React.FC = ({ ? files.filter(f => f.fileName.toLowerCase().includes(autocompleteFilter)) : []; - const hasAttachments = attachedFileIds.length > 0 || attachedDataSourceIds.length > 0; + const hasAttachments = attachedFileIds.length > 0 || attachedDataSourceIds.length > 0 || attachedFeatureDataSourceIds.length > 0; const _horizontalPadding = isMobile ? 12 : 24; const _controlSize = isMobile ? 38 : 40; @@ -371,7 +381,7 @@ export const WorkspaceInput: React.FC = ({ background: '#e8f5e9', color: '#2e7d32', fontWeight: 500, }} > - 🔗 {ds?.label || dsId} + 🔗 {ds?.label || ds?.path || dsId} + + ); + })} )} @@ -480,8 +516,8 @@ export const WorkspaceInput: React.FC = ({ title="Datenquellen anhängen" style={{ width: _controlSize, height: _controlSize, borderRadius: 8, border: '1px solid var(--border-color, #ddd)', - background: attachedDataSourceIds.length > 0 ? '#e8f5e9' : 'var(--secondary-bg, #f5f5f5)', - color: attachedDataSourceIds.length > 0 ? '#2e7d32' : '#666', + background: (attachedDataSourceIds.length + attachedFeatureDataSourceIds.length) > 0 ? '#e8f5e9' : 'var(--secondary-bg, #f5f5f5)', + color: (attachedDataSourceIds.length + attachedFeatureDataSourceIds.length) > 0 ? '#2e7d32' : '#666', cursor: isProcessing ? 'not-allowed' : 'pointer', fontSize: 16, display: 'flex', alignItems: 'center', justifyContent: 'center', opacity: isProcessing ? 0.5 : 1, @@ -489,14 +525,14 @@ export const WorkspaceInput: React.FC = ({ }} > 🔗 - {attachedDataSourceIds.length > 0 && ( + {(attachedDataSourceIds.length + attachedFeatureDataSourceIds.length) > 0 && ( - {attachedDataSourceIds.length} + {attachedDataSourceIds.length + attachedFeatureDataSourceIds.length} )} @@ -539,6 +575,45 @@ export const WorkspaceInput: React.FC = ({ ); })} + {featureDataSources.length > 0 && ( + <> +
+ Feature Data Sources +
+ {featureDataSources.map(fds => { + const isSelected = attachedFeatureDataSourceIds.includes(fds.id); + return ( +
_toggleFeatureDataSource(fds.id)} + style={{ + padding: '8px 12px', cursor: 'pointer', fontSize: 13, + display: 'flex', alignItems: 'center', gap: 8, + background: isSelected ? '#f3e5f5' : 'transparent', + }} + onMouseEnter={e => { if (!isSelected) e.currentTarget.style.background = '#f5f5f5'; }} + onMouseLeave={e => { if (!isSelected) e.currentTarget.style.background = ''; }} + > + + {isSelected ? '✓' : ''} + + + {getPageIcon(`feature.${fds.featureCode}`) || '\uD83D\uDDC3\uFE0F'} + + + {fds.label || fds.featureCode} – {fds.tableName} + +
+ ); + })} + + )} )} diff --git a/src/pages/views/workspace/WorkspacePage.tsx b/src/pages/views/workspace/WorkspacePage.tsx index e0935a4..0466ee8 100644 --- a/src/pages/views/workspace/WorkspacePage.tsx +++ b/src/pages/views/workspace/WorkspacePage.tsx @@ -240,7 +240,9 @@ export const WorkspacePage: React.FC = ({ persistentInstance )} @@ -395,15 +397,16 @@ export const WorkspacePage: React.FC = ({ persistentInstance /> { + onSend={(prompt, fileIds, dataSourceIds, featureDataSourceIds) => { const allFileIds = [...new Set([...pendingFiles.map(f => f.fileId), ...(fileIds || [])])]; - workspace.sendMessage(prompt, allFileIds, dataSourceIds, selectedProviders); + workspace.sendMessage(prompt, allFileIds, dataSourceIds, selectedProviders, featureDataSourceIds); setPendingFiles([]); }} isProcessing={workspace.isProcessing} onStop={workspace.stopProcessing} files={workspace.files} dataSources={workspace.dataSources} + featureDataSources={workspace.featureDataSources} pendingFiles={pendingFiles} onRemovePendingFile={_handleRemovePendingFile} onFileUploadClick={() => fileInputRef.current?.click()} diff --git a/src/pages/views/workspace/useWorkspace.ts b/src/pages/views/workspace/useWorkspace.ts index b759a4e..d974955 100644 --- a/src/pages/views/workspace/useWorkspace.ts +++ b/src/pages/views/workspace/useWorkspace.ts @@ -56,6 +56,17 @@ export interface DataSource { label: string; } +export interface FeatureDataSource { + id: string; + featureInstanceId: string; + featureCode: string; + tableName: string; + objectKey: string; + label: string; + mandateId: string; + workspaceInstanceId: string; +} + export interface FileEditProposal { id: string; fileId: string; @@ -78,13 +89,15 @@ export interface DataSourceAccessEvent { interface UseWorkspaceReturn { messages: Message[]; isProcessing: boolean; - sendMessage: (prompt: string, fileIds?: string[], dataSourceIds?: string[], allowedProviders?: string[]) => void; + sendMessage: (prompt: string, fileIds?: string[], dataSourceIds?: string[], allowedProviders?: string[], featureDataSourceIds?: string[]) => void; stopProcessing: () => void; loadWorkflow: (workflowId: string) => void; resetToNew: () => void; files: WorkspaceFile[]; folders: WorkspaceFolder[]; dataSources: DataSource[]; + featureDataSources: FeatureDataSource[]; + refreshFeatureDataSources: () => void; agentProgress: AgentProgress | null; toolActivities: ToolActivity[]; pendingEdits: FileEditProposal[]; @@ -104,6 +117,7 @@ export function useWorkspace(instanceId: string): UseWorkspaceReturn { const [files, setFiles] = useState([]); const [folders, setFolders] = useState([]); const [dataSources, setDataSources] = useState([]); + const [featureDataSources, setFeatureDataSources] = useState([]); const [agentProgress, setAgentProgress] = useState(null); const [toolActivities, setToolActivities] = useState([]); const [pendingEdits, setPendingEdits] = useState([]); @@ -133,12 +147,20 @@ export function useWorkspace(instanceId: string): UseWorkspaceReturn { .catch(() => {}); }, [instanceId]); + const refreshFeatureDataSources = useCallback(() => { + if (!instanceId) return; + api.get(`/api/workspace/${instanceId}/feature-datasources`) + .then(res => setFeatureDataSources(res.data.featureDataSources || [])) + .catch(() => {}); + }, [instanceId]); + useEffect(() => { if (!instanceId) return; refreshFiles(); refreshFolders(); refreshDataSources(); - }, [instanceId, refreshFiles, refreshFolders, refreshDataSources]); + refreshFeatureDataSources(); + }, [instanceId, refreshFiles, refreshFolders, refreshDataSources, refreshFeatureDataSources]); const loadWorkflow = useCallback((wfId: string) => { if (!instanceId || !wfId) return; @@ -173,7 +195,7 @@ export function useWorkspace(instanceId: string): UseWorkspaceReturn { }, []); const sendMessage = useCallback( - (prompt: string, fileIds: string[] = [], dataSourceIds: string[] = [], allowedProviders: string[] = []) => { + (prompt: string, fileIds: string[] = [], dataSourceIds: string[] = [], allowedProviders: string[] = [], featureDataSourceIds: string[] = []) => { if (!instanceId || isProcessing) return; setIsProcessing(true); @@ -202,6 +224,7 @@ export function useWorkspace(instanceId: string): UseWorkspaceReturn { prompt, fileIds, dataSourceIds, + featureDataSourceIds, userLanguage: navigator.language?.slice(0, 2) || 'en', }; if (workflowId) { @@ -415,6 +438,8 @@ export function useWorkspace(instanceId: string): UseWorkspaceReturn { files, folders, dataSources, + featureDataSources, + refreshFeatureDataSources, agentProgress, toolActivities, pendingEdits,