flare-chat-core 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +28 -0
  2. package/docs/CAPABILITY-INVENTORY.md +42 -0
  3. package/docs/CHAT-CORE-BOUNDARY.md +47 -0
  4. package/docs/CORE-APP-REALIGNMENT-WORKLOAD-2026-04-18.md +86 -0
  5. package/docs/SSOT-CHAT-CORE-BOUNDARY.md +73 -0
  6. package/docs/SSOT-CHAT-CORE-DATAFLOW.md +97 -0
  7. package/index.html +12 -0
  8. package/package.json +24 -2
  9. package/src/adapters/index.js +6 -0
  10. package/src/adapters/message-api.adapter.js +59 -0
  11. package/src/adapters/session-api.adapter.js +133 -0
  12. package/src/adapters/session-message-api.http.js +161 -0
  13. package/src/adapters/session-message-api.js +34 -0
  14. package/src/adapters/session-message-api.normalize-source-record.test.mjs +180 -0
  15. package/src/adapters/session-message-api.normalizers.js +153 -0
  16. package/src/adapters/source-api.adapter.js +135 -0
  17. package/src/adapters/sse-client.js +244 -0
  18. package/src/adapters/sse-event-dispatcher.js +121 -0
  19. package/src/app/App.jsx +11 -0
  20. package/src/app/AppProviders.jsx +12 -0
  21. package/src/app/ChatWorkspaceScreen.jsx +33 -0
  22. package/src/app/WorkspaceLayout.jsx +125 -0
  23. package/src/app/components/AppCanvasPanel.jsx +64 -0
  24. package/src/app/components/TriggerThresholdPopoverContent.jsx +122 -0
  25. package/src/app/components/WorkspaceBodySection.jsx +109 -0
  26. package/src/app/components/WorkspaceMainPane.jsx +113 -0
  27. package/src/app/components/WorkspaceSessionPane.jsx +48 -0
  28. package/src/app/components/WorkspaceTopBarSection.jsx +65 -0
  29. package/src/app/core-chat-entry/ComposerSectionNode.jsx +241 -0
  30. package/src/app/core-chat-entry/attachmentSendRefs.js +154 -0
  31. package/src/app/core-chat-entry/attachmentSendRefs.test.mjs +101 -0
  32. package/src/app/core-chat-entry/composerActionRouter.js +26 -0
  33. package/src/app/core-chat-entry/constants.js +108 -0
  34. package/src/app/core-chat-entry/selectors.js +28 -0
  35. package/src/app/core-chat-entry/useAppActionErrorGuards.js +68 -0
  36. package/src/app/core-chat-entry/useChatCorePipelines.js +110 -0
  37. package/src/app/core-chat-entry/useComposerModeSuggestion.js +89 -0
  38. package/src/app/core-chat-entry/useDevCapabilityStatusNote.js +22 -0
  39. package/src/app/core-chat-entry/useProjectNameEditing.js +41 -0
  40. package/src/app/core-chat-entry/useProjectSourceUpload.js +341 -0
  41. package/src/app/core-chat-entry/useRealApiReadinessGate.js +103 -0
  42. package/src/app/core-chat-entry/useUnavailableActionError.js +29 -0
  43. package/src/app/core-chat-entry/useWorkspaceCanvasController.jsx +177 -0
  44. package/src/app/core-chat-entry/useWorkspaceCanvasProjection.jsx +171 -0
  45. package/src/app/core-chat-entry/useWorkspaceComposerController.jsx +199 -0
  46. package/src/app/core-chat-entry/useWorkspaceController.jsx +226 -0
  47. package/src/app/core-chat-entry/useWorkspacePanels.js +55 -0
  48. package/src/app/hooks/useComposerAttachmentSync.js +223 -0
  49. package/src/app/hooks/useComposerChooserHandlers.js +52 -0
  50. package/src/app/hooks/useSendWithContextRefs.js +140 -0
  51. package/src/app/hooks/useSendWithContextRefs.test.mjs +29 -0
  52. package/src/app/hooks/useUserThresholdProfile.js +121 -0
  53. package/src/app/index.js +1 -0
  54. package/src/app/selectors/assistantTextSelector.js +73 -0
  55. package/src/app/selectors/canvasEvidenceSummarySelector.js +28 -0
  56. package/src/app/selectors/canvasReportTemplateSelector.js +28 -0
  57. package/src/app/selectors/canvasTabsSelector.js +58 -0
  58. package/src/app/selectors/evidenceProjectionSelector.js +175 -0
  59. package/src/app/selectors/evidenceProjectionSelector.test.mjs +107 -0
  60. package/src/app/selectors/modeSuggestionSelector.js +50 -0
  61. package/src/chat-core/app/mockRuntime.js +291 -0
  62. package/src/chat-core/app/useAppStream.js +187 -0
  63. package/src/chat-core/app/useAppStream.refs.test.mjs +44 -0
  64. package/src/chat-core/app/useAppStream.request-body.test.mjs +116 -0
  65. package/src/chat-core/app/useCoreChatApp.js +115 -0
  66. package/src/chat-core/facade/useBasicConversationFacade.js +280 -0
  67. package/src/chat-core/index.js +9 -1
  68. package/src/chat-core/messages/buildTimelineItems.analysis-route.test.mjs +36 -0
  69. package/src/chat-core/messages/buildTimelineItems.js +172 -11
  70. package/src/chat-core/messages/buildTimelineItems.knowledge-citation.test.mjs +183 -0
  71. package/src/chat-core/messages/contextUsageDefaults.js +3 -0
  72. package/src/chat-core/messages/contextUsageViewModel.js +147 -0
  73. package/src/chat-core/messages/contextUsageViewModel.test.mjs +74 -0
  74. package/src/chat-core/messages/useContextUsageViewModel.js +41 -0
  75. package/src/chat-core/orchestration/useBasicSendHandler.js +55 -0
  76. package/src/chat-core/pipelines/build-action-request.js +46 -0
  77. package/src/chat-core/pipelines/build-stream-request.js +74 -0
  78. package/src/chat-core/pipelines/entity-extraction.js +159 -0
  79. package/src/chat-core/pipelines/preprocess-message.js +16 -0
  80. package/src/chat-core/pipelines/stream-persist-utils.js +32 -0
  81. package/src/chat-core/pipelines/transport/send-mock-stream.js +86 -0
  82. package/src/chat-core/pipelines/transport/send-real-stream.js +330 -0
  83. package/src/chat-core/pipelines/transport/send-real-stream.test.mjs +27 -0
  84. package/src/chat-core/pipelines/transport/send-sourcing-search.js +86 -0
  85. package/src/chat-core/pipelines/transport/send-sourcing-search.test.mjs +14 -0
  86. package/src/chat-core/pipelines/transport/sourcing-response-templates.js +55 -0
  87. package/src/chat-core/pipelines/transport/sourcing-search-api.js +155 -0
  88. package/src/chat-core/runtime/runtimeMode.js +69 -0
  89. package/src/chat-core/session/chatSessionActionTypes.js +24 -0
  90. package/src/chat-core/session/chatSessionReducer.js +352 -0
  91. package/src/chat-core/session/chatSessionReducer.streaming-done.test.mjs +39 -0
  92. package/src/chat-core/session/index.js +2 -0
  93. package/src/chat-core/session/sessionActionsMessages.js +44 -0
  94. package/src/chat-core/session/sessionActionsSessionCrud.js +131 -0
  95. package/src/chat-core/session/sessionActionsStreaming.js +80 -0
  96. package/src/chat-core/session/sessionActionsUiState.js +51 -0
  97. package/src/chat-core/session/useChatSessionReducer.js +67 -390
  98. package/src/chat-core/session/useSessionListController.js +67 -0
  99. package/src/chat-core/stream/sse-client.js +1 -142
  100. package/src/chat-core/stream/sse-event-dispatcher.js +1 -0
  101. package/src/chat-core/stream/sse-events.js +1 -598
  102. package/src/chat-core/stream/useSSEStream.js +1 -273
  103. package/src/chat-core/stream/useStreamSendController.js +46 -0
  104. package/src/contracts/context-ssot.js +47 -0
  105. package/src/contracts/index.js +1 -0
  106. package/src/contracts/sse-events/base-parsers.js +79 -0
  107. package/src/contracts/sse-events/domain-parsers.js +3 -0
  108. package/src/contracts/sse-events/internal-normalizers.js +143 -0
  109. package/src/contracts/sse-events/parsers-intake.js +235 -0
  110. package/src/contracts/sse-events/parsers-runtime.js +37 -0
  111. package/src/contracts/sse-events/parsers-sourcing.js +179 -0
  112. package/src/contracts/sse-events/patch-event-parser.js +121 -0
  113. package/src/contracts/sse-events/runtime-parsers.js +79 -0
  114. package/src/contracts/sse-events.js +4 -0
  115. package/src/index.js +5 -0
  116. package/src/main.jsx +28 -0
  117. package/src/orchestration/index.js +6 -0
  118. package/src/orchestration/useSSEStream.js +221 -0
  119. package/src/state/index.js +4 -0
  120. package/vite.config.js +36 -0
