Merge pull request #19 from valueonag/feat/code-editor
Feat/code editor
This commit is contained in:
commit
735a6f3d3b
9 changed files with 616 additions and 22 deletions
|
|
@ -12,7 +12,7 @@
|
|||
*/
|
||||
|
||||
import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react';
|
||||
import { FaFolder, FaFolderOpen, FaPlus, FaPen, FaTrash, FaChevronRight, FaGlobe, FaSyncAlt } from 'react-icons/fa';
|
||||
import { FaFolder, FaFolderOpen, FaPlus, FaPen, FaTrash, FaChevronRight, FaGlobe, FaSyncAlt, FaDownload } from 'react-icons/fa';
|
||||
import styles from './FolderTree.module.css';
|
||||
|
||||
/* ── Public types ──────────────────────────────────────────────────────── */
|
||||
|
|
@ -61,6 +61,7 @@ export interface FolderTreeProps {
|
|||
onDeleteFile?: (fileId: string) => Promise<void>;
|
||||
onDeleteFiles?: (fileIds: string[]) => Promise<void>;
|
||||
onDeleteFolders?: (folderIds: string[]) => Promise<void>;
|
||||
onDownloadFolder?: (folderId: string, folderName: string) => Promise<void>;
|
||||
}
|
||||
|
||||
/* ── Helpers ───────────────────────────────────────────────────────────── */
|
||||
|
|
@ -285,12 +286,14 @@ interface TreeNodeProps {
|
|||
onMoveFolders?: (folderIds: string[], targetParentId: string | null) => Promise<void>;
|
||||
onMoveFile?: (fileId: string, targetFolderId: string | null) => Promise<void>;
|
||||
onMoveFiles?: (fileIds: string[], targetFolderId: string | null) => Promise<void>;
|
||||
onDownloadFolder?: (folderId: string, folderName: string) => Promise<void>;
|
||||
}
|
||||
|
||||
function _TreeNode({
|
||||
node, depth, selectedFolderId, expandedIds, showFiles, filesByFolder, sel,
|
||||
onToggle, onSelect,
|
||||
onCreateFolder, onRenameFolder, onDeleteFolder, onMoveFolder, onMoveFolders, onMoveFile, onMoveFiles,
|
||||
onDownloadFolder,
|
||||
}: TreeNodeProps) {
|
||||
const [renaming, setRenaming] = useState(false);
|
||||
const [renameValue, setRenameValue] = useState(node.name);
|
||||
|
|
@ -436,6 +439,11 @@ function _TreeNode({
|
|||
<span className={styles.folderName}>{node.name}</span>
|
||||
)}
|
||||
<span className={styles.actions}>
|
||||
{onDownloadFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && (
|
||||
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); onDownloadFolder(node.id, node.name); }} title="Ordner herunterladen (ZIP)">
|
||||
<FaDownload />
|
||||
</button>
|
||||
)}
|
||||
{onCreateFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && (
|
||||
<button className={styles.actionBtn} onClick={_handleAdd} title="Neuer Unterordner">
|
||||
<FaPlus />
|
||||
|
|
@ -489,6 +497,7 @@ function _TreeNode({
|
|||
onMoveFolders={onMoveFolders}
|
||||
onMoveFile={onMoveFile}
|
||||
onMoveFiles={onMoveFiles}
|
||||
onDownloadFolder={onDownloadFolder}
|
||||
/>
|
||||
))}
|
||||
{folderFiles.map((file) => (
|
||||
|
|
@ -507,7 +516,7 @@ export default function FolderTree({
|
|||
selectedItemIds: externalSelectedIds, onSelectionChange,
|
||||
expandedIds: externalExpandedIds, onToggleExpand,
|
||||
onCreateFolder, onRenameFolder, onDeleteFolder, onMoveFolder, onMoveFolders, onMoveFile, onMoveFiles,
|
||||
onRenameFile, onDeleteFile, onDeleteFiles, onDeleteFolders, onRefresh,
|
||||
onRenameFile, onDeleteFile, onDeleteFiles, onDeleteFolders, onRefresh, onDownloadFolder,
|
||||
}: FolderTreeProps) {
|
||||
const [internalExpandedIds, setInternalExpandedIds] = useState<Set<string>>(new Set());
|
||||
const [rootDropOver, setRootDropOver] = useState(false);
|
||||
|
|
@ -720,6 +729,7 @@ export default function FolderTree({
|
|||
onMoveFolders={onMoveFolders}
|
||||
onMoveFile={onMoveFile}
|
||||
onMoveFiles={onMoveFiles}
|
||||
onDownloadFolder={onDownloadFolder}
|
||||
/>
|
||||
))}
|
||||
{rootFiles.map((file) => (
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ interface FileContextType {
|
|||
handleMoveFile: (fileId: string, targetFolderId: string | null) => Promise<void>;
|
||||
handleMoveFiles: (fileIds: string[], targetFolderId: string | null) => Promise<void>;
|
||||
handleMoveFolders: (folderIds: string[], targetParentId: string | null) => Promise<void>;
|
||||
handleDownloadFolder: (folderId: string, folderName: string) => Promise<void>;
|
||||
expandedFolderIds: Set<string>;
|
||||
toggleFolderExpanded: (id: string) => void;
|
||||
}
|
||||
|
|
@ -122,6 +123,24 @@ export function FileProvider({ children }: { children: React.ReactNode }) {
|
|||
await refreshFolders();
|
||||
}, [refreshFolders]);
|
||||
|
||||
const handleDownloadFolder = useCallback(async (folderId: string, folderName: string) => {
|
||||
try {
|
||||
const response = await api.get(`/api/files/folders/${folderId}/download`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
const url = window.URL.createObjectURL(response.data);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', `${folderName}.zip`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (err) {
|
||||
console.error('Failed to download folder:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// ── File operations ────────────────────────────────────────────────────
|
||||
|
||||
const handleFileUpload = useCallback(async (file: File, workflowId?: string) => {
|
||||
|
|
@ -174,6 +193,7 @@ export function FileProvider({ children }: { children: React.ReactNode }) {
|
|||
handleMoveFile,
|
||||
handleMoveFiles,
|
||||
handleMoveFolders,
|
||||
handleDownloadFolder,
|
||||
expandedFolderIds,
|
||||
toggleFolderExpanded,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ export const FilesPage: React.FC = () => {
|
|||
handleMoveFolders,
|
||||
handleMoveFile,
|
||||
handleMoveFiles: contextMoveFiles,
|
||||
handleDownloadFolder,
|
||||
expandedFolderIds,
|
||||
toggleFolderExpanded,
|
||||
} = useFileContext();
|
||||
|
|
@ -367,6 +368,7 @@ export const FilesPage: React.FC = () => {
|
|||
onDeleteFile={_handleDeleteTreeFile}
|
||||
onDeleteFiles={_handleDeleteTreeFiles}
|
||||
onDeleteFolders={_handleDeleteTreeFolders}
|
||||
onDownloadFolder={handleDownloadFolder}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Record<string, any> | null>(null);
|
||||
const [importStatus, setImportStatus] = useState<Record<string, any> | 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 = () => {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 4: Import Accounting Data */}
|
||||
{existingConfig?.configured && (
|
||||
<div className={styles.setupStep}>
|
||||
<div className={styles.stepNumber}>4</div>
|
||||
<div className={styles.stepContent}>
|
||||
<h4>Buchhaltungsdaten importieren</h4>
|
||||
<p style={{ fontSize: '0.85rem', color: 'var(--text-secondary)', marginBottom: '0.75rem' }}>
|
||||
Kontenplan, Buchungen, Kontakte und Salden aus dem Buchhaltungssystem einlesen.
|
||||
Diese Daten stehen anschliessend im AI Workspace fuer Analysen zur Verfuegung.
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', gap: '0.75rem', marginBottom: '0.5rem', flexWrap: 'wrap', alignItems: 'flex-end' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '0.8rem', marginBottom: '0.2rem' }}>Von (optional)</label>
|
||||
<input type="date" className={styles.folderSelect} value={dateFrom} onChange={e => setDateFrom(e.target.value)} style={{ width: '160px' }} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '0.8rem', marginBottom: '0.2rem' }}>Bis (optional)</label>
|
||||
<input type="date" className={styles.folderSelect} value={dateTo} onChange={e => setDateTo(e.target.value)} style={{ width: '160px' }} />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '0.4rem', marginBottom: '0.75rem', flexWrap: 'wrap' }}>
|
||||
{[
|
||||
{ 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 => (
|
||||
<button
|
||||
key={s.label}
|
||||
type="button"
|
||||
className={styles.secondaryButton}
|
||||
style={{ fontSize: '0.75rem', padding: '0.25rem 0.6rem', minWidth: 0 }}
|
||||
onClick={() => { setDateFrom(s.from); setDateTo(s.to); }}
|
||||
>
|
||||
{s.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className={styles.primaryButton}
|
||||
disabled={importing}
|
||||
onClick={async () => {
|
||||
if (!instanceId) return;
|
||||
setImporting(true);
|
||||
setImportResult(null);
|
||||
try {
|
||||
const body: Record<string, string> = {};
|
||||
if (dateFrom) body.dateFrom = dateFrom;
|
||||
if (dateTo) body.dateTo = dateTo;
|
||||
const res = await request({ url: `/api/trustee/${instanceId}/accounting/import-data`, method: 'post', data: body });
|
||||
if (mountedRef.current) {
|
||||
setImportResult(res.data);
|
||||
if (res.data.errors?.length) {
|
||||
showError('Import teilweise fehlgeschlagen', res.data.errors.join('; '));
|
||||
} else {
|
||||
showSuccess('Import abgeschlossen',
|
||||
`${res.data.accounts || 0} Konten, ${res.data.journalEntries || 0} Buchungen, ` +
|
||||
`${res.data.contacts || 0} Kontakte, ${res.data.accountBalances || 0} Salden importiert.`);
|
||||
}
|
||||
_loadImportStatus();
|
||||
}
|
||||
} catch (err: any) {
|
||||
showError('Import fehlgeschlagen', err.response?.data?.detail || err.message || 'Unbekannter Fehler');
|
||||
} finally {
|
||||
setImportDone(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{importing ? 'Importiere...' : 'Daten jetzt einlesen'}
|
||||
</button>
|
||||
|
||||
{importResult && !importResult.errors?.length && (
|
||||
<div className={styles.successMessage} style={{ marginTop: '0.75rem' }}>
|
||||
Import abgeschlossen in {importResult.durationSeconds}s:
|
||||
{' '}{importResult.accounts} Konten, {importResult.journalEntries} Buchungen ({importResult.journalLines} Zeilen),
|
||||
{' '}{importResult.contacts} Kontakte, {importResult.accountBalances} Salden
|
||||
</div>
|
||||
)}
|
||||
|
||||
{importStatus && (importStatus.accounts > 0 || importStatus.journalEntries > 0) && (
|
||||
<div style={{ marginTop: '0.75rem', fontSize: '0.85rem', color: 'var(--text-secondary)' }}>
|
||||
<strong>Aktueller Datenbestand:</strong>{' '}
|
||||
{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()}</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<string, string>;
|
||||
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<DataSourcePanelProps> = ({
|
||||
instanceId,
|
||||
dataSources,
|
||||
featureDataSources,
|
||||
onRefresh,
|
||||
onRefreshFeatureDataSources,
|
||||
}) => {
|
||||
const [tree, setTree] = useState<TreeNode[]>([]);
|
||||
const [loadingRoot, setLoadingRoot] = useState(false);
|
||||
const [addingPath, setAddingPath] = useState<string | null>(null);
|
||||
const [featureTree, setFeatureTree] = useState<FeatureConnectionNode[]>([]);
|
||||
const [loadingFeatures, setLoadingFeatures] = useState(false);
|
||||
const [addingFeatureKey, setAddingFeatureKey] = useState<string | null>(null);
|
||||
const mountedRef = useRef(true);
|
||||
useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []);
|
||||
|
||||
|
|
@ -205,6 +231,110 @@ export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
|
|||
);
|
||||
}, [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 (
|
||||
<div style={{ padding: 8, fontSize: 13 }}>
|
||||
{/* Active DataSources */}
|
||||
|
|
@ -217,7 +347,7 @@ export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
|
|||
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 (
|
||||
<div key={ds.id} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
|
|
@ -225,10 +355,10 @@ export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
|
|||
background: `${connColor}18`,
|
||||
borderLeft: `3px solid ${connColor}`,
|
||||
fontSize: 12,
|
||||
}} title={fullPath}>
|
||||
}} title={`${connLabel} – ${ds.path || ds.label}`}>
|
||||
<span style={{ fontSize: 12, flexShrink: 0 }}>{_getSourceIcon(ds.sourceType)}</span>
|
||||
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{ds.label}
|
||||
{connLabel} – {folder}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => _removeDatasource(ds.id)}
|
||||
|
|
@ -282,6 +412,81 @@ export const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
|
|||
addingPath={addingPath}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* ── Feature Data Section ── */}
|
||||
<div style={{ height: 1, background: 'var(--border-color, #e0e0e0)', margin: '12px 0 8px' }} />
|
||||
|
||||
{/* Active Feature Data Sources */}
|
||||
{featureDataSources.length > 0 && (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<div style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase', marginBottom: 4 }}>
|
||||
Active Feature Sources
|
||||
</div>
|
||||
{featureDataSources.map(fds => {
|
||||
const fdsConnLabel = featureTree.find(n => n.featureInstanceId === fds.featureInstanceId)?.label || fds.label;
|
||||
return (
|
||||
<div key={fds.id} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '4px 6px', borderRadius: 4, marginBottom: 2,
|
||||
background: '#7b1fa218',
|
||||
borderLeft: '3px solid #7b1fa2',
|
||||
fontSize: 12,
|
||||
}} title={`${fdsConnLabel} - ${fds.tableName}`}>
|
||||
<span style={{ fontSize: 12, flexShrink: 0, display: 'flex', alignItems: 'center', color: '#7b1fa2' }}>
|
||||
{getPageIcon(`feature.${fds.featureCode}`) || '\uD83D\uDDC3\uFE0F'}
|
||||
</span>
|
||||
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{fdsConnLabel} – {fds.tableName}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => _removeFeatureDataSource(fds.id)}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 11, color: '#999', padding: '0 2px' }}
|
||||
title="Entfernen"
|
||||
>
|
||||
{'\u2715'}
|
||||
</button>
|
||||
</div>
|
||||
); })}
|
||||
<div style={{ height: 1, background: 'var(--border-color, #e0e0e0)', margin: '8px 0' }} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Feature Connections Tree */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
|
||||
<span style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase' }}>
|
||||
Feature Data
|
||||
</span>
|
||||
<button
|
||||
onClick={_loadFeatureConnections}
|
||||
disabled={loadingFeatures}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, color: '#7b1fa2' }}
|
||||
>
|
||||
{loadingFeatures ? '...' : '\u21BB'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loadingFeatures && featureTree.length === 0 && (
|
||||
<div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}>
|
||||
Loading feature instances...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loadingFeatures && featureTree.length === 0 && (
|
||||
<div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}>
|
||||
No feature instances found.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{featureTree.map(fNode => (
|
||||
<_FeatureNodeView
|
||||
key={fNode.featureInstanceId}
|
||||
node={fNode}
|
||||
onToggle={_toggleFeatureNode}
|
||||
onAddTable={_addFeatureTable}
|
||||
isTableAdded={_isFeatureTableAdded}
|
||||
addingKey={addingFeatureKey}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -391,6 +596,129 @@ const _TreeNodeView: React.FC<TreeNodeViewProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
/* ─── 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<FeatureNodeViewProps> = ({
|
||||
node, onToggle, onAddTable, isTableAdded, addingKey,
|
||||
}) => {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const chevron = node.expanded ? '\u25BE' : '\u25B8';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
onClick={() => 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',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 10, color: '#888', width: 12, textAlign: 'center', flexShrink: 0 }}>
|
||||
{node.loading ? _Spinner() : chevron}
|
||||
</span>
|
||||
<span style={{ fontSize: 13, flexShrink: 0, display: 'flex', alignItems: 'center', color: '#7b1fa2' }}>
|
||||
{getPageIcon(`feature.${node.featureCode}`) || '\uD83D\uDDC3\uFE0F'}
|
||||
</span>
|
||||
<span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 12, fontWeight: 600 }}>
|
||||
{node.label}
|
||||
</span>
|
||||
<span style={{ fontSize: 10, color: '#999', flexShrink: 0 }}>
|
||||
{node.tableCount} tables
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{node.expanded && node.tables && node.tables.length > 0 && (
|
||||
<div>
|
||||
{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}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{node.expanded && node.tables && node.tables.length === 0 && !node.loading && (
|
||||
<div style={{ paddingLeft: 36, fontSize: 11, color: '#bbb', padding: '2px 0 2px 36px' }}>
|
||||
(no tables)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface FeatureTableRowProps {
|
||||
featureNode: FeatureConnectionNode;
|
||||
table: FeatureTableNode;
|
||||
onAdd: (node: FeatureConnectionNode, table: FeatureTableNode) => void;
|
||||
isAdded: boolean;
|
||||
isAdding: boolean;
|
||||
}
|
||||
|
||||
const _FeatureTableRow: React.FC<FeatureTableRowProps> = ({
|
||||
featureNode, table, onAdd, isAdded, isAdding,
|
||||
}) => {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const tableLabel = table.label?.en || table.label?.de || table.tableName;
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => 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(', ')}`}
|
||||
>
|
||||
<span style={{ fontSize: 14, flexShrink: 0 }}>{'\uD83D\uDCC1'}</span>
|
||||
<span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 12 }}>
|
||||
{tableLabel}
|
||||
</span>
|
||||
{hovered && !isAdded && (
|
||||
<button
|
||||
onClick={() => onAdd(featureNode, table)}
|
||||
disabled={isAdding}
|
||||
style={{
|
||||
background: 'none', border: '1px solid #7b1fa2', borderRadius: 3,
|
||||
cursor: isAdding ? 'not-allowed' : 'pointer',
|
||||
fontSize: 10, color: '#7b1fa2', padding: '1px 5px',
|
||||
opacity: isAdding ? 0.5 : 1, flexShrink: 0,
|
||||
}}
|
||||
title="Add as feature data source"
|
||||
>
|
||||
{isAdding ? '...' : '+ Add'}
|
||||
</button>
|
||||
)}
|
||||
{isAdded && (
|
||||
<span style={{ fontSize: 10, color: '#4caf50', flexShrink: 0 }} title="Already added">
|
||||
{'\u2713'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/* ─── Spinner (inline) ──────────────────────────────────────────────── */
|
||||
|
||||
function _Spinner(): React.ReactElement {
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export const FileBrowser: React.FC<FileBrowserProps> = ({
|
|||
handleMoveFile,
|
||||
handleMoveFiles: contextMoveFiles,
|
||||
handleFileDelete,
|
||||
handleDownloadFolder,
|
||||
expandedFolderIds,
|
||||
toggleFolderExpanded,
|
||||
} = useFileContext();
|
||||
|
|
@ -238,6 +239,7 @@ export const FileBrowser: React.FC<FileBrowserProps> = ({
|
|||
onDeleteFile={_onDeleteFile}
|
||||
onDeleteFiles={_onDeleteFiles}
|
||||
onDeleteFolders={_onDeleteFolders}
|
||||
onDownloadFolder={handleDownloadFolder}
|
||||
/>
|
||||
|
||||
{_fileNodes.length === 0 && (
|
||||
|
|
|
|||
|
|
@ -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<WorkspaceInputProps> = ({
|
|||
onStop,
|
||||
files,
|
||||
dataSources,
|
||||
featureDataSources = [],
|
||||
pendingFiles = [],
|
||||
onRemovePendingFile,
|
||||
onFileUploadClick,
|
||||
|
|
@ -79,6 +82,7 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
|
|||
const [showLangPicker, setShowLangPicker] = useState(false);
|
||||
const [attachedFileIds, setAttachedFileIds] = useState<string[]>([]);
|
||||
const [attachedDataSourceIds, setAttachedDataSourceIds] = useState<string[]>([]);
|
||||
const [attachedFeatureDataSourceIds, setAttachedFeatureDataSourceIds] = useState<string[]>([]);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const promptBeforeVoiceRef = useRef('');
|
||||
const finalizedTextRef = useRef('');
|
||||
|
|
@ -112,12 +116,12 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
|
|||
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<WorkspaceInputProps> = ({
|
|||
);
|
||||
}, []);
|
||||
|
||||
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<WorkspaceInputProps> = ({
|
|||
? 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<WorkspaceInputProps> = ({
|
|||
background: '#e8f5e9', color: '#2e7d32', fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
🔗 {ds?.label || dsId}
|
||||
🔗 {ds?.label || ds?.path || dsId}
|
||||
<button
|
||||
onClick={() => _removeAttachedDataSource(dsId)}
|
||||
style={{
|
||||
|
|
@ -384,6 +394,32 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
|
|||
</span>
|
||||
);
|
||||
})}
|
||||
{attachedFeatureDataSourceIds.map(fdsId => {
|
||||
const fds = featureDataSources.find(d => d.id === fdsId);
|
||||
const fdsIcon = fds ? getPageIcon(`feature.${fds.featureCode}`) : null;
|
||||
return (
|
||||
<span
|
||||
key={fdsId}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 4,
|
||||
padding: '3px 8px', borderRadius: 12, fontSize: 11,
|
||||
background: '#f3e5f5', color: '#7b1fa2', fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
<span style={{ display: 'flex', alignItems: 'center', fontSize: 12 }}>{fdsIcon || '\uD83D\uDDC3\uFE0F'}</span>
|
||||
{fds?.label || fdsId} – {fds?.tableName || ''}
|
||||
<button
|
||||
onClick={() => _toggleFeatureDataSource(fdsId)}
|
||||
style={{
|
||||
border: 'none', background: 'none', cursor: 'pointer',
|
||||
fontSize: 12, color: '#7b1fa2', padding: 0, lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -480,8 +516,8 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
|
|||
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<WorkspaceInputProps> = ({
|
|||
}}
|
||||
>
|
||||
🔗
|
||||
{attachedDataSourceIds.length > 0 && (
|
||||
{(attachedDataSourceIds.length + attachedFeatureDataSourceIds.length) > 0 && (
|
||||
<span style={{
|
||||
position: 'absolute', top: -4, right: -4,
|
||||
background: '#2e7d32', color: '#fff', fontSize: 9, fontWeight: 700,
|
||||
borderRadius: '50%', width: 16, height: 16,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
{attachedDataSourceIds.length}
|
||||
{attachedDataSourceIds.length + attachedFeatureDataSourceIds.length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
|
@ -539,6 +575,45 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
|
|||
</div>
|
||||
);
|
||||
})}
|
||||
{featureDataSources.length > 0 && (
|
||||
<>
|
||||
<div style={{ padding: '8px 12px', fontSize: 11, color: '#999', fontWeight: 600, borderTop: '1px solid #f0f0f0', borderBottom: '1px solid #f0f0f0' }}>
|
||||
Feature Data Sources
|
||||
</div>
|
||||
{featureDataSources.map(fds => {
|
||||
const isSelected = attachedFeatureDataSourceIds.includes(fds.id);
|
||||
return (
|
||||
<div
|
||||
key={fds.id}
|
||||
onClick={() => _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 = ''; }}
|
||||
>
|
||||
<span style={{
|
||||
width: 16, height: 16, borderRadius: 3,
|
||||
border: isSelected ? '2px solid #7b1fa2' : '2px solid #ccc',
|
||||
background: isSelected ? '#7b1fa2' : 'transparent',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: '#fff', fontSize: 10, fontWeight: 700, flexShrink: 0,
|
||||
}}>
|
||||
{isSelected ? '✓' : ''}
|
||||
</span>
|
||||
<span style={{ display: 'flex', alignItems: 'center', fontSize: 13, color: '#7b1fa2', flexShrink: 0 }}>
|
||||
{getPageIcon(`feature.${fds.featureCode}`) || '\uD83D\uDDC3\uFE0F'}
|
||||
</span>
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{fds.label || fds.featureCode} – {fds.tableName}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -240,7 +240,9 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
|
|||
<DataSourcePanel
|
||||
instanceId={instanceId}
|
||||
dataSources={workspace.dataSources}
|
||||
featureDataSources={workspace.featureDataSources}
|
||||
onRefresh={workspace.refreshDataSources}
|
||||
onRefreshFeatureDataSources={workspace.refreshFeatureDataSources}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -395,15 +397,16 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
|
|||
/>
|
||||
<WorkspaceInput
|
||||
instanceId={instanceId}
|
||||
onSend={(prompt, fileIds, dataSourceIds) => {
|
||||
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()}
|
||||
|
|
|
|||
|
|
@ -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<WorkspaceFile[]>([]);
|
||||
const [folders, setFolders] = useState<WorkspaceFolder[]>([]);
|
||||
const [dataSources, setDataSources] = useState<DataSource[]>([]);
|
||||
const [featureDataSources, setFeatureDataSources] = useState<FeatureDataSource[]>([]);
|
||||
const [agentProgress, setAgentProgress] = useState<AgentProgress | null>(null);
|
||||
const [toolActivities, setToolActivities] = useState<ToolActivity[]>([]);
|
||||
const [pendingEdits, setPendingEdits] = useState<FileEditProposal[]>([]);
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue