This commit is contained in:
ValueOn AG 2026-04-21 08:57:49 +02:00
parent 8e5a01df6d
commit 1c4233c7ea
2 changed files with 41 additions and 99 deletions

View file

@ -606,17 +606,6 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged, onSe
} }
}, [instanceId, _fetchDataSources, onSourcesChanged]); }, [instanceId, _fetchDataSources, onSourcesChanged]);
/* ── Remove DataSource ── */
const _removeDatasource = useCallback(async (dsId: string) => {
try {
await api.delete(`/api/workspace/${instanceId}/datasources/${dsId}`);
_fetchDataSources();
onSourcesChanged?.();
} catch (err) {
console.error('Failed to remove data source:', err);
}
}, [instanceId, _fetchDataSources, onSourcesChanged]);
/* ── Check if a path is already added ── */ /* ── Check if a path is already added ── */
const _isAdded = useCallback((connectionId: string, service: string | undefined, path: string | undefined): boolean => { const _isAdded = useCallback((connectionId: string, service: string | undefined, path: string | undefined): boolean => {
const expectedSourceType = service ? (_SERVICE_TO_SOURCE_TYPE[service] || service) : undefined; const expectedSourceType = service ? (_SERVICE_TO_SOURCE_TYPE[service] || service) : undefined;
@ -841,17 +830,6 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged, onSe
} }
}, [instanceId, _fetchFeatureDataSources, onSourcesChanged]); }, [instanceId, _fetchFeatureDataSources, onSourcesChanged]);
/* ── Feature: Remove FeatureDataSource ── */
const _removeFeatureDataSource = useCallback(async (fdsId: string) => {
try {
await api.delete(`/api/workspace/${instanceId}/feature-datasources/${fdsId}`);
_fetchFeatureDataSources();
onSourcesChanged?.();
} catch (err) {
console.error('Failed to remove feature data source:', err);
}
}, [instanceId, _fetchFeatureDataSources, onSourcesChanged]);
/* ── Feature: check if table already added (no record filter) ── */ /* ── Feature: check if table already added (no record filter) ── */
const _isFeatureTableAdded = useCallback((featureInstanceId: string, tableName: string): boolean => { const _isFeatureTableAdded = useCallback((featureInstanceId: string, tableName: string): boolean => {
return featureDataSources.some(fds => return featureDataSources.some(fds =>
@ -1021,7 +999,6 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged, onSe
dataSources={dataSources} dataSources={dataSources}
onCycleScope={_cyclePersonalScope} onCycleScope={_cyclePersonalScope}
onToggleNeutralize={_togglePersonalNeutralize} onToggleNeutralize={_togglePersonalNeutralize}
onRemoveDs={_removeDatasource}
onSendToChat={_sendNodeToChat} onSendToChat={_sendNodeToChat}
scopeCycleTitle={_scopeCycleTitle} scopeCycleTitle={_scopeCycleTitle}
selectedKeys={selectedKeys} selectedKeys={selectedKeys}
@ -1081,7 +1058,6 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged, onSe
onCycleScope={_cycleFeatureScope} onCycleScope={_cycleFeatureScope}
onToggleNeutralize={_toggleFeatureNeutralize} onToggleNeutralize={_toggleFeatureNeutralize}
onToggleNeutralizeField={_toggleNeutralizeField} onToggleNeutralizeField={_toggleNeutralizeField}
onRemoveFds={_removeFeatureDataSource}
featureTree={featureTree} featureTree={featureTree}
/> />
))} ))}
@ -1110,7 +1086,6 @@ interface _TreeNodeViewProps {
dataSources: UdbDataSource[]; dataSources: UdbDataSource[];
onCycleScope: (ds: UdbDataSource) => void; onCycleScope: (ds: UdbDataSource) => void;
onToggleNeutralize: (ds: UdbDataSource) => void; onToggleNeutralize: (ds: UdbDataSource) => void;
onRemoveDs: (dsId: string) => void;
onSendToChat?: (params: { connectionId: string; sourceType: string; path: string; label: string; displayPath?: string }) => void; onSendToChat?: (params: { connectionId: string; sourceType: string; path: string; label: string; displayPath?: string }) => void;
scopeCycleTitle: (scope: string) => string; scopeCycleTitle: (scope: string) => string;
selectedKeys: Set<string>; selectedKeys: Set<string>;
@ -1121,7 +1096,7 @@ interface _TreeNodeViewProps {
const _TreeNodeView: React.FC<_TreeNodeViewProps> = ({ const _TreeNodeView: React.FC<_TreeNodeViewProps> = ({
node, depth, onToggle, onEnsureDs, isAdded, addingPath, node, depth, onToggle, onEnsureDs, isAdded, addingPath,
dataSources, onCycleScope, onToggleNeutralize, onRemoveDs, onSendToChat, scopeCycleTitle, dataSources, onCycleScope, onToggleNeutralize, onSendToChat, scopeCycleTitle,
selectedKeys, onSelect, inheritedScope, inheritedNeutralize, selectedKeys, onSelect, inheritedScope, inheritedNeutralize,
}) => { }) => {
const { t } = useLanguage(); const { t } = useLanguage();
@ -1216,19 +1191,11 @@ const _TreeNodeView: React.FC<_TreeNodeViewProps> = ({
{node.label} {node.label}
</span> </span>
{/* Dynamic action: Remove (only when DS exists) placed LEFT of the {/* Stable trio: chat | scope | neutralize (always in this order).
* stable trio so the trio always anchors at the right edge. */} * No "remove from workspace" button here by design: the UDB row only
{ds && ( * exposes the catalog state. Detach from the *current chat* happens
<button * via the chip "x" in WorkspaceInput; that chip is the single source
onClick={e => { e.stopPropagation(); onRemoveDs(ds.id); }} * of truth for chat-scoped attachment lifecycle. */}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 10, color: '#999', padding: '0 2px', flexShrink: 0 }}
title={t('Entfernen')}
>
{'\u2715'}
</button>
)}
{/* ── Stable trio: chat | scope | neutralize (always in this order) ── */}
<button <button
onClick={e => { e.stopPropagation(); onSendToChat?.(_chatPayload); }} onClick={e => { e.stopPropagation(); onSendToChat?.(_chatPayload); }}
style={{ style={{
@ -1290,7 +1257,6 @@ const _TreeNodeView: React.FC<_TreeNodeViewProps> = ({
dataSources={dataSources} dataSources={dataSources}
onCycleScope={onCycleScope} onCycleScope={onCycleScope}
onToggleNeutralize={onToggleNeutralize} onToggleNeutralize={onToggleNeutralize}
onRemoveDs={onRemoveDs}
onSendToChat={onSendToChat} onSendToChat={onSendToChat}
scopeCycleTitle={scopeCycleTitle} scopeCycleTitle={scopeCycleTitle}
selectedKeys={selectedKeys} selectedKeys={selectedKeys}
@ -1343,7 +1309,6 @@ interface _FeatureActionContext {
onCycleScope: (fds: UdbFeatureDataSource) => void; onCycleScope: (fds: UdbFeatureDataSource) => void;
onToggleNeutralize: (fds: UdbFeatureDataSource) => void; onToggleNeutralize: (fds: UdbFeatureDataSource) => void;
onToggleNeutralizeField: (fds: UdbFeatureDataSource, fieldName: string) => void; onToggleNeutralizeField: (fds: UdbFeatureDataSource, fieldName: string) => void;
onRemoveFds: (fdsId: string) => void;
featureTree: MandateGroupNode[]; featureTree: MandateGroupNode[];
} }
@ -1463,16 +1428,6 @@ const _FeatureNodeView: React.FC<_FeatureNodeViewProps> = (props) => {
{node.tableCount} {t('Tabellen')} {node.tableCount} {t('Tabellen')}
</span> </span>
{wildcardFds && (
<button
onClick={e => { e.stopPropagation(); ctx.onRemoveFds(wildcardFds.id); }}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 10, color: '#999', padding: '0 2px', flexShrink: 0 }}
title={t('Entfernen')}
>
{'\u2715'}
</button>
)}
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@ -1608,7 +1563,6 @@ const _FeatureItemView: React.FC<_FeatureItemViewProps> = (props) => {
onCycleScope={ctx.onCycleScope} onCycleScope={ctx.onCycleScope}
onToggleNeutralize={ctx.onToggleNeutralize} onToggleNeutralize={ctx.onToggleNeutralize}
onToggleNeutralizeField={ctx.onToggleNeutralizeField} onToggleNeutralizeField={ctx.onToggleNeutralizeField}
onRemoveFds={ctx.onRemoveFds}
featureTree={ctx.featureTree} featureTree={ctx.featureTree}
inheritedScope={inheritedScope} inheritedScope={inheritedScope}
inheritedNeutralize={inheritedNeutralize} inheritedNeutralize={inheritedNeutralize}
@ -1884,16 +1838,6 @@ const _RecordRowView: React.FC<_RecordRowViewProps> = (props) => {
</button> </button>
)} )}
{fds && (
<button
onClick={(e) => { e.stopPropagation(); ctx.onRemoveFds(fds.id); }}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 10, color: '#999', padding: '0 2px', flexShrink: 0 }}
title={t('Entfernen')}
>
{'\u2715'}
</button>
)}
<button <button
onClick={(e) => { e.stopPropagation(); ctx.onSendToChat?.(_chatPayload); }} onClick={(e) => { e.stopPropagation(); ctx.onSendToChat?.(_chatPayload); }}
style={{ style={{
@ -1975,7 +1919,6 @@ interface _FeatureTableRowProps {
onCycleScope: (fds: UdbFeatureDataSource) => void; onCycleScope: (fds: UdbFeatureDataSource) => void;
onToggleNeutralize: (fds: UdbFeatureDataSource) => void; onToggleNeutralize: (fds: UdbFeatureDataSource) => void;
onToggleNeutralizeField: (fds: UdbFeatureDataSource, fieldName: string) => void; onToggleNeutralizeField: (fds: UdbFeatureDataSource, fieldName: string) => void;
onRemoveFds: (fdsId: string) => void;
featureTree: MandateGroupNode[]; featureTree: MandateGroupNode[];
inheritedScope?: string; inheritedScope?: string;
inheritedNeutralize?: boolean; inheritedNeutralize?: boolean;
@ -1984,7 +1927,7 @@ interface _FeatureTableRowProps {
const _FeatureTableRow: React.FC<_FeatureTableRowProps> = ({ const _FeatureTableRow: React.FC<_FeatureTableRowProps> = ({
featureNode, table, depth, onAddFeatureTable, onSendToChat, featureNode, table, depth, onAddFeatureTable, onSendToChat,
featureDataSources, onCycleScope, onToggleNeutralize, onToggleNeutralizeField, featureDataSources, onCycleScope, onToggleNeutralize, onToggleNeutralizeField,
onRemoveFds, featureTree, inheritedScope, inheritedNeutralize, featureTree, inheritedScope, inheritedNeutralize,
}) => { }) => {
const { t } = useLanguage(); const { t } = useLanguage();
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false);
@ -2052,16 +1995,6 @@ const _FeatureTableRow: React.FC<_FeatureTableRowProps> = ({
)} )}
</span> </span>
{fds && (
<button
onClick={e => { e.stopPropagation(); onRemoveFds(fds.id); }}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 10, color: '#999', padding: '0 2px', flexShrink: 0 }}
title={t('Entfernen')}
>
{'\u2715'}
</button>
)}
<button <button
onClick={e => { e.stopPropagation(); onSendToChat?.(_chatPayload); }} onClick={e => { e.stopPropagation(); onSendToChat?.(_chatPayload); }}
style={{ style={{

View file

@ -118,23 +118,43 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({ instanceId,
} }
}, [draftAppend, onDraftAppendConsumed]); }, [draftAppend, onDraftAppendConsumed]);
// Persist a changed attachment list to the backend so the next chat
// reload reflects the current state. Defined early so the
// pendingAttachDsId / pendingAttachFdsId effects below can also persist
// immediately after a 💬-click or drag-drop attach.
const _persistAttachments = useCallback((dsIds: string[], fdsIds: string[]) => {
if (!instanceId || !workflowId) return;
api.patch(`/api/workspace/${instanceId}/workflows/${workflowId}/attachments`, {
dataSourceIds: dsIds,
featureDataSourceIds: fdsIds,
}).catch(err => console.warn('Failed to persist chat attachments:', err));
}, [instanceId, workflowId]);
// 💬-click or drag-drop attach: parent sets pendingAttachDsId after
// creating/finding the DataSource. Add to the chip bar AND persist
// immediately so a chat reload before the user sends a message still
// shows the chip.
useEffect(() => { useEffect(() => {
if (pendingAttachDsId) { if (!pendingAttachDsId) return;
setAttachedDataSourceIds(prev => setAttachedDataSourceIds(prev => {
prev.includes(pendingAttachDsId) ? prev : [...prev, pendingAttachDsId], if (prev.includes(pendingAttachDsId)) return prev;
); const next = [...prev, pendingAttachDsId];
onPendingAttachDsConsumed?.(); _persistAttachments(next, attachedFeatureDataSourceIds);
} return next;
}, [pendingAttachDsId, onPendingAttachDsConsumed]); });
onPendingAttachDsConsumed?.();
}, [pendingAttachDsId, onPendingAttachDsConsumed, _persistAttachments, attachedFeatureDataSourceIds]);
useEffect(() => { useEffect(() => {
if (pendingAttachFdsId) { if (!pendingAttachFdsId) return;
setAttachedFeatureDataSourceIds(prev => setAttachedFeatureDataSourceIds(prev => {
prev.includes(pendingAttachFdsId) ? prev : [...prev, pendingAttachFdsId], if (prev.includes(pendingAttachFdsId)) return prev;
); const next = [...prev, pendingAttachFdsId];
onPendingAttachFdsConsumed?.(); _persistAttachments(attachedDataSourceIds, next);
} return next;
}, [pendingAttachFdsId, onPendingAttachFdsConsumed]); });
onPendingAttachFdsConsumed?.();
}, [pendingAttachFdsId, onPendingAttachFdsConsumed, _persistAttachments, attachedDataSourceIds]);
// Rehydrate the chip-bar whenever the parent re-loads a chat (loadedNonce // Rehydrate the chip-bar whenever the parent re-loads a chat (loadedNonce
// bumps on every loadWorkflow call). We trust the loaded IDs initially; // bumps on every loadWorkflow call). We trust the loaded IDs initially;
@ -184,17 +204,6 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({ instanceId,
}); });
}, [loadedNonce, featureDataSources]); }, [loadedNonce, featureDataSources]);
// Persist a changed attachment list to the backend so the next chat
// reload reflects the current state. We debounce slightly by sending on
// the next animation frame to coalesce rapid clicks.
const _persistAttachments = useCallback((dsIds: string[], fdsIds: string[]) => {
if (!instanceId || !workflowId) return;
api.patch(`/api/workspace/${instanceId}/workflows/${workflowId}/attachments`, {
dataSourceIds: dsIds,
featureDataSourceIds: fdsIds,
}).catch(err => console.warn('Failed to persist chat attachments:', err));
}, [instanceId, workflowId]);
const promptBeforeVoiceRef = useRef(''); const promptBeforeVoiceRef = useRef('');
const finalizedTextRef = useRef(''); const finalizedTextRef = useRef('');
const currentInterimRef = useRef(''); const currentInterimRef = useRef('');