flare-chat-core 0.2.1 → 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 +170 -12
  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 +62 -455
  98. package/src/chat-core/session/useSessionListController.js +67 -0
  99. package/src/chat-core/stream/sse-client.js +1 -244
  100. package/src/chat-core/stream/sse-event-dispatcher.js +1 -0
  101. package/src/chat-core/stream/sse-events.js +1 -867
  102. package/src/chat-core/stream/useSSEStream.js +1 -356
  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,226 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import {
3
+ CHAT_UI_THEME_TOKENS,
4
+ DEFAULT_CHAT_UI_THEME,
5
+ KnowledgeHubDrawerContent,
6
+ } from 'flare-chat-ui';
7
+ import { useChatCorePipelines } from './useChatCorePipelines.js';
8
+ import {
9
+ RESOLVED_UI_LABELS,
10
+ STARTER_SCENARIOS,
11
+ } from './constants.js';
12
+ import {
13
+ selectActiveSession,
14
+ selectProjectItems,
15
+ } from './selectors.js';
16
+ import { useProjectSourceUpload } from './useProjectSourceUpload.js';
17
+ import { useProjectNameEditing } from './useProjectNameEditing.js';
18
+ import { useWorkspacePanels } from './useWorkspacePanels.js';
19
+ import { useWorkspaceComposerController } from './useWorkspaceComposerController.jsx';
20
+ import { useWorkspaceCanvasController } from './useWorkspaceCanvasController.jsx';
21
+
22
+ export function useWorkspaceController({
23
+ functionType = 'chat_component_debug',
24
+ defaultSessionTitle = '调试会话',
25
+ projectId = 'project_demo_001',
26
+ userId = 'user_demo_001',
27
+ backendMode = 'real',
28
+ kernelBaseUrl = 'http://127.0.0.1:18002',
29
+ apiBaseUrl = '',
30
+ apiToken = '',
31
+ defaultProjectName = '本地演示项目',
32
+ sessionAPI,
33
+ messageAPI,
34
+ } = {}) {
35
+ const {
36
+ apiReadinessGate,
37
+ blockedApiError,
38
+ realApi,
39
+ viewModel,
40
+ } = useChatCorePipelines({
41
+ functionType,
42
+ defaultSessionTitle,
43
+ projectId,
44
+ userId,
45
+ backendMode,
46
+ kernelBaseUrl,
47
+ apiBaseUrl,
48
+ apiToken,
49
+ sessionAPI,
50
+ messageAPI,
51
+ });
52
+
53
+ const themeTokens = CHAT_UI_THEME_TOKENS[DEFAULT_CHAT_UI_THEME];
54
+ const [activeWorkspaceTab, setActiveWorkspaceTab] = useState('chats');
55
+ const [showAllScenarios, setShowAllScenarios] = useState(false);
56
+ const {
57
+ knowledgeHubPopoverOpen,
58
+ setKnowledgeHubPopoverOpen,
59
+ showCanvasPanel,
60
+ setShowCanvasPanel,
61
+ canvasActiveTabKey,
62
+ setCanvasActiveTabKey,
63
+ handleOpenKnowledgeHub,
64
+ handleToggleWorkspacePanel,
65
+ } = useWorkspacePanels();
66
+ const [canvasFullscreenOpen, setCanvasFullscreenOpen] = useState(false);
67
+ const {
68
+ projectNameEditing,
69
+ projectNameDraft,
70
+ setProjectNameDraft,
71
+ projectDisplayName,
72
+ handleProjectNameStartEdit,
73
+ handleProjectNameCancel,
74
+ handleProjectNameSave,
75
+ } = useProjectNameEditing({ defaultProjectName });
76
+
77
+ const {
78
+ fileInputRef: sourceFileInputRef,
79
+ sourceActionError,
80
+ sourceItems,
81
+ sourceRemovingId,
82
+ sourceSyncLoading,
83
+ sourceUploadLoading,
84
+ sourceUploadProgressItems,
85
+ handleOpenSourcePicker,
86
+ handleSourceFileChange,
87
+ handleRemoveSource,
88
+ handleRetrySource,
89
+ handleViewSourceDetail,
90
+ } = useProjectSourceUpload({
91
+ projectId,
92
+ userId,
93
+ sourceAPI: realApi?.sourceAPI || null,
94
+ shouldLoadSources: activeWorkspaceTab === 'sources' || knowledgeHubPopoverOpen,
95
+ });
96
+
97
+ const sessions = Array.isArray(viewModel.sessionList.sessions) ? viewModel.sessionList.sessions : [];
98
+ const activeSessionId = viewModel.sessionList.activeSessionId;
99
+ const activeSession = selectActiveSession(sessions, activeSessionId);
100
+ const projectItems = selectProjectItems({
101
+ projectId,
102
+ projectDisplayName,
103
+ sessions,
104
+ });
105
+ const projectSlot = projectItems[0] || null;
106
+ const hasProject = Boolean(projectSlot?.name && String(projectSlot.name).trim());
107
+
108
+ const visibleScenarios = useMemo(() => (
109
+ showAllScenarios ? STARTER_SCENARIOS : STARTER_SCENARIOS.slice(0, 2)
110
+ ), [showAllScenarios]);
111
+
112
+ const handleProjectNameStartEditForSlot = () => {
113
+ handleProjectNameStartEdit(projectSlot);
114
+ };
115
+
116
+ const {
117
+ actionGuards,
118
+ inputState,
119
+ composerNode,
120
+ composerModeKey,
121
+ languageGuardEnabled,
122
+ friendlyToneEnabled,
123
+ evidenceStrictMode,
124
+ evidenceHitScoreThreshold,
125
+ } = useWorkspaceComposerController({
126
+ userId,
127
+ projectId,
128
+ activeSessionId,
129
+ realApi,
130
+ viewModel,
131
+ apiReadinessGate,
132
+ blockedApiError,
133
+ sourceItems,
134
+ activeWorkspaceTab,
135
+ setActiveWorkspaceTab,
136
+ setShowCanvasPanel,
137
+ setCanvasActiveTabKey,
138
+ themeTokens,
139
+ });
140
+
141
+ const {
142
+ canvasPanelNode,
143
+ renderedTimelineItems,
144
+ handleUICardAction,
145
+ generativeRegistry,
146
+ } = useWorkspaceCanvasController({
147
+ apiBaseUrl,
148
+ projectId,
149
+ userId,
150
+ activeSessionId,
151
+ timeline: viewModel.timeline,
152
+ composerModeKey,
153
+ evidenceStrictMode,
154
+ evidenceHitScoreThreshold,
155
+ showCanvasPanel,
156
+ setShowCanvasPanel,
157
+ canvasActiveTabKey,
158
+ setCanvasActiveTabKey,
159
+ canvasFullscreenOpen,
160
+ setCanvasFullscreenOpen,
161
+ inputState,
162
+ themeTokens,
163
+ languageGuardEnabled,
164
+ friendlyToneEnabled,
165
+ });
166
+
167
+ const knowledgeHubPopoverContent = (
168
+ <KnowledgeHubDrawerContent
169
+ resolvedUILabels={RESOLVED_UI_LABELS}
170
+ sourceUploadLoading={sourceUploadLoading}
171
+ sourceSyncLoading={sourceSyncLoading}
172
+ sourceActionError={sourceActionError}
173
+ sourceItems={sourceItems}
174
+ sourceUploadProgressItems={sourceUploadProgressItems}
175
+ handleOpenSourcePicker={handleOpenSourcePicker}
176
+ handleRemoveSource={handleRemoveSource}
177
+ handleViewSourceDetail={handleViewSourceDetail}
178
+ />
179
+ );
180
+
181
+ return {
182
+ themeTokens,
183
+ viewModel,
184
+ actionGuards,
185
+ apiReadinessGate,
186
+ hasProject,
187
+ projectItems,
188
+ projectSlot,
189
+ activeSession,
190
+ sessions,
191
+ projectNameEditing,
192
+ projectNameDraft,
193
+ setProjectNameDraft,
194
+ handleProjectNameSave,
195
+ handleProjectNameCancel,
196
+ projectDisplayName,
197
+ handleProjectNameStartEdit: handleProjectNameStartEditForSlot,
198
+ activeWorkspaceTab,
199
+ setActiveWorkspaceTab,
200
+ knowledgeHubPopoverContent,
201
+ knowledgeHubPopoverOpen,
202
+ setKnowledgeHubPopoverOpen,
203
+ handleOpenKnowledgeHub,
204
+ showCanvasPanel,
205
+ handleToggleWorkspacePanel,
206
+ composerNode,
207
+ handleOpenSourcePicker,
208
+ handleRemoveSource,
209
+ handleRetrySource,
210
+ handleViewSourceDetail,
211
+ sourceActionError,
212
+ sourceItems,
213
+ sourceRemovingId,
214
+ sourceSyncLoading,
215
+ sourceUploadLoading,
216
+ canvasPanelNode,
217
+ renderedTimelineItems,
218
+ handleUICardAction,
219
+ generativeRegistry,
220
+ showAllScenarios,
221
+ setShowAllScenarios,
222
+ visibleScenarios,
223
+ sourceFileInputRef,
224
+ handleSourceFileChange,
225
+ };
226
+ }
@@ -0,0 +1,55 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+
3
+ export function useWorkspacePanels() {
4
+ const [knowledgeHubPopoverOpen, setKnowledgeHubPopoverOpen] = useState(false);
5
+ const [showCanvasPanel, setShowCanvasPanel] = useState(false);
6
+ const [canvasActiveTabKey, setCanvasActiveTabKey] = useState('requirement');
7
+ const [canvasFocusEvidenceAnchorId, setCanvasFocusEvidenceAnchorId] = useState('');
8
+
9
+ const handleOpenKnowledgeHub = useCallback(() => {
10
+ setKnowledgeHubPopoverOpen(true);
11
+ }, []);
12
+
13
+ const handleToggleWorkspacePanel = useCallback(() => {
14
+ setShowCanvasPanel((prev) => !prev);
15
+ }, []);
16
+
17
+ const handleUICardAction = useCallback((action) => {
18
+ const actionType = String(action?.type || action?.action_key || '').trim();
19
+ if (actionType === 'open_evidence_citation') {
20
+ const citationId = String(action?.citation_id || '').trim();
21
+ setShowCanvasPanel(true);
22
+ setCanvasActiveTabKey('evidence');
23
+ setCanvasFocusEvidenceAnchorId(citationId ? `retrieval-evidence-${citationId}` : '');
24
+ return;
25
+ }
26
+ if (actionType.includes('canvas') || actionType.includes('evidence') || actionType.includes('sourcing')) {
27
+ setShowCanvasPanel(true);
28
+ setCanvasActiveTabKey(actionType.includes('sourcing') ? 'sourcing' : 'evidence');
29
+ }
30
+ }, []);
31
+
32
+ useEffect(() => {
33
+ if (!canvasFocusEvidenceAnchorId) {
34
+ return;
35
+ }
36
+ const timer = setTimeout(() => setCanvasFocusEvidenceAnchorId(''), 1600);
37
+ return () => clearTimeout(timer);
38
+ }, [canvasFocusEvidenceAnchorId]);
39
+
40
+ return {
41
+ knowledgeHubPopoverOpen,
42
+ setKnowledgeHubPopoverOpen,
43
+ showCanvasPanel,
44
+ setShowCanvasPanel,
45
+ canvasActiveTabKey,
46
+ setCanvasActiveTabKey,
47
+ canvasFocusEvidenceAnchorId,
48
+ setCanvasFocusEvidenceAnchorId,
49
+ handleOpenKnowledgeHub,
50
+ handleToggleWorkspacePanel,
51
+ handleUICardAction,
52
+ };
53
+ }
54
+
55
+ export default useWorkspacePanels;
@@ -0,0 +1,223 @@
1
+ import { useCallback, useEffect, useMemo, useRef } from 'react';
2
+ import {
3
+ collectReadyAttachmentRefs,
4
+ hasNonReadyComposerAttachments,
5
+ } from '../core-chat-entry/attachmentSendRefs.js';
6
+
7
+ export function useComposerAttachmentSync({
8
+ composerFileList,
9
+ onSyncFileList,
10
+ realSourceApi,
11
+ projectId,
12
+ userId,
13
+ activeSessionId,
14
+ } = {}) {
15
+ const composerFileListRef = useRef(Array.isArray(composerFileList) ? composerFileList : []);
16
+ const attachmentUploadTaskRef = useRef(new Set());
17
+ const attachmentPollingTaskRef = useRef(new Set());
18
+
19
+ useEffect(() => {
20
+ composerFileListRef.current = Array.isArray(composerFileList) ? composerFileList : [];
21
+ }, [composerFileList]);
22
+
23
+ const syncComposerFilePatch = useCallback((uid, patch = {}) => {
24
+ const normalizedUid = String(uid || '').trim();
25
+ if (!normalizedUid || typeof onSyncFileList !== 'function') {
26
+ return;
27
+ }
28
+ const currentList = Array.isArray(composerFileListRef.current)
29
+ ? composerFileListRef.current
30
+ : [];
31
+ let changed = false;
32
+ const nextList = currentList.map((item) => {
33
+ if (String(item?.uid || '').trim() !== normalizedUid) {
34
+ return item;
35
+ }
36
+ changed = true;
37
+ return {
38
+ ...item,
39
+ ...patch,
40
+ };
41
+ });
42
+ if (!changed) {
43
+ return;
44
+ }
45
+ composerFileListRef.current = nextList;
46
+ onSyncFileList(nextList);
47
+ }, [onSyncFileList]);
48
+
49
+ const pollComposerAttachmentStatus = useCallback((sourceId, fileUid) => {
50
+ const normalizedSourceId = String(sourceId || '').trim();
51
+ const normalizedFileUid = String(fileUid || '').trim();
52
+ if (
53
+ !normalizedSourceId
54
+ || !normalizedFileUid
55
+ || attachmentPollingTaskRef.current.has(normalizedSourceId)
56
+ || typeof realSourceApi?.listProjectSources !== 'function'
57
+ ) {
58
+ return;
59
+ }
60
+
61
+ attachmentPollingTaskRef.current.add(normalizedSourceId);
62
+ const wait = (ms) => new Promise((resolve) => {
63
+ setTimeout(resolve, ms);
64
+ });
65
+ const maxPollCount = 80;
66
+ const pollIntervalMs = 1500;
67
+
68
+ void (async () => {
69
+ let pollCount = 0;
70
+ while (pollCount < maxPollCount) {
71
+ pollCount += 1;
72
+ const fileExists = composerFileListRef.current.some((item) => String(item?.uid || '').trim() === normalizedFileUid);
73
+ if (!fileExists) {
74
+ return;
75
+ }
76
+ try {
77
+ const response = await realSourceApi.listProjectSources(projectId, activeSessionId || undefined);
78
+ const sources = Array.isArray(response?.sources) ? response.sources : [];
79
+ const matchedSource = sources.find((item) => {
80
+ const currentSourceId = String(item?.source_id || item?.id || '').trim();
81
+ return currentSourceId === normalizedSourceId;
82
+ });
83
+ if (matchedSource) {
84
+ const nextStatus = String(matchedSource.status || '').trim().toLowerCase() || 'uploaded';
85
+ syncComposerFilePatch(normalizedFileUid, {
86
+ source_id: normalizedSourceId,
87
+ status: nextStatus,
88
+ error_message: String(matchedSource?.error_message || ''),
89
+ });
90
+ if (nextStatus === 'ready' || nextStatus.startsWith('failed') || nextStatus === 'error') {
91
+ return;
92
+ }
93
+ }
94
+ } catch (error) {
95
+ syncComposerFilePatch(normalizedFileUid, {
96
+ error_message: String(error?.message || ''),
97
+ });
98
+ }
99
+ await wait(pollIntervalMs);
100
+ }
101
+ syncComposerFilePatch(normalizedFileUid, {
102
+ status: 'failed',
103
+ error_message: '文件处理超时,请重试',
104
+ });
105
+ })().finally(() => {
106
+ attachmentPollingTaskRef.current.delete(normalizedSourceId);
107
+ });
108
+ }, [activeSessionId, projectId, realSourceApi, syncComposerFilePatch]);
109
+
110
+ useEffect(() => {
111
+ if (typeof realSourceApi?.uploadSourceFile !== 'function') {
112
+ return;
113
+ }
114
+
115
+ const currentFileList = Array.isArray(composerFileList) ? composerFileList : [];
116
+ currentFileList.forEach((item) => {
117
+ const fileUid = String(item?.uid || '').trim();
118
+ if (!fileUid || attachmentUploadTaskRef.current.has(fileUid)) {
119
+ return;
120
+ }
121
+ const sourceId = String(item?.source_id || '').trim();
122
+ if (sourceId) {
123
+ const status = String(item?.status || '').trim().toLowerCase();
124
+ if (status && status !== 'ready') {
125
+ pollComposerAttachmentStatus(sourceId, fileUid);
126
+ }
127
+ return;
128
+ }
129
+ const status = String(item?.status || '').trim().toLowerCase();
130
+ if (status.startsWith('failed')) {
131
+ return;
132
+ }
133
+ const uploadableFile = item?.originFileObj || null;
134
+ if (!(uploadableFile instanceof File)) {
135
+ syncComposerFilePatch(fileUid, {
136
+ status: 'failed',
137
+ error_message: '缺少可上传文件',
138
+ });
139
+ return;
140
+ }
141
+
142
+ attachmentUploadTaskRef.current.add(fileUid);
143
+ syncComposerFilePatch(fileUid, {
144
+ status: 'uploading',
145
+ error_message: '',
146
+ });
147
+
148
+ void (async () => {
149
+ try {
150
+ const uploaded = await realSourceApi.uploadSourceFile({
151
+ project_id: projectId,
152
+ user_id: userId,
153
+ session_id: activeSessionId || null,
154
+ file: uploadableFile,
155
+ filename: item?.name || uploadableFile?.name || 'upload.bin',
156
+ });
157
+ const uploadedSourceId = String(uploaded?.source_id || uploaded?.id || '').trim();
158
+ if (!uploadedSourceId) {
159
+ syncComposerFilePatch(fileUid, {
160
+ status: 'failed',
161
+ error_message: '上传成功但未返回 source_id',
162
+ });
163
+ return;
164
+ }
165
+ const uploadedStatus = String(uploaded?.status || 'uploaded').trim().toLowerCase() || 'uploaded';
166
+ syncComposerFilePatch(fileUid, {
167
+ ...uploaded,
168
+ source_id: uploadedSourceId,
169
+ status: uploadedStatus,
170
+ error_message: String(uploaded?.error_message || ''),
171
+ });
172
+ if (uploadedStatus === 'ready' || uploadedStatus.startsWith('failed') || uploadedStatus === 'error') {
173
+ return;
174
+ }
175
+ pollComposerAttachmentStatus(uploadedSourceId, fileUid);
176
+ } catch (error) {
177
+ syncComposerFilePatch(fileUid, {
178
+ status: 'failed',
179
+ error_message: String(error?.message || '上传失败'),
180
+ });
181
+ } finally {
182
+ attachmentUploadTaskRef.current.delete(fileUid);
183
+ }
184
+ })();
185
+ });
186
+ }, [
187
+ activeSessionId,
188
+ composerFileList,
189
+ pollComposerAttachmentStatus,
190
+ projectId,
191
+ userId,
192
+ realSourceApi,
193
+ syncComposerFilePatch,
194
+ ]);
195
+
196
+ const readyAttachmentRefs = useMemo(
197
+ () => collectReadyAttachmentRefs(composerFileList),
198
+ [composerFileList],
199
+ );
200
+ const hasNonReadyAttachments = useMemo(
201
+ () => hasNonReadyComposerAttachments(composerFileList),
202
+ [composerFileList],
203
+ );
204
+ const syncErrors = useMemo(() => {
205
+ const list = Array.isArray(composerFileList) ? composerFileList : [];
206
+ return list
207
+ .map((item) => ({
208
+ uid: String(item?.uid || '').trim(),
209
+ source_id: String(item?.source_id || '').trim(),
210
+ error_message: String(item?.error_message || '').trim(),
211
+ }))
212
+ .filter((item) => item.error_message);
213
+ }, [composerFileList]);
214
+
215
+ return {
216
+ composerFileListRef,
217
+ hasNonReadyAttachments,
218
+ readyAttachmentRefs,
219
+ syncErrors,
220
+ };
221
+ }
222
+
223
+ export default useComposerAttachmentSync;
@@ -0,0 +1,52 @@
1
+ import { useCallback } from 'react';
2
+
3
+ function buildDraftRoundKey({ activeSessionId, composerValue }) {
4
+ return `${String(activeSessionId || 'draft')}:${String(composerValue || '').trim()}`;
5
+ }
6
+
7
+ export function useComposerChooserHandlers({
8
+ setComposerModeKey,
9
+ setSourcingChooser,
10
+ setShowCanvasPanel,
11
+ setCanvasActiveTabKey,
12
+ setSourcingChooserDismissDraftKey,
13
+ activeSessionId,
14
+ composerValue,
15
+ } = {}) {
16
+ const onComposerActionSelect = useCallback((choice) => {
17
+ const actionKey = String(choice?.action?.action_key || '').trim();
18
+ const draftRoundKey = buildDraftRoundKey({ activeSessionId, composerValue });
19
+ if (actionKey === 'open_sourcing') {
20
+ setComposerModeKey('intelligent_sourcing');
21
+ setSourcingChooser(null);
22
+ setShowCanvasPanel(true);
23
+ setCanvasActiveTabKey('sourcing');
24
+ return;
25
+ }
26
+ if (actionKey === 'dismiss_sourcing_once') {
27
+ setSourcingChooser(null);
28
+ setSourcingChooserDismissDraftKey(draftRoundKey);
29
+ }
30
+ }, [
31
+ activeSessionId,
32
+ composerValue,
33
+ setCanvasActiveTabKey,
34
+ setComposerModeKey,
35
+ setShowCanvasPanel,
36
+ setSourcingChooser,
37
+ setSourcingChooserDismissDraftKey,
38
+ ]);
39
+
40
+ const onComposerChooserDismiss = useCallback(() => {
41
+ const draftRoundKey = buildDraftRoundKey({ activeSessionId, composerValue });
42
+ setSourcingChooser(null);
43
+ setSourcingChooserDismissDraftKey(draftRoundKey);
44
+ }, [activeSessionId, composerValue, setSourcingChooser, setSourcingChooserDismissDraftKey]);
45
+
46
+ return {
47
+ onComposerActionSelect,
48
+ onComposerChooserDismiss,
49
+ };
50
+ }
51
+
52
+ export default useComposerChooserHandlers;
@@ -0,0 +1,140 @@
1
+ import { useCallback } from 'react';
2
+ import { detectSourcingIntent } from '../../chat-core/pipelines/preprocess-message.js';
3
+ import {
4
+ collectSourceIdStrings,
5
+ mergeSourceReferenceLists,
6
+ } from '../core-chat-entry/attachmentSendRefs.js';
7
+
8
+ function normalizeText(value) {
9
+ return String(value || '').trim();
10
+ }
11
+
12
+ export function isSourcingRefreshCommand(value) {
13
+ const normalized = normalizeText(value);
14
+ if (!normalized) {
15
+ return false;
16
+ }
17
+ if (normalized === '继续' || normalized === '继续寻源') {
18
+ return true;
19
+ }
20
+ return normalized.includes('换一批') || normalized.includes('再来一批');
21
+ }
22
+
23
+ export function resolveSourcingPayloadExtra({
24
+ streamOptions,
25
+ isSourcingMode,
26
+ currentInput,
27
+ latestSourcingQuery,
28
+ latestSourcingResultIds,
29
+ }) {
30
+ const payloadExtra = (
31
+ streamOptions?.payloadExtra
32
+ && typeof streamOptions.payloadExtra === 'object'
33
+ && !Array.isArray(streamOptions.payloadExtra)
34
+ ) ? { ...streamOptions.payloadExtra } : {};
35
+ if (!isSourcingMode) {
36
+ return payloadExtra;
37
+ }
38
+
39
+ const normalizedInput = normalizeText(currentInput);
40
+ const normalizedLatestQuery = normalizeText(latestSourcingQuery);
41
+ if (normalizedLatestQuery) {
42
+ const hasSourcingIntentInInput = detectSourcingIntent(normalizedInput);
43
+ if (!hasSourcingIntentInInput || isSourcingRefreshCommand(normalizedInput)) {
44
+ payloadExtra.sourcing_query = normalizedLatestQuery;
45
+ }
46
+ }
47
+ if (isSourcingRefreshCommand(normalizedInput)) {
48
+ payloadExtra.append_mode = true;
49
+ payloadExtra.lock_existing_order = true;
50
+ payloadExtra.base_result_ids = Array.isArray(latestSourcingResultIds)
51
+ ? latestSourcingResultIds.map((item) => normalizeText(item)).filter(Boolean)
52
+ : [];
53
+ }
54
+ return payloadExtra;
55
+ }
56
+
57
+ export function useSendWithContextRefs({
58
+ composerModeKey,
59
+ setSourcingChooser,
60
+ sourceItems,
61
+ composer,
62
+ activeWorkspaceTab,
63
+ setActiveWorkspaceTab,
64
+ composerFileListRef,
65
+ hasNonReadyAttachments,
66
+ readyAttachmentRefs,
67
+ latestSourcingQuery = '',
68
+ latestSourcingResultIds = [],
69
+ } = {}) {
70
+ return useCallback(async (options = {}) => {
71
+ const streamOptions = (
72
+ options?.streamOptions
73
+ && typeof options.streamOptions === 'object'
74
+ && !Array.isArray(options.streamOptions)
75
+ ) ? { ...options.streamOptions } : {};
76
+ const normalizedModeKey = String(streamOptions.modeKey || composerModeKey || '').trim();
77
+ const isSourcingMode = normalizedModeKey === 'intelligent_sourcing';
78
+ const currentInput = String(composer?.value || '').trim();
79
+
80
+ if (hasNonReadyAttachments) {
81
+ return false;
82
+ }
83
+
84
+ setSourcingChooser(null);
85
+ const currentComposerFileList = Array.isArray(composerFileListRef?.current)
86
+ ? composerFileListRef.current
87
+ : [];
88
+ const projectReadyRefs = Array.isArray(sourceItems)
89
+ ? sourceItems.filter((item) => String(item?.status || '').trim().toLowerCase() === 'ready')
90
+ : [];
91
+ const sourcingDefaultRefs = isSourcingMode ? mergeSourceReferenceLists(projectReadyRefs) : [];
92
+ const mergedRefs = mergeSourceReferenceLists(
93
+ options?.context_refs,
94
+ options?.knowledge_refs,
95
+ readyAttachmentRefs,
96
+ sourcingDefaultRefs,
97
+ );
98
+ const mergedKnowledgeRefIds = collectSourceIdStrings(
99
+ options?.context_refs,
100
+ options?.knowledge_refs,
101
+ readyAttachmentRefs,
102
+ sourcingDefaultRefs,
103
+ );
104
+ streamOptions.payloadExtra = resolveSourcingPayloadExtra({
105
+ streamOptions,
106
+ isSourcingMode,
107
+ currentInput,
108
+ latestSourcingQuery,
109
+ latestSourcingResultIds,
110
+ });
111
+
112
+ const sendResult = await composer.onSend({
113
+ ...options,
114
+ ...(currentInput ? { content: currentInput } : {}),
115
+ streamOptions,
116
+ context_refs: mergedRefs,
117
+ knowledge_refs: mergedKnowledgeRefIds,
118
+ attachedSessionFilesSnapshot: currentComposerFileList,
119
+ });
120
+
121
+ if (sendResult === true && activeWorkspaceTab === 'sources') {
122
+ setActiveWorkspaceTab('chats');
123
+ }
124
+ return sendResult;
125
+ }, [
126
+ activeWorkspaceTab,
127
+ composer,
128
+ composerFileListRef,
129
+ composerModeKey,
130
+ hasNonReadyAttachments,
131
+ readyAttachmentRefs,
132
+ setActiveWorkspaceTab,
133
+ setSourcingChooser,
134
+ sourceItems,
135
+ latestSourcingQuery,
136
+ latestSourcingResultIds,
137
+ ]);
138
+ }
139
+
140
+ export default useSendWithContextRefs;