From 2a0078a0df19321eee48e2c20d64ef5c37890ea7 Mon Sep 17 00:00:00 2001 From: Richard Griffiths Date: Sat, 2 May 2026 19:49:12 +0200 Subject: [PATCH 01/29] Add conversation diagnostics panel --- .../ConversationDiagnosticsModal.css | 86 +++ .../ConversationDiagnosticsModal.tsx | 138 +++++ .../ConversationDiagnosticsModal/index.ts | 1 + .../ConversationEditor/ConversationEditor.tsx | 16 +- .../conversation-diagnostics-utils.test.ts | 179 ++++++ .../utils/conversation-diagnostics-utils.ts | 524 ++++++++++++++++++ 6 files changed, 942 insertions(+), 2 deletions(-) create mode 100644 app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css create mode 100644 app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx create mode 100644 app/src/components/ConversationDiagnosticsModal/index.ts create mode 100644 app/src/utils/__tests__/conversation-diagnostics-utils.test.ts create mode 100644 app/src/utils/conversation-diagnostics-utils.ts diff --git a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css new file mode 100644 index 0000000..6e95492 --- /dev/null +++ b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css @@ -0,0 +1,86 @@ +@import '../../css/constants.css'; + +.conversation-diagnostics { + display: flex; + flex-direction: column; + gap: $mediumSpace; + min-height: 24rem; + max-height: min(70vh, 48rem); +} + +.conversation-diagnostics__summary { + display: flex; + flex-direction: column; + gap: $mediumSpace; +} + +.conversation-diagnostics__summary .ant-alert { + border-radius: $tinySpace; +} + +.conversation-diagnostics__list { + display: flex; + flex-direction: column; + gap: $smallSpace; + min-height: 0; + overflow-y: auto; + padding-right: $tinySpace; +} + +.conversation-diagnostics__item { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: start; + gap: $mediumSpace; + padding: $mediumSpace; + border: 1px solid $borderTertiary; + border-left-width: 4px; + border-radius: $tinySpace; + background: $backgroundSecondary; +} + +.conversation-diagnostics__item--error { + border-left-color: #ff4d4f; +} + +.conversation-diagnostics__item--warning { + border-left-color: #faad14; +} + +.conversation-diagnostics__item-main { + min-width: 0; +} + +.conversation-diagnostics__item-title { + display: flex; + align-items: center; + gap: $tinySpace; + min-width: 0; + color: $textColourPrimary; + font-weight: 600; +} + +.conversation-diagnostics__item-title > span:last-child { + min-width: 0; + overflow-wrap: anywhere; +} + +.conversation-diagnostics__item-node { + margin-top: $tinySpace; + color: $textColourSecondary; + font-size: $fontSizeSmall; +} + +.conversation-diagnostics__item-description { + margin-top: $smallSpace; + color: $textColourPrimary; + line-height: 1.4; + overflow-wrap: anywhere; +} + +.conversation-diagnostics__jump-button.ant-btn { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 6rem; +} diff --git a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx new file mode 100644 index 0000000..0444a39 --- /dev/null +++ b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx @@ -0,0 +1,138 @@ +import { useEffect, useState } from 'react'; +import type { ComponentProps } from 'react'; +import { Alert, Button, Empty, Segmented, Tag } from 'antd'; +import { AimOutlined } from '@ant-design/icons'; +import { observer } from 'mobx-react'; + +import { useStore } from 'hooks/useStore'; +import { DataStore } from 'stores/dataStore/data-store'; +import { DefStore } from 'stores/defStore/def-store'; +import { ModalStore } from 'stores/modalStore/modal-store'; +import { NodeStore } from 'stores/nodeStore/node-store'; +import { buildConversationDiagnostics, ConversationDiagnostic, ConversationDiagnosticSeverity } from 'utils/conversation-diagnostics-utils'; + +import './ConversationDiagnosticsModal.css'; + +type Props = { + globalModalId: string; +}; + +type FilterValue = 'all' | ConversationDiagnosticSeverity; +type SegmentedOptions = NonNullable['options']>; + +function getSeverityLabel(severity: ConversationDiagnosticSeverity): string { + if (severity === 'error') return 'Error'; + return 'Warning'; +} + +function getCategoryLabel(category: ConversationDiagnostic['category']): string { + if (category === 'graph') return 'Graph'; + if (category === 'operation') return 'Operation'; + if (category === 'reference') return 'Reference'; + return 'Content'; +} + +function ConversationDiagnosticsModal({ globalModalId }: Props) { + const dataStore = useStore('data'); + const defStore = useStore('def'); + const modalStore = useStore('modal'); + const nodeStore = useStore('node'); + const [filter, setFilter] = useState('all'); + + const conversationAsset = dataStore.unsavedActiveConversationAsset; + const diagnostics = + conversationAsset == null + ? [] + : buildConversationDiagnostics({ + conversationAsset, + operationDefinitions: defStore.operations, + loadedConversationAssets: Array.from(dataStore.conversationAssets.values()), + }); + + const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === 'error').length; + const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === 'warning').length; + const filteredDiagnostics = diagnostics.filter((diagnostic) => filter === 'all' || diagnostic.severity === filter); + + const filterOptions: SegmentedOptions = [ + { label: `All (${diagnostics.length})`, value: 'all' }, + { label: `Errors (${errorCount})`, value: 'error' }, + { label: `Warnings (${warningCount})`, value: 'warning' }, + ]; + + const jumpToNode = (diagnostic: ConversationDiagnostic) => { + if (diagnostic.nodeId == null) return; + + nodeStore.setActiveNode(diagnostic.nodeId); + modalStore.closeModal(globalModalId); + window.setTimeout(() => nodeStore.scrollToActiveNode(true), 50); + }; + + useEffect(() => { + modalStore.setTitle('Conversation Diagnostics', globalModalId); + modalStore.setWidth('78vw', globalModalId); + modalStore.setShowOkButton(false, globalModalId); + modalStore.setShowCancelButton(true, globalModalId); + modalStore.setCancelLabel('Close', globalModalId); + }, []); + + return ( +
+
+ 0 ? 'error' : warningCount > 0 ? 'warning' : 'success'} + showIcon + message={ + diagnostics.length > 0 + ? `${diagnostics.length} diagnostic${diagnostics.length === 1 ? '' : 's'} found` + : 'No diagnostics found' + } + description={ + diagnostics.length > 0 + ? `${errorCount} error${errorCount === 1 ? '' : 's'} and ${warningCount} warning${warningCount === 1 ? '' : 's'} in the active conversation.` + : 'The active conversation passed the current content, graph, operation, and reference checks.' + } + /> + {diagnostics.length > 0 && ( + setFilter(value as FilterValue)} + /> + )} +
+ + {filteredDiagnostics.length <= 0 ? ( + + ) : ( +
+ {filteredDiagnostics.map((diagnostic) => ( +
+
+
+ {getSeverityLabel(diagnostic.severity)} + {getCategoryLabel(diagnostic.category)} + {diagnostic.title} +
+
{diagnostic.nodeLabel}
+
{diagnostic.description}
+
+ +
+ ))} +
+ )} +
+ ); +} + +export const ObservingConversationDiagnosticsModal = observer(ConversationDiagnosticsModal); diff --git a/app/src/components/ConversationDiagnosticsModal/index.ts b/app/src/components/ConversationDiagnosticsModal/index.ts new file mode 100644 index 0000000..1bb5eb9 --- /dev/null +++ b/app/src/components/ConversationDiagnosticsModal/index.ts @@ -0,0 +1 @@ +export { ObservingConversationDiagnosticsModal as ConversationDiagnosticsModal } from './ConversationDiagnosticsModal'; diff --git a/app/src/containers/ConversationEditor/ConversationEditor.tsx b/app/src/containers/ConversationEditor/ConversationEditor.tsx index 711b77d..2b5e86c 100644 --- a/app/src/containers/ConversationEditor/ConversationEditor.tsx +++ b/app/src/containers/ConversationEditor/ConversationEditor.tsx @@ -2,8 +2,8 @@ import { useEffect, useState } from 'react'; import type { ChangeEvent, ComponentProps } from 'react'; import { toJS } from 'mobx'; import { observer } from 'mobx-react'; -import { message, Button, Row, Col, Form, Input, Tabs, Popconfirm } from 'antd'; -import { ArrowRightOutlined, MenuFoldOutlined, MenuUnfoldOutlined, RetweetOutlined, SaveOutlined } from '@ant-design/icons'; +import { message, Button, Row, Col, Form, Input, Tabs, Popconfirm, Tooltip } from 'antd'; +import { ArrowRightOutlined, MenuFoldOutlined, MenuUnfoldOutlined, RetweetOutlined, SaveOutlined, WarningOutlined } from '@ant-design/icons'; import { updateConversation } from 'services/api'; import { regenerateConversationId } from 'utils/conversation-utils'; @@ -11,9 +11,11 @@ import { detectType } from 'utils/node-utils'; import { useStore } from 'hooks/useStore'; import { DialogEditor } from 'components/DialogEditor'; import { DialogTextArea } from 'components/DialogTextArea'; +import { ConversationDiagnosticsModal } from 'components/ConversationDiagnosticsModal'; import { Split } from 'components/Split'; import { NodeStore } from 'stores/nodeStore/node-store'; import { DataStore } from 'stores/dataStore/data-store'; +import { ModalStore } from 'stores/modalStore/modal-store'; import { positiveActionButtonProps } from 'utils/antd-button-utils'; import { ElementNodeType, ConversationAssetType } from 'types'; @@ -58,6 +60,7 @@ const inactiveNodeSplitSizes = { function ConversationEditor({ conversationAsset }: Props) { const nodeStore = useStore('node'); const dataStore = useStore('data'); + const modalStore = useStore('modal'); const [isAllExpanded, setIsAllExpanded] = useState(true); const { unsavedActiveConversationAsset } = dataStore; @@ -192,6 +195,15 @@ function ConversationEditor({ conversationAsset }: Props) { }} icon={} /> + + + + + {diagnostics.length > 0 && ( +
setFilter(value as FilterValue)} /> - )} -
+
+ } + placeholder="Search diagnostics..." + value={searchText} + onChange={(event) => setSearchText(event.target.value)} + /> + + value={categoryFilter} + onChange={setCategoryFilter} + options={[ + { label: 'All areas', value: 'all' }, + { label: 'Content', value: 'content' }, + { label: 'Graph', value: 'graph' }, + { label: 'Operation', value: 'operation' }, + { label: 'Reference', value: 'reference' }, + ]} + /> +
+ + )} {filteredDiagnostics.length <= 0 ? ( - + ) : (
{filteredDiagnostics.map((diagnostic) => (
-
-
- {getSeverityLabel(diagnostic.severity)} - {getCategoryLabel(diagnostic.category)} - {diagnostic.title} + ))}
@@ -135,4 +211,19 @@ function ConversationDiagnosticsModal({ globalModalId }: Props) { ); } +function getSummaryTone(errorCount: number, warningCount: number): ConversationDiagnosticSeverity | 'success' { + if (errorCount > 0) return 'error'; + if (warningCount > 0) return 'warning'; + return 'success'; +} + +function MetaRow({ label, value }: { label: string; value: string }) { + return ( +
+
{label}
+
{value}
+
+ ); +} + export const ObservingConversationDiagnosticsModal = observer(ConversationDiagnosticsModal); From 4efa4f8bf14a694ec7c4335437004104209b0d71 Mon Sep 17 00:00:00 2001 From: Richard Griffiths Date: Sat, 2 May 2026 21:28:06 +0200 Subject: [PATCH 05/29] Fix diagnostics filter control styling --- .../ConversationDiagnosticsModal.css | 91 ++++++++++++++++++- .../ConversationDiagnosticsModal.tsx | 2 + 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css index 9859ba1..b226e93 100644 --- a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css +++ b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css @@ -86,12 +86,38 @@ .conversation-diagnostics__toolbar .ant-segmented { justify-self: start; + padding: 0; background: transparent; + box-shadow: inset 0 -1px 0 #dfe5ef; } .conversation-diagnostics__toolbar .ant-segmented-item { - min-height: 36px; - padding: 0 $smallSpace; + min-width: 6rem; + height: 40px; + margin: 0; + padding: 0; + border-radius: 0; + color: #384250; +} + +.conversation-diagnostics__toolbar .ant-segmented-item-label { + display: flex; + align-items: center; + justify-content: center; + height: 40px; + padding: 0 $mediumSpace; + line-height: 40px; +} + +.conversation-diagnostics__toolbar .ant-segmented-thumb { + border-radius: 4px 4px 0 0; + box-shadow: inset 0 -2px 0 $linkColourTertiary; +} + +.conversation-diagnostics__toolbar .ant-segmented-item-selected { + color: $linkColourTertiary; + font-weight: $fontWeightMedium; + box-shadow: inset 0 -2px 0 $linkColourTertiary; } .conversation-diagnostics__toolbar-controls { @@ -106,6 +132,67 @@ border-radius: 4px; } +.conversation-diagnostics__category-select.ant-select { + --ant-select-background-color: #ffffff; + --ant-select-border-color: #d6dde8; + --ant-select-color: #30343a; +} + +.conversation-diagnostics__category-select.ant-select.ant-select-outlined:not(.ant-select-customize-input), +.conversation-diagnostics__category-select.ant-select:not(.ant-select-customize-input) .ant-select-selector { + background-color: #ffffff; + border-color: #d6dde8; + color: #30343a; +} + +.conversation-diagnostics__category-select.ant-select.ant-select-single .ant-select-selector .ant-select-selection-item, +.conversation-diagnostics__category-select.ant-select.ant-select-single .ant-select-selector .ant-select-selection-placeholder, +.conversation-diagnostics__category-select.ant-select:not(.ant-select-customize-input).ant-select-single .ant-select-selector .ant-select-selection-search-input { + color: #30343a; +} + +.conversation-diagnostics__category-select.ant-select .ant-select-arrow, +.conversation-diagnostics__category-select.ant-select .ant-select-suffix { + color: $linkColourTertiary; +} + +.conversation-diagnostics__category-dropdown.ant-select-dropdown { + padding: $tinySpace; + border-radius: 6px; + background: #ffffff; + color: #30343a; + box-shadow: + 0 8px 24px rgba(0, 0, 0, 0.12), + 0 0 0 1px rgba(26, 36, 52, 0.1); +} + +.conversation-diagnostics__category-dropdown.ant-select-dropdown .ant-select-item { + min-height: 36px; + border-radius: 4px; + background: #ffffff; + color: #30343a; +} + +.conversation-diagnostics__category-dropdown.ant-select-dropdown .ant-select-item-option-content { + color: inherit; +} + +.conversation-diagnostics__category-dropdown.ant-select-dropdown .ant-select-item-option-active:not(.ant-select-item-option-disabled) { + background: #eef6ff; + color: #123a67; +} + +.conversation-diagnostics__category-dropdown.ant-select-dropdown .ant-select-item-option-selected:not(.ant-select-item-option-disabled) { + border-left: 0; + background: #e6f4ff; + color: $linkColourTertiary; + font-weight: $fontWeightSemibold; +} + +.conversation-diagnostics__category-dropdown.ant-select-dropdown .ant-select-item-option-selected:not(.ant-select-item-option-disabled) .ant-select-item-option-state { + color: $linkColourTertiary; +} + .conversation-diagnostics__list { display: flex; flex-direction: column; diff --git a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx index 3202cc9..ae6f05f 100644 --- a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx +++ b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx @@ -157,6 +157,8 @@ function ConversationDiagnosticsModal({ globalModalId }: Props) { onChange={(event) => setSearchText(event.target.value)} /> + className="conversation-diagnostics__category-select" + classNames={{ popup: { root: 'conversation-diagnostics__category-dropdown' } }} value={categoryFilter} onChange={setCategoryFilter} options={[ From e72a18af8addc3d1ab2fd4d97065241510e847d5 Mon Sep 17 00:00:00 2001 From: Richard Griffiths Date: Sat, 2 May 2026 21:33:30 +0200 Subject: [PATCH 06/29] Add informational diagnostics severity --- .../ConversationDiagnosticsModal.css | 37 +++++++++++ .../ConversationDiagnosticsModal.tsx | 39 +++++++++-- .../conversation-diagnostics-utils.test.ts | 65 ++++++++++++++++++- .../utils/conversation-diagnostics-utils.ts | 42 ++++++++---- 4 files changed, 162 insertions(+), 21 deletions(-) diff --git a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css index b226e93..d5c9907 100644 --- a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css +++ b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css @@ -29,6 +29,11 @@ background: linear-gradient(90deg, #fff7f6 0%, #ffffff 68%); } +.conversation-diagnostics__summary--info { + border-color: #cfe8ff; + background: linear-gradient(90deg, #f5fbff 0%, #ffffff 68%); +} + .conversation-diagnostics__summary--success { border-color: #d6f1df; background: linear-gradient(90deg, #f7fff9 0%, #ffffff 68%); @@ -50,6 +55,10 @@ background: #ff4d4f; } +.conversation-diagnostics__summary--info .conversation-diagnostics__summary-icon { + background: #1890ff; +} + .conversation-diagnostics__summary--success .conversation-diagnostics__summary-icon { background: #52c41a; } @@ -132,6 +141,30 @@ border-radius: 4px; } +.conversation-diagnostics__toolbar-controls .ant-input-affix-wrapper { + background: #ffffff; + border-color: #d6dde8; + color: #30343a; +} + +.conversation-diagnostics__toolbar-controls .ant-input-affix-wrapper .ant-input { + background: transparent; + color: #30343a; +} + +.conversation-diagnostics__toolbar-controls .ant-input-affix-wrapper .ant-input::placeholder { + color: #8a94a6; + font-style: normal; +} + +.conversation-diagnostics__toolbar-controls .ant-input-prefix { + color: #596273; +} + +.conversation-diagnostics__toolbar-controls .ant-input-clear-icon { + color: #697386; +} + .conversation-diagnostics__category-select.ant-select { --ant-select-background-color: #ffffff; --ant-select-border-color: #d6dde8; @@ -227,6 +260,10 @@ background: #ff4d4f; } +.conversation-diagnostics__item--info .conversation-diagnostics__item-accent { + background: #1890ff; +} + .conversation-diagnostics__item-body { display: grid; grid-template-columns: minmax(0, 1fr) minmax(13rem, 22rem); diff --git a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx index ae6f05f..ec52e7f 100644 --- a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx +++ b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx @@ -1,7 +1,15 @@ import { useEffect, useState } from 'react'; import type { ComponentProps } from 'react'; import { Button, Empty, Input, Select, Segmented, Tag } from 'antd'; -import { AimOutlined, CheckCircleOutlined, DownloadOutlined, ExclamationCircleOutlined, SearchOutlined, WarningOutlined } from '@ant-design/icons'; +import { + AimOutlined, + CheckCircleOutlined, + DownloadOutlined, + ExclamationCircleOutlined, + InfoCircleOutlined, + SearchOutlined, + WarningOutlined, +} from '@ant-design/icons'; import { observer } from 'mobx-react'; import { useStore } from 'hooks/useStore'; @@ -24,9 +32,16 @@ type SegmentedOptions = NonNullable['options']> function getSeverityLabel(severity: ConversationDiagnosticSeverity): string { if (severity === 'error') return 'Error'; + if (severity === 'info') return 'Info'; return 'Warning'; } +function getSeverityTagColour(severity: ConversationDiagnosticSeverity): string { + if (severity === 'error') return 'error'; + if (severity === 'info') return 'processing'; + return 'warning'; +} + function getCategoryLabel(category: ConversationDiagnostic['category']): string { if (category === 'graph') return 'Graph'; if (category === 'operation') return 'Operation'; @@ -55,6 +70,7 @@ function ConversationDiagnosticsModal({ globalModalId }: Props) { const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === 'error').length; const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === 'warning').length; + const infoCount = diagnostics.filter((diagnostic) => diagnostic.severity === 'info').length; const normalisedSearchText = searchText.trim().toLowerCase(); const filteredDiagnostics = diagnostics.filter((diagnostic) => { if (filter !== 'all' && diagnostic.severity !== filter) return false; @@ -71,6 +87,7 @@ function ConversationDiagnosticsModal({ globalModalId }: Props) { { label: `All (${diagnostics.length})`, value: 'all' }, { label: `Errors (${errorCount})`, value: 'error' }, { label: `Warnings (${warningCount})`, value: 'warning' }, + { label: `Info (${infoCount})`, value: 'info' }, ]; const jumpToNode = (diagnostic: ConversationDiagnostic) => { @@ -87,7 +104,7 @@ function ConversationDiagnosticsModal({ globalModalId }: Props) { `Conversation Diagnostics - ${conversationName}`, '', `${diagnostics.length} diagnostic${diagnostics.length === 1 ? '' : 's'} found`, - `${errorCount} error${errorCount === 1 ? '' : 's'} and ${warningCount} warning${warningCount === 1 ? '' : 's'}`, + getSummaryCountsText(errorCount, warningCount, infoCount), '', ...diagnostics.flatMap((diagnostic, index) => [ `${index + 1}. [${getSeverityLabel(diagnostic.severity)}] ${diagnostic.title}`, @@ -119,9 +136,9 @@ function ConversationDiagnosticsModal({ globalModalId }: Props) { return (
-
+
@@ -131,7 +148,7 @@ function ConversationDiagnosticsModal({ globalModalId }: Props) {
{diagnostics.length > 0 - ? `${errorCount} error${errorCount === 1 ? '' : 's'} and ${warningCount} warning${warningCount === 1 ? '' : 's'} in the active conversation.` + ? `${getSummaryCountsText(errorCount, warningCount, infoCount)} in the active conversation.` : 'The active conversation passed the current content, graph, operation, and reference checks.'}
@@ -183,7 +200,7 @@ function ConversationDiagnosticsModal({ globalModalId }: Props) {
- {getSeverityLabel(diagnostic.severity)} + {getSeverityLabel(diagnostic.severity)} {getCategoryLabel(diagnostic.category)}
{diagnostic.title}
@@ -213,12 +230,20 @@ function ConversationDiagnosticsModal({ globalModalId }: Props) { ); } -function getSummaryTone(errorCount: number, warningCount: number): ConversationDiagnosticSeverity | 'success' { +function getSummaryTone(errorCount: number, warningCount: number, infoCount: number): ConversationDiagnosticSeverity | 'success' { if (errorCount > 0) return 'error'; if (warningCount > 0) return 'warning'; + if (infoCount > 0) return 'info'; return 'success'; } +function getSummaryCountsText(errorCount: number, warningCount: number, infoCount: number): string { + const errorText = `${errorCount} error${errorCount === 1 ? '' : 's'}`; + const warningText = `${warningCount} warning${warningCount === 1 ? '' : 's'}`; + const infoText = `${infoCount} info`; + return `${errorText}, ${warningText}, and ${infoText}`; +} + function MetaRow({ label, value }: { label: string; value: string }) { return (
diff --git a/app/src/utils/__tests__/conversation-diagnostics-utils.test.ts b/app/src/utils/__tests__/conversation-diagnostics-utils.test.ts index b90eac0..c003fff 100644 --- a/app/src/utils/__tests__/conversation-diagnostics-utils.test.ts +++ b/app/src/utils/__tests__/conversation-diagnostics-utils.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; import { buildConversationDiagnostics } from 'utils/conversation-diagnostics-utils'; -import { createConversation, createPromptNode, createRootNode, getId } from 'utils/conversation-utils'; +import { createConversation, createPromptNode, createResponseNode, createRootNode, getId } from 'utils/conversation-utils'; import type { ConversationAssetType, OperationArgType, OperationCallType, OperationDefinitionType } from 'types'; function makeStringArg(value: string): OperationArgType { @@ -97,6 +97,40 @@ describe('conversation diagnostics', () => { ); }); + it('allows empty prompt nodes that continue into response choices', () => { + const conversationAsset = makeBasicConversation(); + const prompt = conversationAsset.conversation.nodes[0]; + const commanderResponse = createResponseNode(); + commanderResponse.responseText = 'Keep going.'; + commanderResponse.nextNodeIndex = -1; + prompt.text = ''; + prompt.branches = [commanderResponse]; + + const diagnostics = buildConversationDiagnostics({ conversationAsset, operationDefinitions: [] }); + const promptDiagnostics = diagnostics.filter((diagnostic) => diagnostic.nodeId === getId(prompt)); + + expect(promptDiagnostics).toEqual( + expect.arrayContaining([expect.objectContaining({ severity: 'info', title: 'Prompt node has no text' })]), + ); + expect(promptDiagnostics.some((diagnostic) => diagnostic.severity === 'warning' && diagnostic.title.startsWith('Prompt node'))).toBe(false); + }); + + it('reports empty prompt nodes that have no responses or actions', () => { + const conversationAsset = makeBasicConversation(); + const prompt = conversationAsset.conversation.nodes[0]; + prompt.text = ''; + prompt.branches = []; + prompt.actions = null; + + const diagnostics = buildConversationDiagnostics({ conversationAsset, operationDefinitions: [] }); + + expect(diagnostics).toEqual( + expect.arrayContaining([ + expect.objectContaining({ severity: 'warning', title: 'Prompt node has no content or responses', nodeId: getId(prompt) }), + ]), + ); + }); + it('does not treat operation definition values as strict validation rules', () => { const conversationAsset = makeBasicConversation(); const prompt = conversationAsset.conversation.nodes[0]; @@ -144,6 +178,35 @@ describe('conversation diagnostics', () => { ); }); + it('reports an empty conversation sub header as info instead of a warning', () => { + const conversationAsset = makeBasicConversation(); + const prompt = conversationAsset.conversation.nodes[0]; + const response = createResponseNode(); + response.responseText = 'Open the custom conversation.'; + response.actions = { + ops: [makeAction('Start Conversation Custom', [makeStringArg('conversation_target'), makeStringArg('Dead Claim'), makeStringArg('')])], + }; + prompt.branches = [response]; + + const diagnostics = buildConversationDiagnostics({ + conversationAsset, + operationDefinitions: [ + makeDefinition('Start Conversation Custom', [ + { label: 'Conversation Id', types: ['string'] }, + { label: 'Conversation Header', types: ['string'] }, + { label: 'Conversation Sub Header', types: ['string'] }, + ]), + ], + }); + + expect(diagnostics).toEqual( + expect.arrayContaining([expect.objectContaining({ severity: 'info', title: 'Operation input is empty', nodeId: getId(response) })]), + ); + expect(diagnostics).not.toEqual( + expect.arrayContaining([expect.objectContaining({ severity: 'warning', title: 'Operation input is empty', nodeId: getId(response) })]), + ); + }); + it('uses the runtime prompt node position for link target checks', () => { const conversationAsset = makeBasicConversation(); const root = conversationAsset.conversation.roots[0]; diff --git a/app/src/utils/conversation-diagnostics-utils.ts b/app/src/utils/conversation-diagnostics-utils.ts index abb1d5b..66d5e02 100644 --- a/app/src/utils/conversation-diagnostics-utils.ts +++ b/app/src/utils/conversation-diagnostics-utils.ts @@ -1,7 +1,7 @@ import type { ConversationAssetType, ElementNodeType, InputType, OperationArgType, OperationCallType, OperationDefinitionType, PromptNodeType } from 'types'; import { getId } from './conversation-utils'; -export type ConversationDiagnosticSeverity = 'error' | 'warning'; +export type ConversationDiagnosticSeverity = 'error' | 'warning' | 'info'; export type ConversationDiagnosticCategory = 'content' | 'graph' | 'operation' | 'reference'; export type ConversationDiagnostic = { @@ -111,15 +111,23 @@ function scanConversationShape( nodes.forEach((node) => { const context = getPromptNodeContext(node); - if (node.text.trim() === '' && !isPromptNodeAutoFollowRouter(node)) { + if (node.text.trim() === '') { diagnostics.push( - createDiagnostic({ - severity: 'warning', - category: 'content', - title: 'Prompt node has no text', - description: 'BattleTech can use empty prompt nodes as auto-follow routers, but this prompt does not look like a simple router. Check that the player will not be left on stale or missing dialogue text.', - context, - }), + isEmptyPromptLikelyInert(node) + ? createDiagnostic({ + severity: 'warning', + category: 'content', + title: 'Prompt node has no content or responses', + description: 'This prompt has no speaker text, no response options, and no actions. Check that the player will not be left on stale or missing dialogue text.', + context, + }) + : createDiagnostic({ + severity: 'info', + category: 'content', + title: 'Prompt node has no text', + description: 'This prompt has no speaker text, but it has response choices or actions. This is valid for continuation routing, chained commander response runs, and other intentional silent prompt nodes.', + context, + }), ); } @@ -130,8 +138,8 @@ function scanConversationShape( }); } -function isPromptNodeAutoFollowRouter(node: PromptNodeType): boolean { - return node.branches.some((branch) => branch.responseText === ''); +function isEmptyPromptLikelyInert(node: PromptNodeType): boolean { + return node.branches.length <= 0 && (node.actions?.ops?.length ?? 0) <= 0; } function scanElementNodeTarget( @@ -335,12 +343,16 @@ function scanOperationArg( } if (!isInputOptional(operation.functionName, input) && isEmptyRequiredArg(arg, input)) { + const severity = isInformationalEmptyArg(operation.functionName, input) ? 'info' : 'warning'; diagnostics.push( createDiagnostic({ - severity: 'warning', + severity, category: 'operation', title: 'Operation input is empty', - description: `${inputPath} is empty. This may fail or do nothing at runtime.`, + description: + severity === 'info' + ? `${inputPath} is empty. This is valid, but check that the missing value is intentional.` + : `${inputPath} is empty. This may fail or do nothing at runtime.`, context, }), ); @@ -472,6 +484,10 @@ function isInputOptional(functionName: string, input: InputType): boolean { return false; } +function isInformationalEmptyArg(functionName: string, input: InputType): boolean { + return functionName === 'Start Conversation Custom' && input.label.toLowerCase() === 'conversation sub header'; +} + function buildLoadedConversationsById(conversationAssets: ConversationAssetType[]): Map { const conversationsById = new Map(); From 4c67726103c7f836671b1b3ca6841a29e2d0fcdf Mon Sep 17 00:00:00 2001 From: Richard Griffiths Date: Sat, 2 May 2026 21:34:52 +0200 Subject: [PATCH 07/29] Space conversation editor toolbar buttons --- app/src/containers/ConversationEditor/ConversationEditor.css | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/containers/ConversationEditor/ConversationEditor.css b/app/src/containers/ConversationEditor/ConversationEditor.css index 6374588..99a12f7 100644 --- a/app/src/containers/ConversationEditor/ConversationEditor.css +++ b/app/src/containers/ConversationEditor/ConversationEditor.css @@ -37,11 +37,14 @@ } &__tool-buttons { + display: flex; + align-items: center; + gap: $smallSpace; margin-bottom: $smallSpace; } &__expand-nodes { - margin-right: $smallSpace; + flex: 0 0 auto; } &__details { From 3517d4d2c122974aa3b2d51db335cdd5501d5d82 Mon Sep 17 00:00:00 2001 From: Richard Griffiths Date: Sat, 2 May 2026 21:36:25 +0200 Subject: [PATCH 08/29] Prevent diagnostics rows from shrinking --- .../ConversationDiagnosticsModal.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css index d5c9907..a25b7da 100644 --- a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css +++ b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css @@ -229,6 +229,7 @@ .conversation-diagnostics__list { display: flex; flex-direction: column; + flex: 1 1 auto; gap: $smallMediumSpace; min-height: 0; overflow-y: auto; @@ -237,6 +238,7 @@ .conversation-diagnostics__item { display: grid; + flex: 0 0 auto; grid-template-columns: 4px minmax(0, 1fr); overflow: hidden; border-radius: 6px; From fc8c27bec72955f473c97a3c8490329ae8189b72 Mon Sep 17 00:00:00 2001 From: Richard Griffiths Date: Sat, 2 May 2026 21:42:12 +0200 Subject: [PATCH 09/29] Collapse diagnostics rows by default --- .../ConversationDiagnosticsModal.css | 74 +++++++++++- .../ConversationDiagnosticsModal.tsx | 114 +++++++++++++----- 2 files changed, 154 insertions(+), 34 deletions(-) diff --git a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css index a25b7da..eb7b475 100644 --- a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css +++ b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css @@ -131,12 +131,13 @@ .conversation-diagnostics__toolbar-controls { display: grid; - grid-template-columns: minmax(12rem, 18rem) 10rem; + grid-template-columns: minmax(12rem, 18rem) 10rem auto; gap: $smallMediumSpace; } .conversation-diagnostics__toolbar-controls .ant-input-affix-wrapper, -.conversation-diagnostics__toolbar-controls .ant-select-selector { +.conversation-diagnostics__toolbar-controls .ant-select-selector, +.conversation-diagnostics__toolbar-controls .ant-btn { min-height: 40px; border-radius: 4px; } @@ -266,11 +267,58 @@ background: #1890ff; } +.conversation-diagnostics__item-panel { + min-width: 0; +} + +.conversation-diagnostics__item-summary { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: $smallMediumSpace; + width: 100%; + min-height: 58px; + padding: $smallMediumSpace $mediumLargeSpace; + border: 0; + background: transparent; + color: inherit; + text-align: left; + cursor: pointer; + transition-property: background-color; + transition-duration: 160ms; + transition-timing-function: cubic-bezier(0.2, 0, 0, 1); +} + +.conversation-diagnostics__item-summary:hover, +.conversation-diagnostics__item-summary:focus-visible { + background: #f7faff; +} + +.conversation-diagnostics__item-summary:focus-visible { + outline: 2px solid #91caff; + outline-offset: -2px; +} + +.conversation-diagnostics__item-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + color: #596273; + font-size: 12px; +} + +.conversation-diagnostics__item-summary-copy { + min-width: 0; +} + .conversation-diagnostics__item-body { display: grid; grid-template-columns: minmax(0, 1fr) minmax(13rem, 22rem); gap: $mediumLargeSpace; - padding: $mediumLargeSpace; + padding: 0 $mediumLargeSpace $mediumLargeSpace calc($mediumLargeSpace + 36px); + box-shadow: inset 0 1px 0 rgba(26, 36, 52, 0.08); } .conversation-diagnostics__item-main { @@ -281,7 +329,8 @@ display: flex; align-items: center; gap: $tinySpace; - margin-bottom: $smallSpace; + justify-content: flex-end; + min-width: max-content; } .conversation-diagnostics__item-tags .ant-tag { @@ -301,14 +350,14 @@ } .conversation-diagnostics__item-node { - margin-top: $smallSpace; + display: block; + margin-top: 2px; color: #697386; font-size: $fontSizeSmall; line-height: $lineHeightSmall; } .conversation-diagnostics__item-description { - margin-top: $smallMediumSpace; color: #384250; line-height: 1.5; overflow-wrap: anywhere; @@ -385,6 +434,19 @@ grid-template-columns: 1fr; } + .conversation-diagnostics__item-summary { + grid-template-columns: auto minmax(0, 1fr); + } + + .conversation-diagnostics__item-tags { + grid-column: 2; + justify-content: flex-start; + } + + .conversation-diagnostics__item-body { + padding-left: $mediumLargeSpace; + } + .conversation-diagnostics__item-details { padding-top: $mediumSpace; padding-left: 0; diff --git a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx index ec52e7f..2389d3e 100644 --- a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx +++ b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx @@ -4,9 +4,11 @@ import { Button, Empty, Input, Select, Segmented, Tag } from 'antd'; import { AimOutlined, CheckCircleOutlined, + DownOutlined, DownloadOutlined, ExclamationCircleOutlined, InfoCircleOutlined, + RightOutlined, SearchOutlined, WarningOutlined, } from '@ant-design/icons'; @@ -57,6 +59,7 @@ function ConversationDiagnosticsModal({ globalModalId }: Props) { const [filter, setFilter] = useState('all'); const [categoryFilter, setCategoryFilter] = useState('all'); const [searchText, setSearchText] = useState(''); + const [expandedDiagnosticIds, setExpandedDiagnosticIds] = useState>(() => new Set()); const conversationAsset = dataStore.unsavedActiveConversationAsset; const diagnostics = @@ -89,6 +92,32 @@ function ConversationDiagnosticsModal({ globalModalId }: Props) { { label: `Warnings (${warningCount})`, value: 'warning' }, { label: `Info (${infoCount})`, value: 'info' }, ]; + const allFilteredDiagnosticsExpanded = + filteredDiagnostics.length > 0 && filteredDiagnostics.every((diagnostic) => expandedDiagnosticIds.has(diagnostic.id)); + + const toggleDiagnostic = (diagnosticId: string) => { + setExpandedDiagnosticIds((currentIds) => { + const nextIds = new Set(currentIds); + if (nextIds.has(diagnosticId)) { + nextIds.delete(diagnosticId); + } else { + nextIds.add(diagnosticId); + } + return nextIds; + }); + }; + + const toggleFilteredDiagnostics = () => { + setExpandedDiagnosticIds((currentIds) => { + const nextIds = new Set(currentIds); + if (allFilteredDiagnosticsExpanded) { + filteredDiagnostics.forEach((diagnostic) => nextIds.delete(diagnostic.id)); + } else { + filteredDiagnostics.forEach((diagnostic) => nextIds.add(diagnostic.id)); + } + return nextIds; + }); + }; const jumpToNode = (diagnostic: ConversationDiagnostic) => { if (diagnostic.nodeId == null) return; @@ -134,6 +163,14 @@ function ConversationDiagnosticsModal({ globalModalId }: Props) { modalStore.setCancelLabel('Close', globalModalId); }, []); + useEffect(() => { + const diagnosticIds = new Set(diagnostics.map((diagnostic) => diagnostic.id)); + setExpandedDiagnosticIds((currentIds) => { + const nextIds = new Set([...currentIds].filter((diagnosticId) => diagnosticIds.has(diagnosticId))); + return nextIds.size === currentIds.size ? currentIds : nextIds; + }); + }, [diagnostics.map((diagnostic) => diagnostic.id).join('|')]); + return (
@@ -186,6 +223,7 @@ function ConversationDiagnosticsModal({ globalModalId }: Props) { { label: 'Reference', value: 'reference' }, ]} /> +
)} @@ -194,36 +232,56 @@ function ConversationDiagnosticsModal({ globalModalId }: Props) { ) : (
- {filteredDiagnostics.map((diagnostic) => ( -
- -
- ))} + ); + })}
)}
From 840544e62c2e3cfb4ed2401423156883f7912204 Mon Sep 17 00:00:00 2001 From: Richard Griffiths Date: Sat, 2 May 2026 21:55:07 +0200 Subject: [PATCH 10/29] Relax expanded diagnostics row spacing --- .../ConversationDiagnosticsModal.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css index eb7b475..9739c6b 100644 --- a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css +++ b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css @@ -317,7 +317,7 @@ display: grid; grid-template-columns: minmax(0, 1fr) minmax(13rem, 22rem); gap: $mediumLargeSpace; - padding: 0 $mediumLargeSpace $mediumLargeSpace calc($mediumLargeSpace + 36px); + padding: $smallMediumSpace $mediumLargeSpace $mediumLargeSpace calc($mediumLargeSpace + 36px); box-shadow: inset 0 1px 0 rgba(26, 36, 52, 0.08); } From ee047ad04aae80d4d46008c629b3a8617cde4984 Mon Sep 17 00:00:00 2001 From: Richard Griffiths Date: Sat, 2 May 2026 22:07:23 +0200 Subject: [PATCH 11/29] Add reusable diagnostics side panel --- .../ConversationDiagnosticsModal.css | 168 ++++++++++++++++++ .../ConversationDiagnosticsModal.tsx | 32 +++- .../ConversationDiagnosticsModal/index.ts | 1 + .../ConversationEditor/ConversationEditor.tsx | 22 ++- .../Conversations/Conversations.css | 9 + .../Conversations/Conversations.tsx | 6 +- app/src/containers/SidePanel/SidePanel.css | 63 +++++++ app/src/containers/SidePanel/SidePanel.tsx | 29 +++ app/src/containers/SidePanel/index.ts | 1 + app/src/hooks/useStore.ts | 4 +- app/src/stores/index.ts | 3 + app/src/stores/sidePanelStore/index.ts | 2 + .../sidePanelStore/side-panel-store.tsx | 64 +++++++ 13 files changed, 394 insertions(+), 10 deletions(-) create mode 100644 app/src/containers/SidePanel/SidePanel.css create mode 100644 app/src/containers/SidePanel/SidePanel.tsx create mode 100644 app/src/containers/SidePanel/index.ts create mode 100644 app/src/stores/sidePanelStore/index.ts create mode 100644 app/src/stores/sidePanelStore/side-panel-store.tsx diff --git a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css index 9739c6b..c76d778 100644 --- a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css +++ b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css @@ -10,6 +10,13 @@ -webkit-font-smoothing: antialiased; } +.conversation-diagnostics--side-panel { + height: 100%; + min-height: 0; + max-height: none; + color: $textColourPrimary; +} + .conversation-diagnostics__summary { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; @@ -227,6 +234,33 @@ color: $linkColourTertiary; } +.conversation-diagnostics__category-dropdown--dark.ant-select-dropdown { + background: #202831; + color: $textColourPrimary; + box-shadow: + 0 8px 24px rgba(0, 0, 0, 0.35), + 0 0 0 1px rgba(255, 255, 255, 0.12); +} + +.conversation-diagnostics__category-dropdown--dark.ant-select-dropdown .ant-select-item { + background: #202831; + color: $textColourPrimary; +} + +.conversation-diagnostics__category-dropdown--dark.ant-select-dropdown .ant-select-item-option-active:not(.ant-select-item-option-disabled) { + background: rgba(24, 144, 255, 0.16); + color: white; +} + +.conversation-diagnostics__category-dropdown--dark.ant-select-dropdown .ant-select-item-option-selected:not(.ant-select-item-option-disabled) { + background: rgba(24, 144, 255, 0.22); + color: white; +} + +.conversation-diagnostics__category-dropdown--dark.ant-select-dropdown .ant-select-item-option-selected:not(.ant-select-item-option-disabled) .ant-select-item-option-state { + color: $blue4; +} + .conversation-diagnostics__list { display: flex; flex-direction: column; @@ -453,3 +487,137 @@ box-shadow: inset 0 1px 0 rgba(26, 36, 52, 0.1); } } + +.conversation-diagnostics--side-panel .conversation-diagnostics__summary { + grid-template-columns: auto minmax(0, 1fr); + border-color: rgba(24, 144, 255, 0.45); + background: rgba(24, 144, 255, 0.06); + box-shadow: + inset 0 0 0 1px rgba(255, 255, 255, 0.03), + 0 12px 28px rgba(0, 0, 0, 0.22); +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__summary .ant-btn { + grid-column: 1 / -1; + justify-self: stretch; + color: $textColourPrimary; + background: rgba(255, 255, 255, 0.05); + border-color: rgba(255, 255, 255, 0.2); +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__summary .ant-btn:hover, +.conversation-diagnostics--side-panel .conversation-diagnostics__summary .ant-btn:focus { + color: white; + background: rgba(24, 144, 255, 0.16); + border-color: rgba(24, 144, 255, 0.7); +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__summary-title { + color: white; +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__summary-description { + color: #c8d2df; +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__toolbar { + grid-template-columns: 1fr; + gap: $smallMediumSpace; +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__toolbar .ant-segmented { + box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.16); +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__toolbar .ant-segmented-item { + color: #c8d2df; +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__toolbar .ant-segmented-item-selected { + color: $blue4; + box-shadow: inset 0 -2px 0 $blue4; +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__toolbar .ant-segmented-thumb { + box-shadow: inset 0 -2px 0 $blue4; +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__toolbar-controls { + grid-template-columns: 1fr; +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__toolbar-controls .ant-input-affix-wrapper, +.conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select.ant-select-outlined:not(.ant-select-customize-input), +.conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select:not(.ant-select-customize-input) .ant-select-selector, +.conversation-diagnostics--side-panel .conversation-diagnostics__toolbar-controls .ant-btn { + background: rgba(14, 18, 23, 0.72); + border-color: rgba(255, 255, 255, 0.2); + color: $textColourPrimary; +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__toolbar-controls .ant-input-affix-wrapper .ant-input, +.conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select.ant-select-single .ant-select-selector .ant-select-selection-item, +.conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select.ant-select-single .ant-select-selector .ant-select-selection-placeholder, +.conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select:not(.ant-select-customize-input).ant-select-single .ant-select-selector .ant-select-selection-search-input { + color: $textColourPrimary; +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__toolbar-controls .ant-input-affix-wrapper .ant-input::placeholder { + color: #9aa7b7; +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__toolbar-controls .ant-input-prefix, +.conversation-diagnostics--side-panel .conversation-diagnostics__toolbar-controls .ant-input-clear-icon, +.conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select .ant-select-arrow { + color: $blue4; +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__item { + background: rgba(255, 255, 255, 0.035); + box-shadow: + inset 0 0 0 1px rgba(255, 255, 255, 0.12), + 0 10px 24px rgba(0, 0, 0, 0.2); +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__item-summary:hover, +.conversation-diagnostics--side-panel .conversation-diagnostics__item-summary:focus-visible { + background: rgba(255, 255, 255, 0.055); +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__item-title { + color: white; +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__item-node, +.conversation-diagnostics--side-panel .conversation-diagnostics__item-toggle { + color: #b6c1d0; +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__item-body { + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__item-description { + color: #d5dee9; +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__item-details { + box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.12); +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__meta-label { + color: #dfe7f2; +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__meta-value { + color: #eaf1f9; + background: rgba(255, 255, 255, 0.1); +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__jump-button.ant-btn { + color: $blue4; +} + +.conversation-diagnostics--side-panel .ant-empty-description { + color: #c8d2df; +} diff --git a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx index 2389d3e..326d94a 100644 --- a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx +++ b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx @@ -12,6 +12,7 @@ import { SearchOutlined, WarningOutlined, } from '@ant-design/icons'; +import classnames from 'classnames'; import { observer } from 'mobx-react'; import { useStore } from 'hooks/useStore'; @@ -25,7 +26,8 @@ import type { ConversationDiagnostic, ConversationDiagnosticCategory, Conversati import './ConversationDiagnosticsModal.css'; type Props = { - globalModalId: string; + globalModalId?: string; + variant?: 'modal' | 'side-panel'; }; type FilterValue = 'all' | ConversationDiagnosticSeverity; @@ -51,7 +53,7 @@ function getCategoryLabel(category: ConversationDiagnostic['category']): string return 'Content'; } -function ConversationDiagnosticsModal({ globalModalId }: Props) { +function ConversationDiagnosticsContent({ globalModalId, variant = 'modal' }: Props) { const dataStore = useStore('data'); const defStore = useStore('def'); const modalStore = useStore('modal'); @@ -123,7 +125,7 @@ function ConversationDiagnosticsModal({ globalModalId }: Props) { if (diagnostic.nodeId == null) return; nodeStore.setActiveNode(diagnostic.nodeId); - modalStore.closeModal(globalModalId); + if (globalModalId != null) modalStore.closeModal(globalModalId); window.setTimeout(() => nodeStore.scrollToActiveNode(true), 50); }; @@ -156,6 +158,8 @@ function ConversationDiagnosticsModal({ globalModalId }: Props) { }; useEffect(() => { + if (globalModalId == null) return; + modalStore.setTitle('Conversation Diagnostics', globalModalId); modalStore.setWidth('min(74rem, 86vw)', globalModalId); modalStore.setShowOkButton(false, globalModalId); @@ -172,7 +176,11 @@ function ConversationDiagnosticsModal({ globalModalId }: Props) { }, [diagnostics.map((diagnostic) => diagnostic.id).join('|')]); return ( -
+
diff --git a/app/src/containers/Conversations/Conversations.css b/app/src/containers/Conversations/Conversations.css index 8cbeb9d..aeb95fa 100644 --- a/app/src/containers/Conversations/Conversations.css +++ b/app/src/containers/Conversations/Conversations.css @@ -63,7 +63,16 @@ height: 32px; } + &__workspace { + display: flex; + height: 100%; + min-width: 0; + min-height: 0; + overflow: hidden; + } + &__main { + flex: 1 1 auto; height: 100%; min-width: 0; min-height: 0; diff --git a/app/src/containers/Conversations/Conversations.tsx b/app/src/containers/Conversations/Conversations.tsx index 9adaa7f..38065f8 100644 --- a/app/src/containers/Conversations/Conversations.tsx +++ b/app/src/containers/Conversations/Conversations.tsx @@ -14,6 +14,7 @@ import type { SplitHandleDoubleClickContext } from 'components/Split'; import { ConversationTree } from '../ConversationTree'; import { ConversationEditor } from '../ConversationEditor'; +import { SidePanel } from '../SidePanel'; import { SplashScreen } from '../SplashScreen'; import './Conversations.css'; @@ -122,7 +123,10 @@ const Conversations = () => { )}
-
{mainView}
+
+
{mainView}
+ +
); diff --git a/app/src/containers/SidePanel/SidePanel.css b/app/src/containers/SidePanel/SidePanel.css new file mode 100644 index 0000000..9916fab --- /dev/null +++ b/app/src/containers/SidePanel/SidePanel.css @@ -0,0 +1,63 @@ +@import '../../css/constants.css'; + +.side-panel { + display: flex; + flex: 0 0 var(--side-panel-width); + flex-direction: column; + width: var(--side-panel-width); + min-width: 24rem; + max-width: min(34rem, 42vw); + min-height: 0; + color: $textColourPrimary; + background: + radial-gradient(circle at top right, rgba(42, 176, 247, 0.12), transparent 28rem), + #1c2229; + box-shadow: + inset 1px 0 0 rgba(255, 255, 255, 0.08), + -10px 0 28px rgba(0, 0, 0, 0.25); +} + +.side-panel__header { + display: flex; + flex: 0 0 auto; + align-items: center; + justify-content: space-between; + gap: $mediumSpace; + min-height: 58px; + padding: $mediumSpace $mediumLargeSpace $smallMediumSpace $mediumLargeSpace; +} + +.side-panel__title { + min-width: 0; + overflow: hidden; + color: white; + font-size: $fontSizeLarge; + font-weight: $fontWeightSemibold; + line-height: $lineHeightLarge; + text-overflow: ellipsis; + white-space: nowrap; +} + +.side-panel__close.ant-btn { + flex: 0 0 auto; + min-width: 40px; + min-height: 40px; + color: #b6c1d0; +} + +.side-panel__close.ant-btn:hover, +.side-panel__close.ant-btn:focus { + color: white; + background: rgba(255, 255, 255, 0.08); +} + +.side-panel__close.ant-btn:active { + transform: scale(0.96); +} + +.side-panel__content { + flex: 1 1 auto; + min-width: 0; + min-height: 0; + padding: 0 $mediumLargeSpace $mediumLargeSpace $mediumLargeSpace; +} diff --git a/app/src/containers/SidePanel/SidePanel.tsx b/app/src/containers/SidePanel/SidePanel.tsx new file mode 100644 index 0000000..9443647 --- /dev/null +++ b/app/src/containers/SidePanel/SidePanel.tsx @@ -0,0 +1,29 @@ +import { isValidElement } from 'react'; +import type { CSSProperties } from 'react'; +import { Button } from 'antd'; +import { CloseOutlined } from '@ant-design/icons'; +import { observer } from 'mobx-react'; + +import { useStore } from 'hooks/useStore'; +import { SidePanelStore } from 'stores/sidePanelStore/side-panel-store'; + +import './SidePanel.css'; + +function SidePanel() { + const sidePanelStore = useStore('sidePanel'); + const { panel, options } = sidePanelStore; + + if (!options.isVisible || panel == null) return null; + + return ( + + ); +} + +export const ObservingSidePanel = observer(SidePanel); diff --git a/app/src/containers/SidePanel/index.ts b/app/src/containers/SidePanel/index.ts new file mode 100644 index 0000000..28ed05c --- /dev/null +++ b/app/src/containers/SidePanel/index.ts @@ -0,0 +1 @@ +export { ObservingSidePanel as SidePanel } from './SidePanel'; diff --git a/app/src/hooks/useStore.ts b/app/src/hooks/useStore.ts index 33847fb..1ba886d 100644 --- a/app/src/hooks/useStore.ts +++ b/app/src/hooks/useStore.ts @@ -5,9 +5,10 @@ import { NodeStore } from 'stores/nodeStore/node-store'; import { DataStore } from 'stores/dataStore/data-store'; import { ErrorStore } from 'stores/errorStore/error-store'; import { ModalStore } from 'stores/modalStore/modal-store'; +import { SidePanelStore } from 'stores/sidePanelStore/side-panel-store'; import { DefStore } from 'stores/defStore/def-store'; -export const useStore = (storeType: string): T => { +export const useStore = (storeType: string): T => { const store = useContext(storeContext); if (store == null) { @@ -17,6 +18,7 @@ export const useStore = ; + this.options = observable.object( + { + isVisible: show, + title, + width: '31rem', + props, + }, + {}, + { deep: false }, + ); + } + + closePanel = (): void => { + this.reset(); + }; + + reset = (): void => { + this.options.isVisible = false; + }; +} + +export const sidePanelStore = getDevPreservedStore('__conversetekSidePanelStore', () => new SidePanelStore(), SidePanelStore.prototype); + +export { SidePanelStore }; From 1e155f0e52858465bdb5ac6d6e7eb5aeb1e66c94 Mon Sep 17 00:00:00 2001 From: Richard Griffiths Date: Sat, 2 May 2026 22:13:22 +0200 Subject: [PATCH 12/29] Polish diagnostics side panel behaviour --- .../ConversationDiagnosticsModal.css | 14 +++++++++- .../ConversationEditor/ConversationEditor.tsx | 18 ++++++------ app/src/containers/SidePanel/SidePanel.css | 28 +++++++++++++++++++ app/src/containers/SidePanel/SidePanel.tsx | 20 +++++++++++-- 4 files changed, 66 insertions(+), 14 deletions(-) diff --git a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css index c76d778..df14d8a 100644 --- a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css +++ b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css @@ -594,6 +594,9 @@ } .conversation-diagnostics--side-panel .conversation-diagnostics__item-body { + grid-template-columns: 1fr; + gap: $mediumSpace; + padding-left: $mediumLargeSpace; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); } @@ -602,7 +605,16 @@ } .conversation-diagnostics--side-panel .conversation-diagnostics__item-details { - box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.12); + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: $smallMediumSpace; + padding-top: $smallMediumSpace; + padding-left: 0; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12); +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__jump-button.ant-btn { + grid-column: 1 / -1; } .conversation-diagnostics--side-panel .conversation-diagnostics__meta-label { diff --git a/app/src/containers/ConversationEditor/ConversationEditor.tsx b/app/src/containers/ConversationEditor/ConversationEditor.tsx index e793adb..040eca9 100644 --- a/app/src/containers/ConversationEditor/ConversationEditor.tsx +++ b/app/src/containers/ConversationEditor/ConversationEditor.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import type { ChangeEvent, ComponentProps } from 'react'; import { toJS } from 'mobx'; import { observer } from 'mobx-react'; -import { message, Button, Row, Col, Form, Input, Tabs, Popconfirm, Tooltip } from 'antd'; +import { message, Button, Row, Col, Form, Input, Tabs, Popconfirm } from 'antd'; import { ArrowRightOutlined, MenuFoldOutlined, MenuUnfoldOutlined, RetweetOutlined, SaveOutlined, WarningOutlined } from '@ant-design/icons'; import { updateConversation } from 'services/api'; @@ -213,15 +213,13 @@ function ConversationEditor({ conversationAsset }: Props) { }} icon={} /> - -
diff --git a/app/src/containers/SidePanel/SidePanel.css b/app/src/containers/SidePanel/SidePanel.css index 9916fab..55ba588 100644 --- a/app/src/containers/SidePanel/SidePanel.css +++ b/app/src/containers/SidePanel/SidePanel.css @@ -15,6 +15,22 @@ box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.08), -10px 0 28px rgba(0, 0, 0, 0.25); + opacity: 1; + transform: translateX(0); + transition-property: transform, opacity; + transition-duration: 180ms; + transition-timing-function: cubic-bezier(0.2, 0, 0, 1); + will-change: transform, opacity; +} + +.side-panel--open { + animation: side-panel-enter 180ms cubic-bezier(0.2, 0, 0, 1); +} + +.side-panel--closing { + opacity: 0; + pointer-events: none; + transform: translateX(100%); } .side-panel__header { @@ -61,3 +77,15 @@ min-height: 0; padding: 0 $mediumLargeSpace $mediumLargeSpace $mediumLargeSpace; } + +@keyframes side-panel-enter { + from { + opacity: 0; + transform: translateX(100%); + } + + to { + opacity: 1; + transform: translateX(0); + } +} diff --git a/app/src/containers/SidePanel/SidePanel.tsx b/app/src/containers/SidePanel/SidePanel.tsx index 9443647..560d93d 100644 --- a/app/src/containers/SidePanel/SidePanel.tsx +++ b/app/src/containers/SidePanel/SidePanel.tsx @@ -1,4 +1,4 @@ -import { isValidElement } from 'react'; +import { isValidElement, useEffect, useState } from 'react'; import type { CSSProperties } from 'react'; import { Button } from 'antd'; import { CloseOutlined } from '@ant-design/icons'; @@ -12,11 +12,25 @@ import './SidePanel.css'; function SidePanel() { const sidePanelStore = useStore('sidePanel'); const { panel, options } = sidePanelStore; + const [shouldRender, setShouldRender] = useState(options.isVisible); - if (!options.isVisible || panel == null) return null; + useEffect(() => { + if (options.isVisible) { + setShouldRender(true); + return undefined; + } + + const timeoutId = window.setTimeout(() => setShouldRender(false), 180); + return () => window.clearTimeout(timeoutId); + }, [options.isVisible]); + + if (!shouldRender || panel == null) return null; return ( -
{diagnostics.length > 0 - ? `${getSummaryCountsText(errorCount, warningCount, infoCount)} in the active conversation.` + ? `${getSummaryCountsText(errorCount, warningCount, infoCount)}.` : 'The active conversation passed the current content, graph, operation, and reference checks.'}
From 9d825b3fabb28600772f623af0d124043bad8791 Mon Sep 17 00:00:00 2001 From: Richard Griffiths Date: Sat, 2 May 2026 22:32:44 +0200 Subject: [PATCH 18/29] Compact diagnostics expand control --- .../ConversationDiagnosticsModal.css | 20 ++++++++++++++++++- .../ConversationDiagnosticsModal.tsx | 13 +++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css index 9bb2142..6252f9d 100644 --- a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css +++ b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css @@ -149,6 +149,10 @@ border-radius: 4px; } +.conversation-diagnostics__expand-toggle.ant-btn { + min-width: 40px; +} + .conversation-diagnostics__toolbar-controls .ant-input-affix-wrapper { background: #ffffff; border-color: #d6dde8; @@ -581,7 +585,21 @@ } .conversation-diagnostics--side-panel .conversation-diagnostics__toolbar-controls { - grid-template-columns: 1fr; + grid-template-columns: minmax(0, 1fr) 40px; + align-items: center; +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__toolbar-controls .ant-input-affix-wrapper { + grid-column: 1 / -1; +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__category-select { + min-width: 0; +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__expand-toggle.ant-btn { + width: 40px; + padding: 0; } .conversation-diagnostics--side-panel .conversation-diagnostics__toolbar-controls .ant-input-affix-wrapper, diff --git a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx index 9141047..2e7da1e 100644 --- a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx +++ b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx @@ -4,9 +4,11 @@ import { Button, Empty, Input, Select, Segmented, Tag } from 'antd'; import { AimOutlined, CheckCircleOutlined, + CompressOutlined, DownOutlined, DownloadOutlined, ExclamationCircleOutlined, + ExpandAltOutlined, InfoCircleOutlined, RightOutlined, SearchOutlined, @@ -96,6 +98,7 @@ function ConversationDiagnosticsContent({ globalModalId, variant = 'modal' }: Pr ]; const allFilteredDiagnosticsExpanded = filteredDiagnostics.length > 0 && filteredDiagnostics.every((diagnostic) => expandedDiagnosticIds.has(diagnostic.id)); + const toggleVisibleDiagnosticsLabel = allFilteredDiagnosticsExpanded ? 'Collapse visible diagnostics' : 'Expand visible diagnostics'; const toggleDiagnostic = (diagnosticId: string) => { setExpandedDiagnosticIds((currentIds) => { @@ -237,7 +240,15 @@ function ConversationDiagnosticsContent({ globalModalId, variant = 'modal' }: Pr { label: 'Reference', value: 'reference' }, ]} /> - +
)} From 28f2f2ec03d924db4fd186b24819f375f4f9814a Mon Sep 17 00:00:00 2001 From: Richard Griffiths Date: Sat, 2 May 2026 22:37:45 +0200 Subject: [PATCH 19/29] Refine side panel icon controls --- .../ConversationDiagnosticsModal.css | 8 ++++++-- app/src/containers/SidePanel/SidePanel.css | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css index 6252f9d..308637a 100644 --- a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css +++ b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css @@ -585,7 +585,7 @@ } .conversation-diagnostics--side-panel .conversation-diagnostics__toolbar-controls { - grid-template-columns: minmax(0, 1fr) 40px; + grid-template-columns: minmax(0, 1fr) 32px; align-items: center; } @@ -598,7 +598,11 @@ } .conversation-diagnostics--side-panel .conversation-diagnostics__expand-toggle.ant-btn { - width: 40px; + justify-self: end; + width: 32px; + min-width: 32px; + height: 32px; + min-height: 32px; padding: 0; } diff --git a/app/src/containers/SidePanel/SidePanel.css b/app/src/containers/SidePanel/SidePanel.css index 63d506a..ac18abf 100644 --- a/app/src/containers/SidePanel/SidePanel.css +++ b/app/src/containers/SidePanel/SidePanel.css @@ -102,8 +102,8 @@ .side-panel__close.ant-btn:hover, .side-panel__close.ant-btn:focus { - color: white; - background: rgba(255, 255, 255, 0.08); + color: white !important; + background: rgba(24, 144, 255, 0.16) !important; } .side-panel__close.ant-btn:active { From 79a171297b92e32256f12c543d59ace5d5fe1d26 Mon Sep 17 00:00:00 2001 From: Richard Griffiths Date: Sat, 2 May 2026 22:41:21 +0200 Subject: [PATCH 20/29] Align diagnostics dropdown theme --- .../ConversationDiagnosticsModal.css | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css index 308637a..daebf6f 100644 --- a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css +++ b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css @@ -239,30 +239,31 @@ } .conversation-diagnostics__category-dropdown--dark.ant-select-dropdown { - background: #202831; + background: $backgroundTertiary; color: $textColourPrimary; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35), - 0 0 0 1px rgba(255, 255, 255, 0.12); + 0 0 0 1px rgba(255, 255, 255, 0.1); } .conversation-diagnostics__category-dropdown--dark.ant-select-dropdown .ant-select-item { - background: #202831; + background: $backgroundTertiary; color: $textColourPrimary; } .conversation-diagnostics__category-dropdown--dark.ant-select-dropdown .ant-select-item-option-active:not(.ant-select-item-option-disabled) { - background: rgba(24, 144, 255, 0.16); + background: $highlightColourPrimary; color: white; } .conversation-diagnostics__category-dropdown--dark.ant-select-dropdown .ant-select-item-option-selected:not(.ant-select-item-option-disabled) { - background: rgba(24, 144, 255, 0.22); + border-left: 3px solid $linkColourPrimary; + background: rgba(42, 176, 247, 0.14); color: white; } .conversation-diagnostics__category-dropdown--dark.ant-select-dropdown .ant-select-item-option-selected:not(.ant-select-item-option-disabled) .ant-select-item-option-state { - color: $blue4; + color: white; } .conversation-diagnostics__list { @@ -619,7 +620,12 @@ .conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select.ant-select-single .ant-select-selector .ant-select-selection-item, .conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select.ant-select-single .ant-select-selector .ant-select-selection-placeholder, .conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select:not(.ant-select-customize-input).ant-select-single .ant-select-selector .ant-select-selection-search-input { - color: $textColourPrimary; + color: $textColourPrimary !important; +} + +.conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select-open .ant-select-selector .ant-select-selection-item, +.conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select-focused .ant-select-selector .ant-select-selection-item { + color: $textColourPrimary !important; } .conversation-diagnostics--side-panel .conversation-diagnostics__toolbar-controls .ant-input-affix-wrapper .ant-input::placeholder { From 9a25e7aa7ad72c9c18bd96cfa85ace56204a0507 Mon Sep 17 00:00:00 2001 From: Richard Griffiths Date: Sat, 2 May 2026 22:43:21 +0200 Subject: [PATCH 21/29] Toggle diagnostics side panel button --- .../containers/ConversationEditor/ConversationEditor.tsx | 9 ++++++++- app/src/stores/sidePanelStore/side-panel-store.tsx | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app/src/containers/ConversationEditor/ConversationEditor.tsx b/app/src/containers/ConversationEditor/ConversationEditor.tsx index 040eca9..accc0eb 100644 --- a/app/src/containers/ConversationEditor/ConversationEditor.tsx +++ b/app/src/containers/ConversationEditor/ConversationEditor.tsx @@ -59,6 +59,8 @@ const inactiveNodeSplitSizes = { minSecondarySize: '0%', }; +const diagnosticsSidePanelId = 'conversation-diagnostics'; + function ConversationEditor({ conversationAsset }: Props) { const nodeStore = useStore('node'); const dataStore = useStore('data'); @@ -112,7 +114,12 @@ function ConversationEditor({ conversationAsset }: Props) { const onDiagnosticsButtonClicked = () => { if ((windowSize.width ?? 0) >= 1280) { - sidePanelStore.setPanelContent(ConversationDiagnosticsPanel, {}, 'Conversation Diagnostics'); + if (sidePanelStore.isPanelVisible(diagnosticsSidePanelId)) { + sidePanelStore.closePanel(); + return; + } + + sidePanelStore.setPanelContent(ConversationDiagnosticsPanel, {}, 'Conversation Diagnostics', true, diagnosticsSidePanelId); return; } diff --git a/app/src/stores/sidePanelStore/side-panel-store.tsx b/app/src/stores/sidePanelStore/side-panel-store.tsx index 7e07beb..3e3b7fa 100644 --- a/app/src/stores/sidePanelStore/side-panel-store.tsx +++ b/app/src/stores/sidePanelStore/side-panel-store.tsx @@ -4,6 +4,7 @@ import { action, makeObservable, observable } from 'mobx'; import { getDevPreservedStore } from '../dev-preserved-store'; type SidePanelOptions = { + contentId: string | null; isVisible: boolean; title: string | ReactElement; width: string; @@ -16,6 +17,7 @@ class SidePanelStore { panel: ElementType | ReactElement | null = null; options: SidePanelOptions = observable.object( { + contentId: null, isVisible: false, title: '', width: defaultSidePanelWidth, @@ -39,11 +41,12 @@ class SidePanelStore { }); } - setPanelContent(PanelContent: ElementType, props = {}, title: string | ReactElement = '', show = true): void { + setPanelContent(PanelContent: ElementType, props = {}, title: string | ReactElement = '', show = true, contentId: string | null = null): void { this.panelVersion += 1; this.panel = ; this.options = observable.object( { + contentId, isVisible: show, title, width: this.options.width || defaultSidePanelWidth, @@ -69,6 +72,10 @@ class SidePanelStore { reset = (): void => { this.options.isVisible = false; }; + + isPanelVisible(contentId: string): boolean { + return this.options.isVisible && this.options.contentId === contentId; + } } export const sidePanelStore = getDevPreservedStore('__conversetekSidePanelStore', () => new SidePanelStore(), SidePanelStore.prototype); From 4dace903da720a941b45d0b544fb9e639fe9cfbc Mon Sep 17 00:00:00 2001 From: Richard Griffiths Date: Sat, 2 May 2026 23:45:41 +0200 Subject: [PATCH 22/29] Clarify diagnostics dropdown contrast --- .../ConversationDiagnosticsModal.css | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css index daebf6f..af250d7 100644 --- a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css +++ b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css @@ -239,31 +239,39 @@ } .conversation-diagnostics__category-dropdown--dark.ant-select-dropdown { - background: $backgroundTertiary; - color: $textColourPrimary; + background: $backgroundTertiary !important; + color: $textColourPrimary !important; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(255, 255, 255, 0.1); } .conversation-diagnostics__category-dropdown--dark.ant-select-dropdown .ant-select-item { - background: $backgroundTertiary; - color: $textColourPrimary; + background: $backgroundTertiary !important; + color: $textColourPrimary !important; +} + +.conversation-diagnostics__category-dropdown--dark.ant-select-dropdown .ant-select-item-option-content { + color: inherit !important; } .conversation-diagnostics__category-dropdown--dark.ant-select-dropdown .ant-select-item-option-active:not(.ant-select-item-option-disabled) { - background: $highlightColourPrimary; - color: white; + background: rgba(255, 255, 255, 0.08) !important; + color: white !important; } .conversation-diagnostics__category-dropdown--dark.ant-select-dropdown .ant-select-item-option-selected:not(.ant-select-item-option-disabled) { border-left: 3px solid $linkColourPrimary; - background: rgba(42, 176, 247, 0.14); - color: white; + background: rgba(255, 255, 255, 0.04) !important; + color: white !important; +} + +.conversation-diagnostics__category-dropdown--dark.ant-select-dropdown .ant-select-item-option-selected:not(.ant-select-item-option-disabled).ant-select-item-option-active { + background: rgba(255, 255, 255, 0.1) !important; } .conversation-diagnostics__category-dropdown--dark.ant-select-dropdown .ant-select-item-option-selected:not(.ant-select-item-option-disabled) .ant-select-item-option-state { - color: white; + color: $linkColourPrimary !important; } .conversation-diagnostics__list { @@ -617,10 +625,14 @@ } .conversation-diagnostics--side-panel .conversation-diagnostics__toolbar-controls .ant-input-affix-wrapper .ant-input, +.conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select .ant-select-selection-item, +.conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select .ant-select-selection-placeholder, +.conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select .ant-select-selection-search-input, .conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select.ant-select-single .ant-select-selector .ant-select-selection-item, .conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select.ant-select-single .ant-select-selector .ant-select-selection-placeholder, .conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select:not(.ant-select-customize-input).ant-select-single .ant-select-selector .ant-select-selection-search-input { color: $textColourPrimary !important; + -webkit-text-fill-color: $textColourPrimary !important; } .conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select-open .ant-select-selector .ant-select-selection-item, From f55799cb8b2aec088c6c415315921107c1834389 Mon Sep 17 00:00:00 2001 From: Richard Griffiths Date: Sat, 2 May 2026 23:47:28 +0200 Subject: [PATCH 23/29] Compact diagnostics export action --- .../ConversationDiagnosticsModal.css | 3 +++ .../ConversationDiagnosticsModal.tsx | 11 +++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css index af250d7..3e78cd5 100644 --- a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css +++ b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css @@ -519,7 +519,10 @@ .conversation-diagnostics--side-panel .conversation-diagnostics__summary .ant-btn { grid-column: auto; justify-self: end; + width: 34px; + min-width: 34px; min-height: 34px; + padding: 0; color: $textColourPrimary; background: $backgroundPrimary; border-color: $borderTertiary; diff --git a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx index 2e7da1e..654682d 100644 --- a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx +++ b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx @@ -200,8 +200,15 @@ function ConversationDiagnosticsContent({ globalModalId, variant = 'modal' }: Pr : 'The active conversation passed the current content, graph, operation, and reference checks.'} - From 8f411f833f8cf4a5d1fdc586b51394ba235c72cc Mon Sep 17 00:00:00 2001 From: Richard Griffiths Date: Sat, 2 May 2026 23:51:13 +0200 Subject: [PATCH 24/29] Split diagnostics select themes --- .../ConversationDiagnosticsModal.css | 49 ++++++++++--------- .../ConversationDiagnosticsModal.tsx | 11 +++-- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css index 3e78cd5..a57fe73 100644 --- a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css +++ b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css @@ -238,7 +238,7 @@ color: $linkColourTertiary; } -.conversation-diagnostics__category-dropdown--dark.ant-select-dropdown { +.conversation-diagnostics__category-dropdown--side-panel.ant-select-dropdown { background: $backgroundTertiary !important; color: $textColourPrimary !important; box-shadow: @@ -246,32 +246,32 @@ 0 0 0 1px rgba(255, 255, 255, 0.1); } -.conversation-diagnostics__category-dropdown--dark.ant-select-dropdown .ant-select-item { +.conversation-diagnostics__category-dropdown--side-panel.ant-select-dropdown .ant-select-item { background: $backgroundTertiary !important; color: $textColourPrimary !important; } -.conversation-diagnostics__category-dropdown--dark.ant-select-dropdown .ant-select-item-option-content { +.conversation-diagnostics__category-dropdown--side-panel.ant-select-dropdown .ant-select-item-option-content { color: inherit !important; } -.conversation-diagnostics__category-dropdown--dark.ant-select-dropdown .ant-select-item-option-active:not(.ant-select-item-option-disabled) { - background: rgba(255, 255, 255, 0.08) !important; +.conversation-diagnostics__category-dropdown--side-panel.ant-select-dropdown .ant-select-item-option-active:not(.ant-select-item-option-disabled) { + background: $highlightColourPrimary !important; color: white !important; } -.conversation-diagnostics__category-dropdown--dark.ant-select-dropdown .ant-select-item-option-selected:not(.ant-select-item-option-disabled) { +.conversation-diagnostics__category-dropdown--side-panel.ant-select-dropdown .ant-select-item-option-selected:not(.ant-select-item-option-disabled) { border-left: 3px solid $linkColourPrimary; - background: rgba(255, 255, 255, 0.04) !important; + background: rgba(42, 176, 247, 0.14) !important; color: white !important; } -.conversation-diagnostics__category-dropdown--dark.ant-select-dropdown .ant-select-item-option-selected:not(.ant-select-item-option-disabled).ant-select-item-option-active { - background: rgba(255, 255, 255, 0.1) !important; +.conversation-diagnostics__category-dropdown--side-panel.ant-select-dropdown .ant-select-item-option-selected:not(.ant-select-item-option-disabled).ant-select-item-option-active { + background: $highlightColourPrimary !important; } -.conversation-diagnostics__category-dropdown--dark.ant-select-dropdown .ant-select-item-option-selected:not(.ant-select-item-option-disabled) .ant-select-item-option-state { - color: $linkColourPrimary !important; +.conversation-diagnostics__category-dropdown--side-panel.ant-select-dropdown .ant-select-item-option-selected:not(.ant-select-item-option-disabled) .ant-select-item-option-state { + color: white !important; } .conversation-diagnostics__list { @@ -619,8 +619,8 @@ } .conversation-diagnostics--side-panel .conversation-diagnostics__toolbar-controls .ant-input-affix-wrapper, -.conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select.ant-select-outlined:not(.ant-select-customize-input), -.conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select:not(.ant-select-customize-input) .ant-select-selector, +.conversation-diagnostics__category-select--side-panel.ant-select.ant-select-outlined:not(.ant-select-customize-input), +.conversation-diagnostics__category-select--side-panel.ant-select:not(.ant-select-customize-input) .ant-select-selector, .conversation-diagnostics--side-panel .conversation-diagnostics__toolbar-controls .ant-btn { background: $backgroundTertiary; border-color: $borderTertiary; @@ -628,19 +628,24 @@ } .conversation-diagnostics--side-panel .conversation-diagnostics__toolbar-controls .ant-input-affix-wrapper .ant-input, -.conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select .ant-select-selection-item, -.conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select .ant-select-selection-placeholder, -.conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select .ant-select-selection-search-input, -.conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select.ant-select-single .ant-select-selector .ant-select-selection-item, -.conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select.ant-select-single .ant-select-selector .ant-select-selection-placeholder, -.conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select:not(.ant-select-customize-input).ant-select-single .ant-select-selector .ant-select-selection-search-input { +.conversation-diagnostics__category-select--side-panel.ant-select .ant-select-selector, +.conversation-diagnostics__category-select--side-panel.ant-select .ant-select-selection-wrap, +.conversation-diagnostics__category-select--side-panel.ant-select .ant-select-selection-item, +.conversation-diagnostics__category-select--side-panel.ant-select .ant-select-selection-placeholder, +.conversation-diagnostics__category-select--side-panel.ant-select .ant-select-selection-search, +.conversation-diagnostics__category-select--side-panel.ant-select .ant-select-selection-search-input, +.conversation-diagnostics__category-select--side-panel.ant-select.ant-select-single .ant-select-selector .ant-select-selection-item, +.conversation-diagnostics__category-select--side-panel.ant-select.ant-select-single .ant-select-selector .ant-select-selection-placeholder, +.conversation-diagnostics__category-select--side-panel.ant-select:not(.ant-select-customize-input).ant-select-single .ant-select-selector .ant-select-selection-search-input { color: $textColourPrimary !important; -webkit-text-fill-color: $textColourPrimary !important; + opacity: 1 !important; } -.conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select-open .ant-select-selector .ant-select-selection-item, -.conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select-focused .ant-select-selector .ant-select-selection-item { +.conversation-diagnostics__category-select--side-panel.ant-select-open .ant-select-selector .ant-select-selection-item, +.conversation-diagnostics__category-select--side-panel.ant-select-focused .ant-select-selector .ant-select-selection-item { color: $textColourPrimary !important; + opacity: 1 !important; } .conversation-diagnostics--side-panel .conversation-diagnostics__toolbar-controls .ant-input-affix-wrapper .ant-input::placeholder { @@ -649,7 +654,7 @@ .conversation-diagnostics--side-panel .conversation-diagnostics__toolbar-controls .ant-input-prefix, .conversation-diagnostics--side-panel .conversation-diagnostics__toolbar-controls .ant-input-clear-icon, -.conversation-diagnostics--side-panel .conversation-diagnostics__category-select.ant-select .ant-select-arrow { +.conversation-diagnostics__category-select--side-panel.ant-select .ant-select-arrow { color: $blue4; } diff --git a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx index 654682d..2138a0e 100644 --- a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx +++ b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx @@ -229,12 +229,15 @@ function ConversationDiagnosticsContent({ globalModalId, variant = 'modal' }: Pr onChange={(event) => setSearchText(event.target.value)} /> - className="conversation-diagnostics__category-select" + className={classnames('conversation-diagnostics__category-select', { + 'conversation-diagnostics__category-select--side-panel': variant === 'side-panel', + })} classNames={{ popup: { - root: classnames('conversation-diagnostics__category-dropdown', { - 'conversation-diagnostics__category-dropdown--dark': variant === 'side-panel', - }), + root: + variant === 'side-panel' + ? 'conversation-diagnostics__category-dropdown--side-panel' + : 'conversation-diagnostics__category-dropdown', }, }} value={categoryFilter} From 47e1711ef373964e6bf1245d5296961ce60e59a3 Mon Sep 17 00:00:00 2001 From: Richard Griffiths Date: Sun, 3 May 2026 00:00:07 +0200 Subject: [PATCH 25/29] Fixed style issue --- .../ConversationDiagnosticsModal.css | 56 ++++++------------- .../ConversationDiagnosticsModal.tsx | 5 +- 2 files changed, 19 insertions(+), 42 deletions(-) diff --git a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css index a57fe73..280d90e 100644 --- a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css +++ b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css @@ -238,42 +238,6 @@ color: $linkColourTertiary; } -.conversation-diagnostics__category-dropdown--side-panel.ant-select-dropdown { - background: $backgroundTertiary !important; - color: $textColourPrimary !important; - box-shadow: - 0 8px 24px rgba(0, 0, 0, 0.35), - 0 0 0 1px rgba(255, 255, 255, 0.1); -} - -.conversation-diagnostics__category-dropdown--side-panel.ant-select-dropdown .ant-select-item { - background: $backgroundTertiary !important; - color: $textColourPrimary !important; -} - -.conversation-diagnostics__category-dropdown--side-panel.ant-select-dropdown .ant-select-item-option-content { - color: inherit !important; -} - -.conversation-diagnostics__category-dropdown--side-panel.ant-select-dropdown .ant-select-item-option-active:not(.ant-select-item-option-disabled) { - background: $highlightColourPrimary !important; - color: white !important; -} - -.conversation-diagnostics__category-dropdown--side-panel.ant-select-dropdown .ant-select-item-option-selected:not(.ant-select-item-option-disabled) { - border-left: 3px solid $linkColourPrimary; - background: rgba(42, 176, 247, 0.14) !important; - color: white !important; -} - -.conversation-diagnostics__category-dropdown--side-panel.ant-select-dropdown .ant-select-item-option-selected:not(.ant-select-item-option-disabled).ant-select-item-option-active { - background: $highlightColourPrimary !important; -} - -.conversation-diagnostics__category-dropdown--side-panel.ant-select-dropdown .ant-select-item-option-selected:not(.ant-select-item-option-disabled) .ant-select-item-option-state { - color: white !important; -} - .conversation-diagnostics__list { display: flex; flex-direction: column; @@ -620,7 +584,6 @@ .conversation-diagnostics--side-panel .conversation-diagnostics__toolbar-controls .ant-input-affix-wrapper, .conversation-diagnostics__category-select--side-panel.ant-select.ant-select-outlined:not(.ant-select-customize-input), -.conversation-diagnostics__category-select--side-panel.ant-select:not(.ant-select-customize-input) .ant-select-selector, .conversation-diagnostics--side-panel .conversation-diagnostics__toolbar-controls .ant-btn { background: $backgroundTertiary; border-color: $borderTertiary; @@ -628,6 +591,9 @@ } .conversation-diagnostics--side-panel .conversation-diagnostics__toolbar-controls .ant-input-affix-wrapper .ant-input, +.conversation-diagnostics__category-select--side-panel.ant-select .ant-select-content, +.conversation-diagnostics__category-select--side-panel.ant-select .ant-select-content-has-value, +.conversation-diagnostics__category-select--side-panel.ant-select .ant-select-input, .conversation-diagnostics__category-select--side-panel.ant-select .ant-select-selector, .conversation-diagnostics__category-select--side-panel.ant-select .ant-select-selection-wrap, .conversation-diagnostics__category-select--side-panel.ant-select .ant-select-selection-item, @@ -642,6 +608,20 @@ opacity: 1 !important; } +.conversation-diagnostics__category-select--side-panel.ant-select-open, +.conversation-diagnostics__category-select--side-panel.ant-select-focused { + color: $textColourPrimary !important; +} + +.conversation-diagnostics__category-select--side-panel.ant-select-open .ant-select-content, +.conversation-diagnostics__category-select--side-panel.ant-select-focused .ant-select-content, +.conversation-diagnostics__category-select--side-panel.ant-select-open .ant-select-content-has-value, +.conversation-diagnostics__category-select--side-panel.ant-select-focused .ant-select-content-has-value { + color: $textColourPrimary !important; + -webkit-text-fill-color: $textColourPrimary !important; + opacity: 1 !important; +} + .conversation-diagnostics__category-select--side-panel.ant-select-open .ant-select-selector .ant-select-selection-item, .conversation-diagnostics__category-select--side-panel.ant-select-focused .ant-select-selector .ant-select-selection-item { color: $textColourPrimary !important; @@ -655,7 +635,7 @@ .conversation-diagnostics--side-panel .conversation-diagnostics__toolbar-controls .ant-input-prefix, .conversation-diagnostics--side-panel .conversation-diagnostics__toolbar-controls .ant-input-clear-icon, .conversation-diagnostics__category-select--side-panel.ant-select .ant-select-arrow { - color: $blue4; + color: $highlightColourSecondary; } .conversation-diagnostics--side-panel .conversation-diagnostics__item { diff --git a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx index 2138a0e..00093d6 100644 --- a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx +++ b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx @@ -234,10 +234,7 @@ function ConversationDiagnosticsContent({ globalModalId, variant = 'modal' }: Pr })} classNames={{ popup: { - root: - variant === 'side-panel' - ? 'conversation-diagnostics__category-dropdown--side-panel' - : 'conversation-diagnostics__category-dropdown', + root: variant === 'side-panel' ? undefined : 'conversation-diagnostics__category-dropdown', }, }} value={categoryFilter} From c20f00b9abcb38a784780d7619afb55a38990260 Mon Sep 17 00:00:00 2001 From: Richard Griffiths Date: Sun, 3 May 2026 00:02:23 +0200 Subject: [PATCH 26/29] Tweak --- .../ConversationGeneral.css | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/app/src/containers/ConversationGeneral/ConversationGeneral.css b/app/src/containers/ConversationGeneral/ConversationGeneral.css index 788e2b3..77c4061 100644 --- a/app/src/containers/ConversationGeneral/ConversationGeneral.css +++ b/app/src/containers/ConversationGeneral/ConversationGeneral.css @@ -3,6 +3,7 @@ .conversation-general.ant-card { background-color: $backgroundPrimary; box-sizing: border-box; + container-type: inline-size; height: 100%; &.ant-card-bordered { @@ -22,6 +23,7 @@ color: $textColourQuaternary; line-height: 32px; text-align: right; + white-space: nowrap; } &__value { @@ -54,6 +56,8 @@ justify-content: flex-end; align-items: center; height: 32px; + min-width: 0; + white-space: nowrap; .anticon { flex: 0 0 auto; @@ -72,3 +76,45 @@ } } } + +.conversation-general .ant-card-body > .ant-row { + margin-left: 0 !important; + margin-right: 0 !important; +} + +.conversation-general .ant-card-body > .ant-row > .ant-col { + min-width: 0; + padding-left: $smallSpace !important; + padding-right: $smallSpace !important; +} + +@container (max-width: 28rem) { + .conversation-general .ant-card-body > .ant-row > .ant-col { + flex: 0 0 100% !important; + max-width: 100% !important; + } + + .conversation-general__label { + line-height: $lineHeightNormal; + min-height: 24px; + text-align: left; + } + + .conversation-general__speaker-group-label { + justify-content: flex-start; + height: auto; + min-height: 24px; + } + + .conversation-general__speaker-group { + flex-wrap: wrap; + } + + .conversation-general__speaker-select { + flex: 0 0 115px; + } + + .conversation-general__speaker-group .ant-input { + flex: 1 1 12rem; + } +} From 1f8b2ee6de51177431513d607b02b88145fdbf04 Mon Sep 17 00:00:00 2001 From: Richard Griffiths Date: Sun, 3 May 2026 00:08:52 +0200 Subject: [PATCH 27/29] Style tweaks --- .../ConversationDiagnosticsModal.css | 11 ++-- .../ConversationDiagnosticsModal.tsx | 50 +++++++++---------- .../ConversationEditor/ConversationEditor.css | 2 +- 3 files changed, 34 insertions(+), 29 deletions(-) diff --git a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css index 280d90e..826ce2f 100644 --- a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css +++ b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.css @@ -633,9 +633,14 @@ } .conversation-diagnostics--side-panel .conversation-diagnostics__toolbar-controls .ant-input-prefix, -.conversation-diagnostics--side-panel .conversation-diagnostics__toolbar-controls .ant-input-clear-icon, -.conversation-diagnostics__category-select--side-panel.ant-select .ant-select-arrow { - color: $highlightColourSecondary; +.conversation-diagnostics--side-panel .conversation-diagnostics__toolbar-controls .ant-input-clear-icon { + color: $linkColourPrimary; +} + +.conversation-diagnostics__category-select--side-panel.ant-select .ant-select-suffix, +.conversation-diagnostics__category-select--side-panel.ant-select .ant-select-suffix .anticon, +.conversation-diagnostics__category-select--side-panel.ant-select .ant-select-suffix svg { + color: $highlightColourSecondary !important; } .conversation-diagnostics--side-panel .conversation-diagnostics__item { diff --git a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx index 00093d6..42cbfd8 100644 --- a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx +++ b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx @@ -184,33 +184,33 @@ function ConversationDiagnosticsContent({ globalModalId, variant = 'modal' }: Pr 'conversation-diagnostics--side-panel': variant === 'side-panel', })} > -
- -
-
- {diagnostics.length > 0 - ? `${diagnostics.length} diagnostic${diagnostics.length === 1 ? '' : 's'} found` - : 'No diagnostics found'} + {variant !== 'side-panel' && ( +
+ -
- {diagnostics.length > 0 - ? `${getSummaryCountsText(errorCount, warningCount, infoCount)}.` - : 'The active conversation passed the current content, graph, operation, and reference checks.'} +
+
+ {diagnostics.length > 0 + ? `${diagnostics.length} diagnostic${diagnostics.length === 1 ? '' : 's'} found` + : 'No diagnostics found'} +
+
+ {diagnostics.length > 0 + ? `${getSummaryCountsText(errorCount, warningCount, infoCount)}.` + : 'The active conversation passed the current content, graph, operation, and reference checks.'} +
-
- -
+ +
+ )} {diagnostics.length > 0 && (
diff --git a/app/src/containers/ConversationEditor/ConversationEditor.css b/app/src/containers/ConversationEditor/ConversationEditor.css index 99a12f7..4758056 100644 --- a/app/src/containers/ConversationEditor/ConversationEditor.css +++ b/app/src/containers/ConversationEditor/ConversationEditor.css @@ -29,7 +29,7 @@ &__toolbar { display: flex; justify-content: space-between; - margin: 0 $smallSpace $tinySpace $smallSpace; + margin: $tinySpace $smallSpace; } &__regenerate-ids-button { From 098dae0c644ddb2e08f7cca0f090e1a68143d9f2 Mon Sep 17 00:00:00 2001 From: Richard Griffiths Date: Sun, 3 May 2026 00:21:51 +0200 Subject: [PATCH 28/29] Tweaks --- .../ConversationEditor/ConversationEditor.tsx | 8 ++++ .../conversation-diagnostics-utils.test.ts | 42 +++++++++++++----- .../utils/conversation-diagnostics-utils.ts | 43 ++++++++++--------- 3 files changed, 62 insertions(+), 31 deletions(-) diff --git a/app/src/containers/ConversationEditor/ConversationEditor.tsx b/app/src/containers/ConversationEditor/ConversationEditor.tsx index accc0eb..0fbbe5d 100644 --- a/app/src/containers/ConversationEditor/ConversationEditor.tsx +++ b/app/src/containers/ConversationEditor/ConversationEditor.tsx @@ -71,6 +71,7 @@ function ConversationEditor({ conversationAsset }: Props) { const { unsavedActiveConversationAsset } = dataStore; const { activeNode, rebuild } = nodeStore; + const activeConversationId = conversationAsset.conversation.idRef.id; const createNewUnsavedConversation = () => { const unsavedConversationAsset = { ...toJS(conversationAsset) }; @@ -138,6 +139,13 @@ function ConversationEditor({ conversationAsset }: Props) { createNewUnsavedConversation(); }, [conversationAsset]); + useEffect(() => { + if (!sidePanelStore.isPanelVisible(diagnosticsSidePanelId)) return; + if ((windowSize.width ?? 0) < 1280) return; + + sidePanelStore.setPanelContent(ConversationDiagnosticsPanel, {}, 'Conversation Diagnostics', true, diagnosticsSidePanelId); + }, [activeConversationId, sidePanelStore, windowSize.width]); + useEffect(() => { if (windowSize.width != null && windowSize.width < 1280) sidePanelStore.closePanel(); }, [sidePanelStore, windowSize.width]); diff --git a/app/src/utils/__tests__/conversation-diagnostics-utils.test.ts b/app/src/utils/__tests__/conversation-diagnostics-utils.test.ts index fd636c8..3612126 100644 --- a/app/src/utils/__tests__/conversation-diagnostics-utils.test.ts +++ b/app/src/utils/__tests__/conversation-diagnostics-utils.test.ts @@ -97,7 +97,7 @@ describe('conversation diagnostics', () => { ); }); - it('allows empty prompt nodes that continue into response choices', () => { + it('ignores empty prompt nodes that continue into response choices', () => { const conversationAsset = makeBasicConversation(); const prompt = conversationAsset.conversation.nodes[0]; const commanderResponse = createResponseNode(); @@ -109,21 +109,16 @@ describe('conversation diagnostics', () => { const diagnostics = buildConversationDiagnostics({ conversationAsset, operationDefinitions: [] }); const promptDiagnostics = diagnostics.filter((diagnostic) => diagnostic.nodeId === getId(prompt)); - expect(promptDiagnostics).toEqual( - expect.arrayContaining([expect.objectContaining({ severity: 'info', title: 'Prompt node has no text' })]), - ); - expect(promptDiagnostics.some((diagnostic) => diagnostic.severity === 'warning' && diagnostic.title.startsWith('Prompt node'))).toBe(false); + expect(promptDiagnostics.some((diagnostic) => diagnostic.title.startsWith('Prompt node'))).toBe(false); }); it('shows prompt labels as one-based display numbers', () => { const conversationAsset = makeBasicConversation(); const root = conversationAsset.conversation.roots[0]; const secondPrompt = createPromptNode(1); - const commanderResponse = createResponseNode(); - commanderResponse.responseText = 'Keep going.'; - commanderResponse.nextNodeIndex = -1; secondPrompt.text = ''; - secondPrompt.branches = [commanderResponse]; + secondPrompt.branches = []; + secondPrompt.actions = null; root.nextNodeIndex = 1; conversationAsset.conversation.nodes.push(secondPrompt); @@ -131,7 +126,7 @@ describe('conversation diagnostics', () => { expect(diagnostics).toEqual( expect.arrayContaining([ - expect.objectContaining({ severity: 'info', title: 'Prompt node has no text', nodeId: getId(secondPrompt), nodeLabel: 'Prompt #2' }), + expect.objectContaining({ severity: 'warning', title: 'Prompt node has no content or responses', nodeId: getId(secondPrompt), nodeLabel: 'Prompt #2' }), ]), ); }); @@ -228,6 +223,33 @@ describe('conversation diagnostics', () => { ); }); + it('reports an empty BattleTech viewscreen image key as info instead of a warning', () => { + const conversationAsset = makeBasicConversation(); + const prompt = conversationAsset.conversation.nodes[0]; + prompt.actions = { + ops: [makeAction('Set BattleTech Viewscreen Image', [makeStringArg('')])], + }; + + const diagnostics = buildConversationDiagnostics({ + conversationAsset, + operationDefinitions: [makeDefinition('Set BattleTech Viewscreen Image', [{ label: 'Key', types: ['string'] }])], + }); + + expect(diagnostics).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + severity: 'info', + title: 'Operation input is empty', + description: expect.stringContaining('clearing the conversation viewscreen'), + nodeId: getId(prompt), + }), + ]), + ); + expect(diagnostics).not.toEqual( + expect.arrayContaining([expect.objectContaining({ severity: 'warning', title: 'Operation input is empty', nodeId: getId(prompt) })]), + ); + }); + it('uses the runtime prompt node position for link target checks', () => { const conversationAsset = makeBasicConversation(); const root = conversationAsset.conversation.roots[0]; diff --git a/app/src/utils/conversation-diagnostics-utils.ts b/app/src/utils/conversation-diagnostics-utils.ts index c9e15bc..6b0bfc1 100644 --- a/app/src/utils/conversation-diagnostics-utils.ts +++ b/app/src/utils/conversation-diagnostics-utils.ts @@ -31,6 +31,7 @@ type OperationContext = NodeContext & { }; const sideloadConversationOperation = 'Sideload Conversation'; +const setBattleTechViewscreenImageOperation = 'Set BattleTech Viewscreen Image'; export function buildConversationDiagnostics(options: ConversationDiagnosticsOptions): ConversationDiagnostic[] { const { conversationAsset, operationDefinitions, loadedConversationAssets = [] } = options; @@ -111,23 +112,15 @@ function scanConversationShape( nodes.forEach((node) => { const context = getPromptNodeContext(node); - if (node.text.trim() === '') { + if (node.text.trim() === '' && isEmptyPromptLikelyInert(node)) { diagnostics.push( - isEmptyPromptLikelyInert(node) - ? createDiagnostic({ - severity: 'warning', - category: 'content', - title: 'Prompt node has no content or responses', - description: 'This prompt has no speaker text, no response options, and no actions. Check that the player will not be left on stale or missing dialogue text.', - context, - }) - : createDiagnostic({ - severity: 'info', - category: 'content', - title: 'Prompt node has no text', - description: 'This prompt has no speaker text, but it has response choices or actions. This is valid for continuation routing, chained commander response runs, and other intentional silent prompt nodes.', - context, - }), + createDiagnostic({ + severity: 'warning', + category: 'content', + title: 'Prompt node has no content or responses', + description: 'This prompt has no speaker text, no response options, and no actions. Check that the player will not be left on stale or missing dialogue text.', + context, + }), ); } @@ -349,10 +342,7 @@ function scanOperationArg( severity, category: 'operation', title: 'Operation input is empty', - description: - severity === 'info' - ? `${inputPath} is empty. This is valid, but check that the missing value is intentional.` - : `${inputPath} is empty. This may fail or do nothing at runtime.`, + description: getEmptyRequiredArgDescription(operation.functionName, inputPath, severity), context, }), ); @@ -485,7 +475,18 @@ function isInputOptional(functionName: string, input: InputType): boolean { } function isInformationalEmptyArg(functionName: string, input: InputType): boolean { - return functionName === 'Start Conversation Custom' && input.label.toLowerCase() === 'conversation sub header'; + if (functionName === 'Start Conversation Custom' && input.label.toLowerCase() === 'conversation sub header') return true; + if (functionName === setBattleTechViewscreenImageOperation && input.label.toLowerCase() === 'key') return true; + return false; +} + +function getEmptyRequiredArgDescription(functionName: string, inputPath: string, severity: ConversationDiagnosticSeverity): string { + if (functionName === setBattleTechViewscreenImageOperation) { + return `${inputPath} is empty. BattleTech treats this as clearing the conversation viewscreen, so check that the reset is intentional.`; + } + + if (severity === 'info') return `${inputPath} is empty. This is valid, but check that the missing value is intentional.`; + return `${inputPath} is empty. This may fail or do nothing at runtime.`; } function buildLoadedConversationsById(conversationAssets: ConversationAssetType[]): Map { From 68b20eacc52475bf0f7f6893cda412c4238bc150 Mon Sep 17 00:00:00 2001 From: Richard Griffiths Date: Sun, 3 May 2026 00:33:56 +0200 Subject: [PATCH 29/29] Fixed --- .../ConversationDiagnosticsModal.tsx | 8 ++- .../conversation-diagnostics-utils.test.ts | 68 ++++++++++++++++++ .../utils/conversation-diagnostics-utils.ts | 71 +++++++++++++++++++ 3 files changed, 145 insertions(+), 2 deletions(-) diff --git a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx index 42cbfd8..a13d9ef 100644 --- a/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx +++ b/app/src/components/ConversationDiagnosticsModal/ConversationDiagnosticsModal.tsx @@ -143,7 +143,7 @@ function ConversationDiagnosticsContent({ globalModalId, variant = 'modal' }: Pr ...diagnostics.flatMap((diagnostic, index) => [ `${index + 1}. [${getSeverityLabel(diagnostic.severity)}] ${diagnostic.title}`, ` Area: ${getCategoryLabel(diagnostic.category)}`, - ` Node: ${diagnostic.nodeLabel}`, + ` ${getDiagnosticSubjectLabel(diagnostic)}: ${diagnostic.nodeLabel}`, ` Detail: ${diagnostic.description}`, '', ]), @@ -297,7 +297,7 @@ function ConversationDiagnosticsContent({ globalModalId, variant = 'modal' }: Pr