@@ -0,0 +1,177 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { createDefaultGenerativeRegistry } from 'flare-generative-ui';
3
+ import { parseSourcingCandidates } from '../../contracts/sse-events.js';
4
+ import { requestSourcingSearchApi } from '../../chat-core/pipelines/transport/sourcing-search-api.js';
5
+ import { useWorkspaceCanvasProjection } from './useWorkspaceCanvasProjection.jsx';
6
+
7
+ export function useWorkspaceCanvasController({
8
+ apiBaseUrl,
9
+ projectId,
10
+ userId,
11
+ activeSessionId,
12
+ timeline,
13
+ composerModeKey,
14
+ evidenceStrictMode,
15
+ evidenceHitScoreThreshold,
16
+ showCanvasPanel,
17
+ setShowCanvasPanel,
18
+ canvasActiveTabKey,
19
+ setCanvasActiveTabKey,
20
+ canvasFullscreenOpen,
21
+ setCanvasFullscreenOpen,
22
+ inputState,
23
+ themeTokens,
24
+ languageGuardEnabled,
25
+ friendlyToneEnabled,
26
+ }) {
27
+ const [canvasFocusEvidenceAnchorId, setCanvasFocusEvidenceAnchorId] = useState('');
28
+ const [sourcingLoadedTopK, setSourcingLoadedTopK] = useState(8);
29
+ const [sourcingHasMore, setSourcingHasMore] = useState(false);
30
+ const [sourcingLoadingMore, setSourcingLoadingMore] = useState(false);
31
+ const [sourcingDisplayedResultIds, setSourcingDisplayedResultIds] = useState([]);
32
+ const sourcingMaxTopK = 100;
33
+ const hadSourcingEvidenceRef = useRef(false);
34
+
35
+ useEffect(() => {
36
+ hadSourcingEvidenceRef.current = false;
37
+ setSourcingHasMore(false);
38
+ setSourcingLoadedTopK(8);
39
+ setSourcingDisplayedResultIds([]);
40
+ }, [activeSessionId]);
41
+
42
+ const handleUICardAction = useCallback((action) => {
43
+ const actionType = String(action?.type || action?.action_key || '').trim();
44
+ if (actionType === 'open_evidence_citation') {
45
+ const citationId = String(action?.citation_id || '').trim();
46
+ setShowCanvasPanel(true);
47
+ setCanvasActiveTabKey('evidence');
48
+ setCanvasFocusEvidenceAnchorId(citationId ? `retrieval-evidence-${citationId}` : '');
49
+ return;
50
+ }
51
+ if (actionType.includes('canvas') || actionType.includes('evidence') || actionType.includes('sourcing')) {
52
+ setShowCanvasPanel(true);
53
+ setCanvasActiveTabKey(actionType.includes('sourcing') ? 'sourcing' : 'evidence');
54
+ }
55
+ }, [setCanvasActiveTabKey, setShowCanvasPanel]);
56
+
57
+ const generativeRegistry = useMemo(() => {
58
+ const registry = createDefaultGenerativeRegistry();
59
+ registry.register('knowledge_search', () => null);
60
+ registry.register('sourcing_candidates', () => null);
61
+ return registry;
62
+ }, []);
63
+
64
+ useEffect(() => {
65
+ if (!canvasFocusEvidenceAnchorId) {
66
+ return;
67
+ }
68
+ const timer = setTimeout(() => setCanvasFocusEvidenceAnchorId(''), 1600);
69
+ return () => clearTimeout(timer);
70
+ }, [canvasFocusEvidenceAnchorId]);
71
+
72
+ useEffect(() => {
73
+ const sourcingItem = [...(Array.isArray(timeline.items) ? timeline.items : [])]
74
+ .reverse()
75
+ .find((item) => String(item?.type || '').trim() === 'sourcing_candidates');
76
+ const payload = (
77
+ sourcingItem?.payload
78
+ && typeof sourcingItem.payload === 'object'
79
+ && !Array.isArray(sourcingItem.payload)
80
+ ) ? sourcingItem.payload : null;
81
+ if (!payload) {
82
+ setSourcingHasMore(false);
83
+ setSourcingLoadedTopK(8);
84
+ setSourcingDisplayedResultIds([]);
85
+ return;
86
+ }
87
+ const candidates = Array.isArray(payload.candidates) ? payload.candidates : [];
88
+ const nextIds = candidates
89
+ .map((item) => String(item?.result_id || item?.id || '').trim())
90
+ .filter(Boolean);
91
+ setSourcingDisplayedResultIds(nextIds);
92
+ setSourcingHasMore(payload.has_more === true);
93
+ if (Number.isFinite(Number(payload.requested_top_k))) {
94
+ setSourcingLoadedTopK(Number(payload.requested_top_k));
95
+ }
96
+ }, [timeline.items]);
97
+
98
+ const handleCanvasSourcingLoadMore = useCallback(async () => {
99
+ if (sourcingLoadingMore || !sourcingHasMore || sourcingLoadedTopK >= sourcingMaxTopK) {
100
+ return;
101
+ }
102
+ const sourcingItem = [...(Array.isArray(timeline.items) ? timeline.items : [])]
103
+ .reverse()
104
+ .find((item) => String(item?.type || '').trim() === 'sourcing_candidates');
105
+ const payload = (
106
+ sourcingItem?.payload
107
+ && typeof sourcingItem.payload === 'object'
108
+ && !Array.isArray(sourcingItem.payload)
109
+ ) ? sourcingItem.payload : null;
110
+ const query = String(payload?.query || '').trim();
111
+ const currentSessionId = String(activeSessionId || '').trim();
112
+ if (!query || !currentSessionId) {
113
+ return;
114
+ }
115
+ const nextTopK = Math.min(sourcingLoadedTopK + 8, sourcingMaxTopK);
116
+ setSourcingLoadingMore(true);
117
+ try {
118
+ const { sourcingPayload } = await requestSourcingSearchApi({
119
+ apiBaseUrl,
120
+ projectId,
121
+ userId,
122
+ sessionId: currentSessionId,
123
+ query,
124
+ topK: nextTopK,
125
+ appendMode: true,
126
+ lockExistingOrder: true,
127
+ baseResultIds: sourcingDisplayedResultIds,
128
+ });
129
+ timeline.onSourcingCandidates?.(parseSourcingCandidates(sourcingPayload));
130
+ setSourcingLoadedTopK(Number(sourcingPayload.requested_top_k) || nextTopK);
131
+ setSourcingHasMore(sourcingPayload.has_more === true);
132
+ } catch (_error) {
133
+ setSourcingHasMore(true);
134
+ } finally {
135
+ setSourcingLoadingMore(false);
136
+ }
137
+ }, [
138
+ activeSessionId,
139
+ apiBaseUrl,
140
+ projectId,
141
+ sourcingDisplayedResultIds,
142
+ sourcingHasMore,
143
+ sourcingLoadedTopK,
144
+ sourcingLoadingMore,
145
+ timeline,
146
+ userId,
147
+ ]);
148
+
149
+ const { canvasPanelNode, renderedTimelineItems } = useWorkspaceCanvasProjection({
150
+ timelineItems: timeline.items,
151
+ composerModeKey,
152
+ evidenceStrictMode,
153
+ evidenceHitScoreThreshold,
154
+ showCanvasPanel,
155
+ canvasFocusEvidenceAnchorId,
156
+ setCanvasActiveTabKey,
157
+ hadSourcingEvidenceRef,
158
+ setShowCanvasPanel,
159
+ canvasActiveTabKey,
160
+ canvasFullscreenOpen,
161
+ setCanvasFullscreenOpen,
162
+ inputState,
163
+ themeTokens,
164
+ languageGuardEnabled,
165
+ friendlyToneEnabled,
166
+ sourcingHasMore,
167
+ sourcingLoadingMore,
168
+ onCanvasSourcingLoadMore: handleCanvasSourcingLoadMore,
169
+ });
170
+
171
+ return {
172
+ canvasPanelNode,
173
+ renderedTimelineItems,
174
+ handleUICardAction,
175
+ generativeRegistry,
176
+ };
177
+ }
@@ -0,0 +1,171 @@
1
+ import React, { useEffect, useMemo } from 'react';
2
+ import { debugCoreLog, getCoreRuntimeMode } from '../../chat-core/runtime/runtimeMode.js';
3
+ import { RESOLVED_UI_LABELS, RESPONSE_STYLE_POLICY } from './constants.js';
4
+ import { selectProjectedCanvasEvidenceItems } from '../selectors/evidenceProjectionSelector.js';
5
+ import {
6
+ selectCanvasAutoTab,
7
+ selectCanvasEvidencePresence,
8
+ selectCanvasTabs,
9
+ selectResolvedCanvasEvidenceTabKey,
10
+ selectShouldAutoOpenCanvasForSourcing,
11
+ } from '../selectors/canvasTabsSelector.js';
12
+ import { selectCanvasEvidenceSummary } from '../selectors/canvasEvidenceSummarySelector.js';
13
+ import { selectRenderedTimelineItems } from '../selectors/assistantTextSelector.js';
14
+ import { selectCanvasReportTemplate } from '../selectors/canvasReportTemplateSelector.js';
15
+ import AppCanvasPanel from '../components/AppCanvasPanel.jsx';
16
+
17
+ export function useWorkspaceCanvasProjection({
18
+ timelineItems,
19
+ composerModeKey,
20
+ evidenceStrictMode,
21
+ evidenceHitScoreThreshold,
22
+ showCanvasPanel,
23
+ canvasFocusEvidenceAnchorId,
24
+ setCanvasActiveTabKey,
25
+ hadSourcingEvidenceRef,
26
+ setShowCanvasPanel,
27
+ canvasActiveTabKey,
28
+ canvasFullscreenOpen,
29
+ setCanvasFullscreenOpen,
30
+ inputState,
31
+ themeTokens,
32
+ languageGuardEnabled,
33
+ friendlyToneEnabled,
34
+ sourcingHasMore = false,
35
+ sourcingLoadingMore = false,
36
+ onCanvasSourcingLoadMore,
37
+ } = {}) {
38
+ const canvasEvidenceItems = useMemo(() => selectProjectedCanvasEvidenceItems({
39
+ timelineItems,
40
+ composerModeKey,
41
+ evidenceStrictMode,
42
+ evidenceHitScoreThreshold,
43
+ }), [composerModeKey, evidenceHitScoreThreshold, evidenceStrictMode, timelineItems]);
44
+
45
+ const canvasReportTemplate = useMemo(
46
+ () => selectCanvasReportTemplate(timelineItems),
47
+ [timelineItems],
48
+ );
49
+
50
+ useEffect(() => {
51
+ const runtimeMode = getCoreRuntimeMode();
52
+ debugCoreLog('canvas.projection', {
53
+ mode: runtimeMode.mode,
54
+ evidenceCount: canvasEvidenceItems.length,
55
+ hasReportTemplate: Boolean(canvasReportTemplate),
56
+ timelineTypes: (Array.isArray(timelineItems) ? timelineItems : [])
57
+ .map((item) => String(item?.type || '').trim())
58
+ .filter(Boolean),
59
+ });
60
+ }, [canvasEvidenceItems.length, canvasReportTemplate, timelineItems]);
61
+
62
+ useEffect(() => {
63
+ const nextAutoTab = selectCanvasAutoTab({
64
+ canvasEvidenceItems,
65
+ canvasFocusEvidenceAnchorId,
66
+ showCanvasPanel,
67
+ canvasActiveTabKey,
68
+ });
69
+ if (nextAutoTab && nextAutoTab !== canvasActiveTabKey) {
70
+ setCanvasActiveTabKey(nextAutoTab);
71
+ }
72
+ }, [
73
+ canvasActiveTabKey,
74
+ canvasEvidenceItems,
75
+ canvasFocusEvidenceAnchorId,
76
+ setCanvasActiveTabKey,
77
+ showCanvasPanel,
78
+ ]);
79
+
80
+ const { hasSourcingEvidenceRows, hasRetrievalEvidenceRows } = useMemo(
81
+ () => selectCanvasEvidencePresence(canvasEvidenceItems),
82
+ [canvasEvidenceItems],
83
+ );
84
+ const isPlanMode = String(composerModeKey || '').trim() === 'requirement_canvas';
85
+
86
+ useEffect(() => {
87
+ if (selectShouldAutoOpenCanvasForSourcing({
88
+ hasSourcingEvidenceRows,
89
+ hadSourcingEvidence: hadSourcingEvidenceRef?.current,
90
+ })) {
91
+ setShowCanvasPanel(true);
92
+ setCanvasActiveTabKey('sourcing');
93
+ }
94
+ if (hadSourcingEvidenceRef) {
95
+ hadSourcingEvidenceRef.current = hasSourcingEvidenceRows;
96
+ }
97
+ }, [hadSourcingEvidenceRef, hasSourcingEvidenceRows, setCanvasActiveTabKey, setShowCanvasPanel]);
98
+
99
+ const canvasTabs = useMemo(() => selectCanvasTabs({
100
+ isPlanMode,
101
+ canvasReportTemplate,
102
+ hasSourcingEvidenceRows,
103
+ hasRetrievalEvidenceRows,
104
+ resolvedUILabels: RESOLVED_UI_LABELS,
105
+ }), [canvasReportTemplate, hasRetrievalEvidenceRows, hasSourcingEvidenceRows, isPlanMode]);
106
+
107
+ const resolvedCanvasEvidenceTabKey = useMemo(() => selectResolvedCanvasEvidenceTabKey({
108
+ hasSourcingEvidenceRows,
109
+ canvasActiveTabKey,
110
+ }), [canvasActiveTabKey, hasSourcingEvidenceRows]);
111
+ const canvasEvidenceSummary = useMemo(
112
+ () => selectCanvasEvidenceSummary(canvasEvidenceItems),
113
+ [canvasEvidenceItems],
114
+ );
115
+
116
+ const canvasPanelNode = useMemo(() => (
117
+ <AppCanvasPanel
118
+ canvasActiveTabKey={canvasActiveTabKey}
119
+ showCanvasPanel={showCanvasPanel}
120
+ resolvedUILabels={RESOLVED_UI_LABELS}
121
+ canvasFullscreenOpen={canvasFullscreenOpen}
122
+ canvasTabs={canvasTabs}
123
+ resolvedCanvasEvidenceTabKey={resolvedCanvasEvidenceTabKey}
124
+ canvasEvidenceSummary={canvasEvidenceSummary}
125
+ canvasEvidenceItems={canvasEvidenceItems}
126
+ canvasFocusEvidenceAnchorId={canvasFocusEvidenceAnchorId}
127
+ canvasSourcingHasMore={sourcingHasMore}
128
+ canvasSourcingLoadingMore={sourcingLoadingMore}
129
+ onCanvasSourcingLoadMore={onCanvasSourcingLoadMore}
130
+ canvasReportTemplate={canvasReportTemplate}
131
+ inputState={inputState}
132
+ setCanvasActiveTabKey={setCanvasActiveTabKey}
133
+ setCanvasFullscreenOpen={setCanvasFullscreenOpen}
134
+ setShowCanvasPanel={setShowCanvasPanel}
135
+ themeTokens={themeTokens}
136
+ />
137
+ ), [
138
+ canvasActiveTabKey,
139
+ canvasEvidenceItems,
140
+ canvasEvidenceSummary,
141
+ canvasFocusEvidenceAnchorId,
142
+ canvasFullscreenOpen,
143
+ onCanvasSourcingLoadMore,
144
+ canvasReportTemplate,
145
+ sourcingHasMore,
146
+ sourcingLoadingMore,
147
+ canvasTabs,
148
+ inputState,
149
+ resolvedCanvasEvidenceTabKey,
150
+ setCanvasActiveTabKey,
151
+ setCanvasFullscreenOpen,
152
+ setShowCanvasPanel,
153
+ showCanvasPanel,
154
+ themeTokens,
155
+ ]);
156
+
157
+ const renderedTimelineItems = useMemo(() => selectRenderedTimelineItems({
158
+ timelineItems,
159
+ languageGuardEnabled,
160
+ friendlyToneEnabled,
161
+ responseStylePolicy: RESPONSE_STYLE_POLICY,
162
+ }), [friendlyToneEnabled, languageGuardEnabled, timelineItems]);
163
+
164
+ return {
165
+ canvasPanelNode,
166
+ renderedTimelineItems,
167
+ canvasEvidenceItems,
168
+ };
169
+ }
170
+
171
+ export default useWorkspaceCanvasProjection;
@@ -0,0 +1,199 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import { Typography } from 'antd';
3
+ import ComposerSectionNode from './ComposerSectionNode.jsx';
4
+ import {
5
+ WORKSPACE_CONTENT_MAX_WIDTH,
6
+ RESOLVED_UI_LABELS,
7
+ MODE_SWITCH_OPTIONS,
8
+ MODE_SUGGESTION_TIP_RULES,
9
+ } from './constants.js';
10
+ import { useAppActionErrorGuards } from './useAppActionErrorGuards.js';
11
+ import {
12
+ selectLatestAssistantQuestionPendingReply,
13
+ selectModeSuggestionContextFlags,
14
+ selectModeSuggestionTipRules,
15
+ } from '../selectors/modeSuggestionSelector.js';
16
+ import { useComposerAttachmentSync } from '../hooks/useComposerAttachmentSync.js';
17
+ import { useSendWithContextRefs } from '../hooks/useSendWithContextRefs.js';
18
+ import { useComposerChooserHandlers } from '../hooks/useComposerChooserHandlers.js';
19
+ import { useUserThresholdProfile } from '../hooks/useUserThresholdProfile.js';
20
+ import TriggerThresholdPopoverContent from '../components/TriggerThresholdPopoverContent.jsx';
21
+
22
+ const { Text } = Typography;
23
+
24
+ export function useWorkspaceComposerController({
25
+ userId,
26
+ projectId,
27
+ activeSessionId,
28
+ realApi,
29
+ viewModel,
30
+ apiReadinessGate,
31
+ blockedApiError,
32
+ sourceItems,
33
+ activeWorkspaceTab,
34
+ setActiveWorkspaceTab,
35
+ setShowCanvasPanel,
36
+ setCanvasActiveTabKey,
37
+ themeTokens,
38
+ }) {
39
+ const {
40
+ evidenceStrictMode,
41
+ setEvidenceStrictMode,
42
+ evidenceHitScoreThreshold,
43
+ setEvidenceHitScoreThreshold,
44
+ languageGuardEnabled,
45
+ setLanguageGuardEnabled,
46
+ friendlyToneEnabled,
47
+ setFriendlyToneEnabled,
48
+ questionModeSuggestionEnabled,
49
+ setQuestionModeSuggestionEnabled,
50
+ } = useUserThresholdProfile({ userId, projectId });
51
+
52
+ const [composerModeKey, setComposerModeKey] = useState('auto');
53
+ const [sourcingChooser, setSourcingChooser] = useState(null);
54
+ const [, setSourcingChooserDismissDraftKey] = useState('');
55
+
56
+ const composerFileList = Array.isArray(viewModel.composer.fileList)
57
+ ? viewModel.composer.fileList
58
+ : [];
59
+
60
+ const {
61
+ composerFileListRef,
62
+ hasNonReadyAttachments,
63
+ readyAttachmentRefs,
64
+ } = useComposerAttachmentSync({
65
+ composerFileList,
66
+ onSyncFileList: viewModel.composer.onSyncFileList,
67
+ realSourceApi: realApi?.sourceAPI || null,
68
+ projectId,
69
+ userId,
70
+ activeSessionId,
71
+ });
72
+
73
+ const latestSourcingContext = useMemo(() => {
74
+ const sourcingItem = [...(Array.isArray(viewModel.timeline.items) ? viewModel.timeline.items : [])]
75
+ .reverse()
76
+ .find((item) => String(item?.type || '').trim() === 'sourcing_candidates');
77
+ const payload = (
78
+ sourcingItem?.payload
79
+ && typeof sourcingItem.payload === 'object'
80
+ && !Array.isArray(sourcingItem.payload)
81
+ ) ? sourcingItem.payload : {};
82
+ const latestSourcingQuery = String(payload?.query || '').trim();
83
+ const latestSourcingResultIds = (Array.isArray(payload?.candidates) ? payload.candidates : [])
84
+ .map((item) => String(item?.result_id || item?.id || '').trim())
85
+ .filter(Boolean);
86
+ return {
87
+ latestSourcingQuery,
88
+ latestSourcingResultIds,
89
+ };
90
+ }, [viewModel.timeline.items]);
91
+
92
+ const handleSendWithSourceRefs = useSendWithContextRefs({
93
+ composerModeKey,
94
+ setSourcingChooser,
95
+ sourceItems,
96
+ composer: viewModel.composer,
97
+ activeWorkspaceTab,
98
+ setActiveWorkspaceTab,
99
+ composerFileListRef,
100
+ hasNonReadyAttachments,
101
+ readyAttachmentRefs,
102
+ latestSourcingQuery: latestSourcingContext.latestSourcingQuery,
103
+ latestSourcingResultIds: latestSourcingContext.latestSourcingResultIds,
104
+ });
105
+
106
+ const actionGuards = useAppActionErrorGuards({
107
+ blocked: apiReadinessGate.blocked,
108
+ blockedError: blockedApiError,
109
+ sessionListError: apiReadinessGate.error || viewModel.sessionList.error || null,
110
+ composerError: viewModel.composer.error || null,
111
+ onCreateProject: viewModel.sessionList.onCreateProject,
112
+ onCreateSession: viewModel.sessionList.onCreateSession,
113
+ onSend: handleSendWithSourceRefs,
114
+ });
115
+
116
+ const inputState = {
117
+ inputValue: viewModel.composer.value,
118
+ fileList: viewModel.composer.fileList,
119
+ handleInputChange: viewModel.composer.onChange,
120
+ handleRemoveFile: viewModel.composer.onRemoveFile,
121
+ };
122
+
123
+ const latestAssistantQuestionPendingReply = useMemo(
124
+ () => selectLatestAssistantQuestionPendingReply(viewModel.timeline.items),
125
+ [viewModel.timeline.items],
126
+ );
127
+
128
+ const suggestionTipRules = useMemo(() => selectModeSuggestionTipRules({
129
+ baseRules: MODE_SUGGESTION_TIP_RULES,
130
+ questionModeSuggestionEnabled,
131
+ }), [questionModeSuggestionEnabled]);
132
+
133
+ const suggestionContextFlags = useMemo(() => selectModeSuggestionContextFlags({
134
+ latestAssistantQuestionPendingReply,
135
+ }), [latestAssistantQuestionPendingReply]);
136
+
137
+ const { onComposerActionSelect, onComposerChooserDismiss } = useComposerChooserHandlers({
138
+ setComposerModeKey,
139
+ setSourcingChooser,
140
+ setShowCanvasPanel,
141
+ setCanvasActiveTabKey,
142
+ setSourcingChooserDismissDraftKey,
143
+ activeSessionId: viewModel.sessionList.activeSessionId,
144
+ composerValue: viewModel.composer.value,
145
+ });
146
+
147
+ const triggerThresholdPopoverContent = (
148
+ <TriggerThresholdPopoverContent
149
+ evidenceStrictMode={evidenceStrictMode}
150
+ setEvidenceStrictMode={setEvidenceStrictMode}
151
+ evidenceHitScoreThreshold={evidenceHitScoreThreshold}
152
+ setEvidenceHitScoreThreshold={setEvidenceHitScoreThreshold}
153
+ languageGuardEnabled={languageGuardEnabled}
154
+ setLanguageGuardEnabled={setLanguageGuardEnabled}
155
+ friendlyToneEnabled={friendlyToneEnabled}
156
+ setFriendlyToneEnabled={setFriendlyToneEnabled}
157
+ questionModeSuggestionEnabled={questionModeSuggestionEnabled}
158
+ setQuestionModeSuggestionEnabled={setQuestionModeSuggestionEnabled}
159
+ />
160
+ );
161
+
162
+ const composerNode = (
163
+ <ComposerSectionNode
164
+ themeTokens={themeTokens}
165
+ contentMaxWidth={WORKSPACE_CONTENT_MAX_WIDTH}
166
+ streamLoading={viewModel.composer.loading}
167
+ sendDisabled={hasNonReadyAttachments}
168
+ resolvedUILabels={RESOLVED_UI_LABELS}
169
+ modeSwitchOptions={MODE_SWITCH_OPTIONS}
170
+ inputState={inputState}
171
+ onAttachFiles={viewModel.composer.onAttachFiles}
172
+ onSend={actionGuards.handleSend}
173
+ effectiveComposerChooser={sourcingChooser}
174
+ onComposerActionSelect={onComposerActionSelect}
175
+ onComposerChooserDismiss={onComposerChooserDismiss}
176
+ initialModeKey={composerModeKey}
177
+ onModeChanged={setComposerModeKey}
178
+ userId={userId}
179
+ shouldRenderModeSuggestion
180
+ tipRules={suggestionTipRules}
181
+ suggestionContextFlags={suggestionContextFlags}
182
+ intentEscalationPolicyPopoverContent={triggerThresholdPopoverContent}
183
+ contextGaugePopoverContent={
184
+ <Text type="secondary">{String(viewModel.contextUsage?.text || '上下文窗口占用(暂无数据)')}</Text>
185
+ }
186
+ />
187
+ );
188
+
189
+ return {
190
+ actionGuards,
191
+ inputState,
192
+ composerNode,
193
+ composerModeKey,
194
+ languageGuardEnabled,
195
+ friendlyToneEnabled,
196
+ evidenceStrictMode,
197
+ evidenceHitScoreThreshold,
198
+ };
199
+ }