diff --git a/src/api/fileApi.ts b/src/api/fileApi.ts index 75151c3..c153b5f 100644 --- a/src/api/fileApi.ts +++ b/src/api/fileApi.ts @@ -36,6 +36,7 @@ export interface PaginationParams { search?: string; viewKey?: string; groupByLevels?: Array<{ field: string; nullLabel?: string; direction?: 'asc' | 'desc' }>; + owner?: 'all' | 'me' | 'shared'; } export interface PaginatedResponse { @@ -109,6 +110,7 @@ export async function fetchFiles( if (params.search) paginationObj.search = params.search; if (params.viewKey) paginationObj.viewKey = params.viewKey; if (params.groupByLevels !== undefined) paginationObj.groupByLevels = params.groupByLevels; + if (params.owner) requestParams.owner = params.owner; if (Object.keys(paginationObj).length > 0) { requestParams.pagination = JSON.stringify(paginationObj); diff --git a/src/components/FlowEditor/editor/Automation2FlowEditor.tsx b/src/components/FlowEditor/editor/Automation2FlowEditor.tsx index 1e8c157..b9923ce 100644 --- a/src/components/FlowEditor/editor/Automation2FlowEditor.tsx +++ b/src/components/FlowEditor/editor/Automation2FlowEditor.tsx @@ -158,6 +158,7 @@ export const Automation2FlowEditor: React.FC = ({ in const [versions, setVersions] = useState([]); const [currentVersionId, setCurrentVersionId] = useState(null); const [versionLoading, setVersionLoading] = useState(false); + const didBootstrapEmptyCanvasRef = useRef(false); const [targetFeatureInstanceId, setTargetFeatureInstanceId] = useState(instanceId); @@ -598,8 +599,22 @@ export const Automation2FlowEditor: React.FC = ({ in useEffect(() => { if (loading || nodeTypes.length === 0) return; - if (currentWorkflowId || initialWorkflowId) return; - if (canvasNodes.length > 0) return; + if (currentWorkflowId || initialWorkflowId) { + didBootstrapEmptyCanvasRef.current = false; + return; + } + if (didBootstrapEmptyCanvasRef.current) return; + didBootstrapEmptyCanvasRef.current = true; + if (canvasNodes.length === 0 && canvasConnections.length === 0 && invocations.length === 0) { + return; + } + console.debug(`${LOG} bootstrapping empty canvas`, { + currentWorkflowId, + initialWorkflowId, + canvasNodes: canvasNodes.length, + canvasConnections: canvasConnections.length, + invocations: invocations.length, + }); applyGraphWithSync({ nodes: [], connections: [] }, [], { skipHistory: true, }); @@ -609,8 +624,9 @@ export const Automation2FlowEditor: React.FC = ({ in currentWorkflowId, initialWorkflowId, canvasNodes.length, + canvasConnections.length, + invocations.length, applyGraphWithSync, - t, ]); const toggleCategory = useCallback((id: string) => { diff --git a/src/components/FlowEditor/editor/FlowCanvas.tsx b/src/components/FlowEditor/editor/FlowCanvas.tsx index 5a8010b..77ad2c8 100644 --- a/src/components/FlowEditor/editor/FlowCanvas.tsx +++ b/src/components/FlowEditor/editor/FlowCanvas.tsx @@ -20,6 +20,8 @@ import { useLanguage } from '../../../providers/language/LanguageContext'; import { AiBadge } from '../nodes/shared/AiBadge'; import { switchOutputLabel } from '../nodes/shared/graphUtils'; +const LOG = '[FlowCanvas]'; + export interface CanvasNode { id: string; type: string; @@ -842,6 +844,8 @@ export const FlowCanvas = forwardRef(function const onHistoryCheckpointRef = useRef(onHistoryCheckpoint); onHistoryCheckpointRef.current = onHistoryCheckpoint; + const onSelectionChangeRef = useRef(onSelectionChange); + onSelectionChangeRef.current = onSelectionChange; const emitHistoryCheckpoint = useCallback(() => { onHistoryCheckpointRef.current?.(); @@ -1019,12 +1023,19 @@ export const FlowCanvas = forwardRef(function ] ); + const lastEmittedSelectionRef = useRef<{ nodeId: string | null; signature: string | null }>({ + nodeId: null, + signature: null, + }); + useEffect(() => { - if (onSelectionChange) { - const node = selectedNodeId ? nodes.find((n) => n.id === selectedNodeId) ?? null : null; - onSelectionChange(node); - } - }, [selectedNodeId, nodes, onSelectionChange]); + const node = selectedNodeId ? nodes.find((n) => n.id === selectedNodeId) ?? null : null; + const signature = node ? JSON.stringify(node) : null; + const last = lastEmittedSelectionRef.current; + if (last.nodeId === selectedNodeId && last.signature === signature) return; + lastEmittedSelectionRef.current = { nodeId: selectedNodeId, signature }; + onSelectionChangeRef.current?.(node); + }, [selectedNodeId, nodes]); const handleConnectionClick = useCallback((e: React.MouseEvent, connId: string) => { e.stopPropagation(); @@ -1088,6 +1099,11 @@ export const FlowCanvas = forwardRef(function const handleDrop = useCallback( async (e: React.DragEvent) => { e.preventDefault(); + console.debug(`${LOG} drop received`, { + types: Array.from(e.dataTransfer.types), + clientX: e.clientX, + clientY: e.clientY, + }); // 1) externe Drop-Targets (z. B. ``application/json+workflow`` aus UDB-FilesTab) if (onExternalDrop) { const reservedMimes = new Set([ @@ -1113,16 +1129,35 @@ export const FlowCanvas = forwardRef(function } // 2) Standard: Node-Type aus der NodeSidebar const raw = e.dataTransfer.getData('application/json'); - if (!raw || !containerRef.current) return; + if (!raw || !containerRef.current) { + console.debug(`${LOG} drop ignored`, { + hasRaw: Boolean(raw), + hasContainer: Boolean(containerRef.current), + }); + return; + } try { const { type } = JSON.parse(raw); const el = containerRef.current; const rect = el.getBoundingClientRect(); const x = (e.clientX - rect.left - panOffset.x) / zoom - NODE_WIDTH / 2; const y = (e.clientY - rect.top - panOffset.y) / zoom - NODE_HEIGHT / 2; + console.debug(`${LOG} placing node from drop`, { + type, + raw, + dropX: x, + dropY: y, + panOffset, + zoom, + }); onDropNodeType(type, Math.max(0, x), Math.max(0, y)); emitHistoryCheckpoint(); - } catch (_) {} + } catch (error) { + console.debug(`${LOG} drop parse failed`, { + raw, + error, + }); + } }, [onDropNodeType, onExternalDrop, panOffset, zoom, emitHistoryCheckpoint] ); diff --git a/src/components/FlowEditor/editor/NodeConfigPanel.tsx b/src/components/FlowEditor/editor/NodeConfigPanel.tsx index c60600b..9f8462c 100644 --- a/src/components/FlowEditor/editor/NodeConfigPanel.tsx +++ b/src/components/FlowEditor/editor/NodeConfigPanel.tsx @@ -9,6 +9,7 @@ import type { GraphDefinedSchemaRef, NodeType, NodeTypeParameter, PortSchema } f import type { ApiRequestFunction } from '../../../api/workflowApi'; import { getLabel } from '../nodes/shared/utils'; import { FRONTEND_TYPE_RENDERERS } from '../nodes/frontendTypeRenderers'; +import { ContextBuilderRenderer } from '../nodes/frontendTypeRenderers/ContextBuilderRenderer'; import { RequiredAttributePicker } from '../nodes/shared/RequiredAttributePicker'; import { findRequiredErrors } from '../nodes/shared/paramValidation'; import { useAutomation2DataFlow } from '../context/Automation2DataFlowContext'; @@ -253,6 +254,7 @@ export const NodeConfigPanel: React.FC = ({ node, for (const param of sortedParameters) { if (param.frontendType === 'hidden') continue; + if (param.name === 'context') continue; if (CONTEXT_EXTRACT_CHUNK_SET.has(param.name)) continue; if (!parameterVisibleForFrontendOptions(param, params, nodeType)) continue; @@ -378,6 +380,15 @@ export const NodeConfigPanel: React.FC = ({ node, t, ]); + const extractContentContextParam = useMemo((): NodeTypeParameter | null => { + if (!node || !nodeType || node.type !== CONTEXT_EXTRACT_CONTENT_NODE_TYPE) return null; + const param = sortedParameters.find((p) => p.name === 'context') ?? null; + if (!param) return null; + if (param.frontendType === 'hidden') return null; + if (!parameterVisibleForFrontendOptions(param, params, nodeType)) return null; + return param; + }, [node, nodeType, sortedParameters, params]); + if (!node || !nodeType) return null; const isTrigger = node.type.startsWith('trigger.'); @@ -483,11 +494,71 @@ export const NodeConfigPanel: React.FC = ({ node, )} {extractContentAccordionItems !== null ? ( - - key={`${node.id}-extract-accordion`} - defaultOpenId={null} - items={extractContentAccordionItems} - /> + <> + {extractContentContextParam ? ( +
+
+ {extractContentContextParam.required && ( + + * + + )} + {verboseSchema && extractContentContextParam.type && ( + + {extractContentContextParam.type} + + )} +
+ updateParam(extractContentContextParam.name, val)} + allParams={params} + instanceId={instanceId} + request={request} + nodeType={node.type} + onPatchParams={patchParams} + /> +
+ ) : null} + {extractContentAccordionItems.length > 0 ? ( + + key={`${node.id}-extract-accordion`} + defaultOpenId={null} + items={extractContentAccordionItems} + /> + ) : null} + ) : ( parameters.map((param: NodeTypeParameter) => { // Safety net: hidden params have no UI footprint at all — no row, diff --git a/src/components/FormGenerator/FilterSearchInput/FilterSearchInput.module.css b/src/components/FormGenerator/FilterSearchInput/FilterSearchInput.module.css new file mode 100644 index 0000000..9756ac2 --- /dev/null +++ b/src/components/FormGenerator/FilterSearchInput/FilterSearchInput.module.css @@ -0,0 +1,48 @@ +.wrapper { + position: relative; + width: 100%; +} + +.input { + width: 100%; + padding: 3px 30px 3px 6px; + font-size: 12px; + border: 1px solid var(--border-color, #ccc); + border-radius: 3px; + outline: none; + box-sizing: border-box; + background: var(--color-bg, #fff); + color: var(--color-text, #334155); +} + +.input:focus { + border-color: var(--primary-color, #F25843); +} + +.input::placeholder { + color: var(--color-text-muted, #94a3b8); +} + +.clearBtn { + position: absolute; + right: 5px; + top: 3px; + bottom: 3px; + display: inline-flex; + align-items: center; + justify-content: center; + width: 25px; + padding: 0; + border: none; + border-radius: 3px; + background: none; + cursor: pointer; + font-size: 25px; + line-height: 1; + color: var(--color-text-secondary, #94a3b8); +} + +.clearBtn:hover { + background: none; + color: var(--color-text-secondary, #94a3b8); +} diff --git a/src/components/FormGenerator/FilterSearchInput/FilterSearchInput.tsx b/src/components/FormGenerator/FilterSearchInput/FilterSearchInput.tsx new file mode 100644 index 0000000..00aaf29 --- /dev/null +++ b/src/components/FormGenerator/FilterSearchInput/FilterSearchInput.tsx @@ -0,0 +1,69 @@ +import React, { type Ref } from 'react'; +import styles from './FilterSearchInput.module.css'; + +export interface FilterSearchInputProps { + value: string; + onChange: (value: string) => void; + placeholder?: string; + inputRef?: Ref; + onInputClick?: (e: React.MouseEvent) => void; + onFocus?: () => void; + onBlur?: () => void; + /** When set, only `inputClassName` styles the input (for floating-label toolbar search). */ + variant?: 'compact' | 'inherit'; + inputClassName?: string; + wrapperClassName?: string; + clearTitle?: string; +} + +export function FilterSearchInput({ + value, + onChange, + placeholder = 'Filter...', + inputRef, + onInputClick, + onFocus, + onBlur, + variant = 'compact', + inputClassName, + wrapperClassName, + clearTitle = 'Eingabe löschen', +}: FilterSearchInputProps) { + const inputClass = variant === 'inherit' + ? inputClassName + : inputClassName + ? `${styles.input} ${inputClassName}` + : styles.input; + + return ( +
+ onChange(e.target.value)} + placeholder={placeholder} + className={inputClass} + onClick={onInputClick} + onFocus={onFocus} + onBlur={onBlur} + /> + {value && ( + + )} +
+ ); +} diff --git a/src/components/FormGenerator/FilterSearchInput/index.ts b/src/components/FormGenerator/FilterSearchInput/index.ts new file mode 100644 index 0000000..7b8e18a --- /dev/null +++ b/src/components/FormGenerator/FilterSearchInput/index.ts @@ -0,0 +1,2 @@ +export { FilterSearchInput } from './FilterSearchInput'; +export type { FilterSearchInputProps } from './FilterSearchInput'; diff --git a/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.module.css b/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.module.css index 9b09fac..9fc82a8 100644 --- a/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.module.css +++ b/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.module.css @@ -168,7 +168,7 @@ .searchInput { width: 100%; height: 40px; - padding: 8px 12px; + padding: 8px 28px 8px 12px; border: 1px solid var(--color-border, #E2E8F0); border-radius: 6px; font-size: 14px; diff --git a/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.tsx b/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.tsx index e57fce2..7d23d9f 100644 --- a/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.tsx +++ b/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.tsx @@ -2,6 +2,7 @@ import React from 'react'; import type { IconType } from 'react-icons'; import { useLanguage } from '../../../providers/language/LanguageContext'; import styles from './FormGeneratorControls.module.css'; +import { FilterSearchInput } from '../FilterSearchInput'; import { Button } from '../../UiComponents/Button'; import { IoIosRefresh } from "react-icons/io"; import { FaTrash, FaDownload } from "react-icons/fa"; @@ -189,14 +190,15 @@ export function FormGeneratorControls({
{searchable && (
- onSearchChange(e.target.value)} + onChange={onSearchChange} + placeholder=" " onFocus={() => onSearchFocus(true)} onBlur={() => onSearchFocus(false)} - className={`${styles.searchInput} ${searchFocused || searchTerm ? styles.focused : ''}`} + inputClassName={`${styles.searchInput} ${searchFocused || searchTerm ? styles.focused : ''}`} + clearTitle={t('Suche löschen')} />
)} - {_allChats.length === 0 && ( + {hasLoadedOnce && _allChats.length === 0 && (
{filter === 'archived' ? t('Keine archivierten Chats') : t('Keine aktiven Chats')}
diff --git a/src/components/UnifiedDataBar/FilesTab.module.css b/src/components/UnifiedDataBar/FilesTab.module.css index d992368..e8889ae 100644 --- a/src/components/UnifiedDataBar/FilesTab.module.css +++ b/src/components/UnifiedDataBar/FilesTab.module.css @@ -81,6 +81,60 @@ flex-wrap: wrap; } +.uploadCircleButton { + background: none; + border: none; + cursor: pointer; + font-size: 12px; + color: #f25843; + width: 26px; + height: 26px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; +} + +.uploadCircleButton:disabled { + cursor: not-allowed; +} + +.uploadCircleWrap { + position: relative; + width: 22px; + height: 22px; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.uploadCircleSvg { + position: absolute; + inset: 0; + transform: rotate(-90deg); +} + +.uploadCircleTrack { + fill: none; + stroke: rgba(242, 88, 67, 0.25); + stroke-width: 2; +} + +.uploadCircleProgress { + fill: none; + stroke: #f25843; + stroke-width: 2; + stroke-linecap: round; + transition: stroke-dashoffset 120ms linear; +} + +.uploadCircleText { + font-size: 8px; + font-weight: 700; + line-height: 1; + color: #f25843; +} + @media (prefers-color-scheme: dark) { .fileRow:hover { background: rgba(255, 255, 255, 0.05); diff --git a/src/components/UnifiedDataBar/FilesTab.tsx b/src/components/UnifiedDataBar/FilesTab.tsx index 5a13127..87d7675 100644 --- a/src/components/UnifiedDataBar/FilesTab.tsx +++ b/src/components/UnifiedDataBar/FilesTab.tsx @@ -28,6 +28,8 @@ const FilesTab: React.FC = ({ context, onFileSelect, onSendToChat const { showSuccess, showError } = useToast(); const [isDragOver, setIsDragOver] = useState(false); const [uploading, setUploading] = useState(false); + const [uploadProgressPercent, setUploadProgressPercent] = useState(0); + const uploadRunIdRef = useRef(0); const fileInputRef = useRef(null); const provider = useMemo(() => createFolderFileProvider(), []); @@ -54,21 +56,41 @@ const FilesTab: React.FC = ({ context, onFileSelect, onSendToChat const _uploadFiles = useCallback(async (fileList: FileList | File[]) => { if (!context.instanceId || uploading) return; + uploadRunIdRef.current += 1; + const runId = uploadRunIdRef.current; setUploading(true); + setUploadProgressPercent(0); try { - for (const file of Array.from(fileList)) { + const files = Array.from(fileList); + const totalFiles = files.length || 1; + for (const [index, file] of files.entries()) { const formData = new FormData(); formData.append('file', file); formData.append('featureInstanceId', context.instanceId); await api.post('/api/files/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' }, + onUploadProgress: progressEvent => { + if (uploadRunIdRef.current !== runId) return; + if (!progressEvent.total) return; + const fileProgress = Math.min(100, Math.round((progressEvent.loaded * 100) / progressEvent.total)); + const baseProgress = (index / totalFiles) * 100; + const scaledFileProgress = fileProgress / totalFiles; + setUploadProgressPercent(Math.min(100, Math.round(baseProgress + scaledFileProgress))); + }, }); } + if (uploadRunIdRef.current === runId) setUploadProgressPercent(100); _handleRefresh(); } catch (err) { console.error('File upload failed:', err); } finally { - setUploading(false); + if (uploadRunIdRef.current === runId) { + setUploading(false); + // Let 100% render briefly, then reset. + window.setTimeout(() => { + if (uploadRunIdRef.current === runId) setUploadProgressPercent(0); + }, 250); + } } }, [context.instanceId, uploading, _handleRefresh]); @@ -135,6 +157,10 @@ const FilesTab: React.FC = ({ context, onFileSelect, onSendToChat onSendToChat?.([{ id: node.id, type: node.type === 'folder' ? 'group' : 'file', name: node.name }]); }, [onSendToChat]); + const circleRadius = 11; + const circleCircumference = 2 * Math.PI * circleRadius; + const circleOffset = circleCircumference * (1 - uploadProgressPercent / 100); + return (
= ({ context, onFileSelect, onSendToChat )}
diff --git a/src/pages/views/teamsbot/Teamsbot.module.css b/src/pages/views/teamsbot/Teamsbot.module.css index 19414b2..e1b5e03 100644 --- a/src/pages/views/teamsbot/Teamsbot.module.css +++ b/src/pages/views/teamsbot/Teamsbot.module.css @@ -851,6 +851,52 @@ background: var(--surface-alt, #fafafa); } +.panelTitleBar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--border-color, #e0e0e0); + background: var(--surface-alt, #fafafa); + flex-shrink: 0; +} + +.panelTitleBar .panelTitle { + padding: 0; + border: none; + background: none; + flex: 1; + min-width: 0; +} + +.panelExpandBtn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + padding: 0; + border: 1px solid var(--border-color, #e0e0e0); + border-radius: 4px; + background: var(--surface-color, #fff); + color: var(--text-secondary, #666); + cursor: pointer; + flex-shrink: 0; + transition: background 0.15s, color 0.15s, border-color 0.15s; +} + +.panelExpandBtn:hover { + background: var(--surface-alt, #f5f5f5); + color: var(--primary-color, #4A90D9); + border-color: var(--primary-color, #4A90D9); +} + +.popupPanelList { + max-height: none; + padding: 0; +} + .transcriptList, .responseList { flex: 1; diff --git a/src/pages/views/teamsbot/TeamsbotSessionView.tsx b/src/pages/views/teamsbot/TeamsbotSessionView.tsx index 9e8febd..151f73c 100644 --- a/src/pages/views/teamsbot/TeamsbotSessionView.tsx +++ b/src/pages/views/teamsbot/TeamsbotSessionView.tsx @@ -25,6 +25,7 @@ import styles from './Teamsbot.module.css'; import { useLanguage } from '../../../providers/language/LanguageContext'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; +import { Popup } from '../../../components/UiComponents/Popup'; /** * TeamsbotSessionView - Live session view with real-time transcript and bot responses. @@ -54,6 +55,8 @@ export const TeamsbotSessionView: React.FC = () => { const [screenshotsLoading, setScreenshotsLoading] = useState(false); const [screenshotsLoaded, setScreenshotsLoaded] = useState(false); const [screenshotsExpanded, setScreenshotsExpanded] = useState(false); + const [transcriptPopupOpen, setTranscriptPopupOpen] = useState(false); + const [botResponsesPopupOpen, setBotResponsesPopupOpen] = useState(false); const [ttsStatusEvents, setTtsStatusEvents] = useState { return colors[Math.abs(hash) % colors.length]; }; + const _renderExpandIcon = () => ( + + + + ); + + const _renderTranscriptList = (endRef?: React.RefObject) => ( + <> + {transcripts.map((seg) => ( +
+ {_formatTime(seg.timestamp)} + + {seg.speaker || t('Unbekannt')}: + + {seg.text} +
+ ))} + {endRef &&
} + {transcripts.length === 0 && ( +
{t('Noch kein Transkript vorhanden')}
+ )} + + ); + + const _renderBotResponsesList = () => ( + <> + {botResponses.map((r) => ( +
+
+ {r.detectedIntent} + {_formatTime(r.timestamp || '')} +
+
+ {r.responseText || ''} +
+ {r.reasoning && ( +
+ {t('Begründung: {text}', { text: r.reasoning })} +
+ )} + {(r.modelName || r.processingTime != null) && ( +
+ {r.modelName || ''} + {r.processingTime != null && {r.processingTime.toFixed(1)}s} + {r.priceCHF != null && {r.priceCHF.toFixed(4)} CHF} +
+ )} +
+ ))} + {botResponses.length === 0 && ( +
{t('Noch keine Botantworten')}
+ )} + + ); + if (loading) return
{t('Sitzung laden')}
; if (noSessions) return (
@@ -1154,63 +1215,69 @@ export const TeamsbotSessionView: React.FC = () => {
{/* Left: Transcript */}
-

- {t('Transkript ({count} Segmente)', { count: transcripts.length })} -

+
+

+ {t('Transkript ({count} Segmente)', { count: transcripts.length })} +

+ +
- {transcripts.map((seg) => ( -
- {_formatTime(seg.timestamp)} - - {seg.speaker || t('Unbekannt')}: - - {seg.text} -
- ))} -
- {transcripts.length === 0 && ( -
{t('Noch kein Transkript vorhanden')}
- )} + {_renderTranscriptList(transcriptEndRef)}
{/* Right: Bot Responses */}
-

Bot-Antworten ({botResponses.length})

+
+

Bot-Antworten ({botResponses.length})

+ +
- {botResponses.map((r) => ( -
-
- {r.detectedIntent} - {_formatTime(r.timestamp || '')} -
-
- {r.responseText || ''} -
- {r.reasoning && ( -
- {t('Begründung: {text}', { text: r.reasoning })} -
- )} - {(r.modelName || r.processingTime != null) && ( -
- {r.modelName || ''} - {r.processingTime != null && {r.processingTime.toFixed(1)}s} - {r.priceCHF != null && {r.priceCHF.toFixed(4)} CHF} -
- )} -
- ))} - {botResponses.length === 0 && ( -
{t('Noch keine Botantworten')}
- )} + {_renderBotResponsesList()}
+ setTranscriptPopupOpen(false)} + size="fullscreen" + closeOnBackdropClick + > +
+ {_renderTranscriptList()} +
+
+ + setBotResponsesPopupOpen(false)} + size="fullscreen" + closeOnBackdropClick + > +
+ {_renderBotResponsesList()} +
+
+ {/* Summary (for ended sessions) */} {session.summary && (
diff --git a/src/pages/views/workspace/WorkspacePage.tsx b/src/pages/views/workspace/WorkspacePage.tsx index 27b913d..9948da4 100644 --- a/src/pages/views/workspace/WorkspacePage.tsx +++ b/src/pages/views/workspace/WorkspacePage.tsx @@ -94,6 +94,7 @@ export const WorkspacePage: React.FC = ({ persistentInstance ); const [mobileLeftOpen, setMobileLeftOpen] = useState(false); const [mobileRightOpen, setMobileRightOpen] = useState(false); + const [chatListRefreshKey, setChatListRefreshKey] = useState(0); useEffect(() => { const _handleResize = () => { @@ -254,6 +255,27 @@ export const WorkspacePage: React.FC = ({ persistentInstance workspace.loadWorkflow(wfId); }; + const sidebarHeaderBtnStyle: React.CSSProperties = { + background: 'none', + border: 'none', + cursor: 'pointer', + fontSize: 14, + color: '#888', + }; + + const createChatBtnStyle: React.CSSProperties = { + ...sidebarHeaderBtnStyle, + fontSize: 20, + fontWeight: 700, + lineHeight: 1, + color: 'var(--text-secondary, #555)', + }; + + const _handleCreateNewChat = useCallback(() => { + workspace.resetToNew(); + setChatListRefreshKey(k => k + 1); + }, [workspace]); + const tabButtonStyle = (active: boolean): React.CSSProperties => ({ flex: 1, padding: '6px 0', @@ -356,7 +378,7 @@ export const WorkspacePage: React.FC = ({ persistentInstance onTabChange={setUdbTab} onSelectChat={_handleConversationSelect} activeWorkflowId={workspace.workflowId ?? undefined} - onCreateNewChat={workspace.resetToNew} + chatListRefreshKey={chatListRefreshKey} onRenameChat={_handleRenameChat} onDeleteChat={_handleDeleteChat} onFileSelect={_handleFileSelect} @@ -408,7 +430,10 @@ export const WorkspacePage: React.FC = ({ persistentInstance }}>
{t('Workspace')} - +
+ + +
{_leftPanelBody} @@ -604,7 +629,10 @@ export const WorkspacePage: React.FC = ({ persistentInstance >
{t('Workspace')} - +
+ + +
{_leftPanelBody}