flare-chat-core 0.2.3 → 0.2.5
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.
- package/dist/index.js +6372 -0
- package/package.json +19 -22
- package/docs/CAPABILITY-INVENTORY.md +0 -42
- package/docs/CHAT-CORE-BOUNDARY.md +0 -47
- package/docs/CORE-APP-REALIGNMENT-WORKLOAD-2026-04-18.md +0 -86
- package/docs/SSOT-CHAT-CORE-BOUNDARY.md +0 -73
- package/docs/SSOT-CHAT-CORE-DATAFLOW.md +0 -97
- package/index.html +0 -12
- package/scripts/check.sh +0 -15
- package/src/adapters/index.js +0 -6
- package/src/adapters/message-api.adapter.js +0 -59
- package/src/adapters/session-api.adapter.js +0 -133
- package/src/adapters/session-message-api.http.js +0 -161
- package/src/adapters/session-message-api.js +0 -34
- package/src/adapters/session-message-api.normalize-source-record.test.mjs +0 -180
- package/src/adapters/session-message-api.normalizers.js +0 -153
- package/src/adapters/source-api.adapter.js +0 -135
- package/src/adapters/sse-client.js +0 -244
- package/src/adapters/sse-event-dispatcher.js +0 -121
- package/src/app/App.jsx +0 -11
- package/src/app/AppProviders.jsx +0 -12
- package/src/app/ChatWorkspaceScreen.jsx +0 -33
- package/src/app/WorkspaceLayout.jsx +0 -190
- package/src/app/components/AppCanvasPanel.jsx +0 -64
- package/src/app/components/TriggerThresholdPopoverContent.jsx +0 -122
- package/src/app/components/WorkspaceBodySection.jsx +0 -156
- package/src/app/components/WorkspaceMainPane.jsx +0 -121
- package/src/app/components/WorkspaceSessionPane.jsx +0 -70
- package/src/app/components/WorkspaceTopBarSection.jsx +0 -71
- package/src/app/core-chat-entry/ComposerSectionNode.jsx +0 -241
- package/src/app/core-chat-entry/attachmentSendRefs.js +0 -154
- package/src/app/core-chat-entry/attachmentSendRefs.test.mjs +0 -101
- package/src/app/core-chat-entry/composerActionRouter.js +0 -26
- package/src/app/core-chat-entry/constants.js +0 -108
- package/src/app/core-chat-entry/selectors.js +0 -28
- package/src/app/core-chat-entry/useAppActionErrorGuards.js +0 -68
- package/src/app/core-chat-entry/useChatCorePipelines.js +0 -110
- package/src/app/core-chat-entry/useComposerModeSuggestion.js +0 -89
- package/src/app/core-chat-entry/useDevCapabilityStatusNote.js +0 -22
- package/src/app/core-chat-entry/useProjectNameEditing.js +0 -41
- package/src/app/core-chat-entry/useProjectSourceUpload.js +0 -341
- package/src/app/core-chat-entry/useRealApiReadinessGate.js +0 -103
- package/src/app/core-chat-entry/useUnavailableActionError.js +0 -29
- package/src/app/core-chat-entry/useWorkspaceCanvasController.jsx +0 -177
- package/src/app/core-chat-entry/useWorkspaceCanvasProjection.jsx +0 -171
- package/src/app/core-chat-entry/useWorkspaceComposerController.jsx +0 -199
- package/src/app/core-chat-entry/useWorkspaceController.jsx +0 -226
- package/src/app/core-chat-entry/useWorkspacePanels.js +0 -55
- package/src/app/hooks/useComposerAttachmentSync.js +0 -223
- package/src/app/hooks/useComposerChooserHandlers.js +0 -52
- package/src/app/hooks/useSendWithContextRefs.js +0 -140
- package/src/app/hooks/useSendWithContextRefs.test.mjs +0 -29
- package/src/app/hooks/useUserThresholdProfile.js +0 -121
- package/src/app/index.js +0 -1
- package/src/app/selectors/assistantTextSelector.js +0 -73
- package/src/app/selectors/canvasEvidenceSummarySelector.js +0 -28
- package/src/app/selectors/canvasReportTemplateSelector.js +0 -28
- package/src/app/selectors/canvasTabsSelector.js +0 -58
- package/src/app/selectors/evidenceProjectionSelector.js +0 -175
- package/src/app/selectors/evidenceProjectionSelector.test.mjs +0 -107
- package/src/app/selectors/modeSuggestionSelector.js +0 -50
- package/src/chat-core/app/mockRuntime.js +0 -291
- package/src/chat-core/app/useAppStream.js +0 -187
- package/src/chat-core/app/useAppStream.refs.test.mjs +0 -44
- package/src/chat-core/app/useAppStream.request-body.test.mjs +0 -116
- package/src/chat-core/app/useCoreChatApp.js +0 -115
- package/src/chat-core/facade/useBasicConversationFacade.js +0 -280
- package/src/chat-core/index.js +0 -14
- package/src/chat-core/input/useChatInput.js +0 -103
- package/src/chat-core/messages/buildTimelineItems.analysis-route.test.mjs +0 -36
- package/src/chat-core/messages/buildTimelineItems.js +0 -201
- package/src/chat-core/messages/buildTimelineItems.knowledge-citation.test.mjs +0 -182
- package/src/chat-core/messages/contextUsageDefaults.js +0 -3
- package/src/chat-core/messages/contextUsageViewModel.js +0 -147
- package/src/chat-core/messages/contextUsageViewModel.test.mjs +0 -74
- package/src/chat-core/messages/useContextUsageViewModel.js +0 -41
- package/src/chat-core/orchestration/useBasicSendHandler.js +0 -55
- package/src/chat-core/pipelines/build-action-request.js +0 -46
- package/src/chat-core/pipelines/build-stream-request.js +0 -74
- package/src/chat-core/pipelines/entity-extraction.js +0 -159
- package/src/chat-core/pipelines/preprocess-message.js +0 -16
- package/src/chat-core/pipelines/stream-persist-utils.js +0 -32
- package/src/chat-core/pipelines/transport/send-mock-stream.js +0 -86
- package/src/chat-core/pipelines/transport/send-real-stream.js +0 -330
- package/src/chat-core/pipelines/transport/send-real-stream.test.mjs +0 -27
- package/src/chat-core/pipelines/transport/send-sourcing-search.js +0 -86
- package/src/chat-core/pipelines/transport/send-sourcing-search.test.mjs +0 -14
- package/src/chat-core/pipelines/transport/sourcing-response-templates.js +0 -55
- package/src/chat-core/pipelines/transport/sourcing-search-api.js +0 -155
- package/src/chat-core/runtime/runtimeMode.js +0 -69
- package/src/chat-core/session/chatSessionActionTypes.js +0 -24
- package/src/chat-core/session/chatSessionReducer.js +0 -352
- package/src/chat-core/session/chatSessionReducer.streaming-done.test.mjs +0 -39
- package/src/chat-core/session/index.js +0 -2
- package/src/chat-core/session/sessionActionsMessages.js +0 -44
- package/src/chat-core/session/sessionActionsSessionCrud.js +0 -131
- package/src/chat-core/session/sessionActionsStreaming.js +0 -80
- package/src/chat-core/session/sessionActionsUiState.js +0 -51
- package/src/chat-core/session/useChatSessionReducer.js +0 -131
- package/src/chat-core/session/useSessionListController.js +0 -67
- package/src/chat-core/stream/sse-client.js +0 -1
- package/src/chat-core/stream/sse-event-dispatcher.js +0 -1
- package/src/chat-core/stream/sse-events.js +0 -1
- package/src/chat-core/stream/useSSEStream.js +0 -1
- package/src/chat-core/stream/useStreamSendController.js +0 -46
- package/src/contracts/context-ssot.js +0 -47
- package/src/contracts/index.js +0 -1
- package/src/contracts/sse-events/base-parsers.js +0 -79
- package/src/contracts/sse-events/domain-parsers.js +0 -3
- package/src/contracts/sse-events/internal-normalizers.js +0 -143
- package/src/contracts/sse-events/parsers-intake.js +0 -235
- package/src/contracts/sse-events/parsers-runtime.js +0 -37
- package/src/contracts/sse-events/parsers-sourcing.js +0 -179
- package/src/contracts/sse-events/patch-event-parser.js +0 -121
- package/src/contracts/sse-events/runtime-parsers.js +0 -79
- package/src/contracts/sse-events.js +0 -4
- package/src/index.js +0 -6
- package/src/main.jsx +0 -28
- package/src/orchestration/index.js +0 -6
- package/src/orchestration/useSSEStream.js +0 -221
- package/src/state/index.js +0 -4
- package/vite.config.js +0 -36
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { useCallback, useMemo, useState } from 'react';
|
|
2
|
-
|
|
3
|
-
function normalizeError(error, fallbackMessage) {
|
|
4
|
-
if (error instanceof Error) {
|
|
5
|
-
return error;
|
|
6
|
-
}
|
|
7
|
-
return new Error(String(error || fallbackMessage));
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function useAppActionErrorGuards({
|
|
11
|
-
blocked = false,
|
|
12
|
-
blockedError,
|
|
13
|
-
sessionListError = null,
|
|
14
|
-
composerError = null,
|
|
15
|
-
onCreateProject,
|
|
16
|
-
onCreateSession,
|
|
17
|
-
onSend,
|
|
18
|
-
} = {}) {
|
|
19
|
-
const [actionError, setActionError] = useState(null);
|
|
20
|
-
|
|
21
|
-
const handleCreateProject = useCallback(async () => {
|
|
22
|
-
if (blocked) {
|
|
23
|
-
setActionError(blockedError);
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
try {
|
|
27
|
-
setActionError(null);
|
|
28
|
-
await onCreateProject?.();
|
|
29
|
-
} catch (nextError) {
|
|
30
|
-
setActionError(normalizeError(nextError, '创建项目失败'));
|
|
31
|
-
}
|
|
32
|
-
}, [blocked, blockedError, onCreateProject]);
|
|
33
|
-
|
|
34
|
-
const handleCreateSession = useCallback(async () => {
|
|
35
|
-
if (blocked) {
|
|
36
|
-
setActionError(blockedError);
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
try {
|
|
40
|
-
setActionError(null);
|
|
41
|
-
await onCreateSession?.();
|
|
42
|
-
} catch (nextError) {
|
|
43
|
-
setActionError(normalizeError(nextError, '创建会话失败'));
|
|
44
|
-
}
|
|
45
|
-
}, [blocked, blockedError, onCreateSession]);
|
|
46
|
-
|
|
47
|
-
const handleSend = useCallback(async (options = {}) => {
|
|
48
|
-
if (blocked) {
|
|
49
|
-
setActionError(blockedError);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
setActionError(null);
|
|
53
|
-
const sendResult = await onSend?.(options);
|
|
54
|
-
if (sendResult instanceof Error) {
|
|
55
|
-
setActionError(sendResult);
|
|
56
|
-
}
|
|
57
|
-
return sendResult;
|
|
58
|
-
}, [blocked, blockedError, onSend]);
|
|
59
|
-
|
|
60
|
-
return useMemo(() => ({
|
|
61
|
-
actionError,
|
|
62
|
-
handleCreateProject,
|
|
63
|
-
handleCreateSession,
|
|
64
|
-
handleSend,
|
|
65
|
-
sessionPaneError: actionError || sessionListError || null,
|
|
66
|
-
streamError: actionError || composerError || null,
|
|
67
|
-
}), [actionError, composerError, handleCreateProject, handleCreateSession, handleSend, sessionListError]);
|
|
68
|
-
}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { useMemo } from 'react';
|
|
2
|
-
import { useCoreChatApp } from '../../chat-core/app/useCoreChatApp.js';
|
|
3
|
-
import { useAppStream } from '../../chat-core/app/useAppStream.js';
|
|
4
|
-
import { createMockRuntime } from '../../chat-core/app/mockRuntime.js';
|
|
5
|
-
import { createSessionMessageAPI } from '../../adapters/session-message-api.js';
|
|
6
|
-
import { useRealApiReadinessGate } from './useRealApiReadinessGate.js';
|
|
7
|
-
|
|
8
|
-
export function useChatCorePipelines({
|
|
9
|
-
functionType,
|
|
10
|
-
defaultSessionTitle,
|
|
11
|
-
projectId,
|
|
12
|
-
userId,
|
|
13
|
-
backendMode,
|
|
14
|
-
kernelBaseUrl,
|
|
15
|
-
apiBaseUrl,
|
|
16
|
-
apiToken,
|
|
17
|
-
sessionAPI,
|
|
18
|
-
messageAPI,
|
|
19
|
-
}) {
|
|
20
|
-
const normalizedApiBaseUrl = String(apiBaseUrl || '').trim();
|
|
21
|
-
const runtime = useMemo(() => createMockRuntime({
|
|
22
|
-
defaultFunctionType: functionType,
|
|
23
|
-
defaultSessionTitle,
|
|
24
|
-
}), [defaultSessionTitle, functionType]);
|
|
25
|
-
|
|
26
|
-
const scope = useMemo(() => ({
|
|
27
|
-
projectId,
|
|
28
|
-
userId,
|
|
29
|
-
}), [projectId, userId]);
|
|
30
|
-
|
|
31
|
-
const listParams = useMemo(() => ({
|
|
32
|
-
status: 'active',
|
|
33
|
-
page: 1,
|
|
34
|
-
page_size: 20,
|
|
35
|
-
function_type: functionType,
|
|
36
|
-
project_id: projectId,
|
|
37
|
-
}), [functionType, projectId]);
|
|
38
|
-
|
|
39
|
-
const shouldUseRealAPI = backendMode === 'real' && !sessionAPI && !messageAPI;
|
|
40
|
-
const apiReadinessGate = useRealApiReadinessGate({
|
|
41
|
-
enabled: shouldUseRealAPI,
|
|
42
|
-
apiBaseUrl: normalizedApiBaseUrl,
|
|
43
|
-
apiToken,
|
|
44
|
-
});
|
|
45
|
-
const blockedApiError = useMemo(() => (
|
|
46
|
-
apiReadinessGate.error || new Error('API 不可用,请检查配置与服务状态')
|
|
47
|
-
), [apiReadinessGate.error]);
|
|
48
|
-
const blockedSessionAPI = useMemo(() => ({
|
|
49
|
-
list: async () => ({ sessions: [] }),
|
|
50
|
-
get: async () => {
|
|
51
|
-
throw blockedApiError;
|
|
52
|
-
},
|
|
53
|
-
create: async () => {
|
|
54
|
-
throw blockedApiError;
|
|
55
|
-
},
|
|
56
|
-
update: async () => {
|
|
57
|
-
throw blockedApiError;
|
|
58
|
-
},
|
|
59
|
-
}), [blockedApiError]);
|
|
60
|
-
const blockedMessageAPI = useMemo(() => ({
|
|
61
|
-
list: async () => {
|
|
62
|
-
throw blockedApiError;
|
|
63
|
-
},
|
|
64
|
-
}), [blockedApiError]);
|
|
65
|
-
const realApi = useMemo(() => {
|
|
66
|
-
if (!shouldUseRealAPI || apiReadinessGate.blocked) {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
return createSessionMessageAPI({
|
|
70
|
-
baseUrl: normalizedApiBaseUrl,
|
|
71
|
-
token: apiToken,
|
|
72
|
-
});
|
|
73
|
-
}, [apiReadinessGate.blocked, apiToken, normalizedApiBaseUrl, shouldUseRealAPI]);
|
|
74
|
-
const resolvedSessionAPI = sessionAPI
|
|
75
|
-
|| realApi?.sessionAPI
|
|
76
|
-
|| (shouldUseRealAPI ? blockedSessionAPI : runtime.sessionAPI);
|
|
77
|
-
const resolvedMessageAPI = messageAPI
|
|
78
|
-
|| realApi?.messageAPI
|
|
79
|
-
|| (shouldUseRealAPI ? blockedMessageAPI : runtime.messageAPI);
|
|
80
|
-
const useRuntimePersistence = !shouldUseRealAPI && !sessionAPI && !messageAPI;
|
|
81
|
-
|
|
82
|
-
const stream = useAppStream({
|
|
83
|
-
runtime,
|
|
84
|
-
backendMode,
|
|
85
|
-
apiBaseUrl: normalizedApiBaseUrl,
|
|
86
|
-
kernelBaseUrl,
|
|
87
|
-
scope,
|
|
88
|
-
persistExchange: useRuntimePersistence ? runtime.appendExchange : null,
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const { viewModel } = useCoreChatApp({
|
|
92
|
-
sessionAPI: resolvedSessionAPI,
|
|
93
|
-
messageAPI: resolvedMessageAPI,
|
|
94
|
-
stream,
|
|
95
|
-
functionType,
|
|
96
|
-
defaultSessionTitle,
|
|
97
|
-
listParams,
|
|
98
|
-
createSessionPayload: {
|
|
99
|
-
project_id: projectId,
|
|
100
|
-
user_id: userId,
|
|
101
|
-
},
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
return {
|
|
105
|
-
apiReadinessGate,
|
|
106
|
-
blockedApiError,
|
|
107
|
-
realApi,
|
|
108
|
-
viewModel,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { useMemo } from 'react';
|
|
2
|
-
|
|
3
|
-
function normalizeText(value) {
|
|
4
|
-
return String(value || '').trim().toLowerCase();
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
function hasAnyKeyword(content, keywords = []) {
|
|
8
|
-
return keywords.some((keyword) => content.includes(String(keyword || '').trim().toLowerCase()));
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function buildSuggestionFromInput({
|
|
12
|
-
inputValue,
|
|
13
|
-
activeModeKey,
|
|
14
|
-
tipRules = [],
|
|
15
|
-
contextFlags = {},
|
|
16
|
-
}) {
|
|
17
|
-
const resolvedModeKey = String(activeModeKey || '').trim();
|
|
18
|
-
if (resolvedModeKey !== 'auto') {
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
const normalizedInput = normalizeText(inputValue);
|
|
22
|
-
const normalizedRules = Array.isArray(tipRules) ? tipRules : [];
|
|
23
|
-
if (!normalizedInput && normalizedRules.length === 0) {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
const matchedRule = normalizedRules.find((rule) => {
|
|
27
|
-
const keywords = Array.isArray(rule?.keywords) ? rule.keywords : [];
|
|
28
|
-
const hasKeywordMatch = normalizedInput && hasAnyKeyword(normalizedInput, keywords);
|
|
29
|
-
const contextKey = String(rule?.whenContextKey || '').trim();
|
|
30
|
-
const hasContextMatch = contextKey ? contextFlags?.[contextKey] === true : false;
|
|
31
|
-
return hasKeywordMatch || hasContextMatch;
|
|
32
|
-
});
|
|
33
|
-
if (!matchedRule) {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
return {
|
|
37
|
-
source: String(matchedRule.source || 'local_input_tip'),
|
|
38
|
-
modeKey: String(matchedRule.modeKey || '').trim(),
|
|
39
|
-
label: String(matchedRule.label || '').trim(),
|
|
40
|
-
reason: String(matchedRule.reason || '').trim(),
|
|
41
|
-
actionText: String(matchedRule.actionText || '').trim(),
|
|
42
|
-
statusOnly: matchedRule.statusOnly === true,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export default function useComposerModeSuggestion({
|
|
47
|
-
inputValue,
|
|
48
|
-
activeModeKey,
|
|
49
|
-
modeSuggestion,
|
|
50
|
-
modeSuggestionAction,
|
|
51
|
-
tipRules = [],
|
|
52
|
-
contextFlags = {},
|
|
53
|
-
}) {
|
|
54
|
-
return useMemo(() => {
|
|
55
|
-
if (modeSuggestionAction && typeof modeSuggestionAction === 'object') {
|
|
56
|
-
return {
|
|
57
|
-
modeSuggestion: modeSuggestion || null,
|
|
58
|
-
modeSuggestionAction,
|
|
59
|
-
fromFallback: false,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
const fallbackSuggestion = buildSuggestionFromInput({
|
|
63
|
-
inputValue,
|
|
64
|
-
activeModeKey,
|
|
65
|
-
tipRules,
|
|
66
|
-
contextFlags,
|
|
67
|
-
});
|
|
68
|
-
if (!fallbackSuggestion) {
|
|
69
|
-
return {
|
|
70
|
-
modeSuggestion: modeSuggestion || null,
|
|
71
|
-
modeSuggestionAction: null,
|
|
72
|
-
fromFallback: false,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
return {
|
|
76
|
-
modeSuggestion: fallbackSuggestion,
|
|
77
|
-
modeSuggestionAction: {
|
|
78
|
-
source: 'mode_suggestion',
|
|
79
|
-
text: fallbackSuggestion.actionText || `启用${fallbackSuggestion.label || fallbackSuggestion.modeKey || ''}`,
|
|
80
|
-
action: {
|
|
81
|
-
action_type: 'switch_mode',
|
|
82
|
-
target_mode: fallbackSuggestion.modeKey,
|
|
83
|
-
label: fallbackSuggestion.actionText || '',
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
fromFallback: true,
|
|
87
|
-
};
|
|
88
|
-
}, [activeModeKey, contextFlags, inputValue, modeSuggestion, modeSuggestionAction, tipRules]);
|
|
89
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { useMemo } from 'react';
|
|
2
|
-
|
|
3
|
-
export function useDevCapabilityStatusNote({
|
|
4
|
-
isDev = false,
|
|
5
|
-
mode = '',
|
|
6
|
-
labels = {},
|
|
7
|
-
} = {}) {
|
|
8
|
-
return useMemo(() => {
|
|
9
|
-
if (!isDev) {
|
|
10
|
-
return '';
|
|
11
|
-
}
|
|
12
|
-
const normalizedMode = String(mode || '').trim().toLowerCase();
|
|
13
|
-
if (normalizedMode === 'persistent') {
|
|
14
|
-
return String(labels.persistent || '开发态:资料能力已接入持久化链路');
|
|
15
|
-
}
|
|
16
|
-
if (normalizedMode === 'local_fallback') {
|
|
17
|
-
return String(labels.localFallback || '开发态:资料能力当前为本地回退模式');
|
|
18
|
-
}
|
|
19
|
-
return String(labels.unknown || '开发态:资料能力状态未知');
|
|
20
|
-
}, [isDev, labels.localFallback, labels.persistent, labels.unknown, mode]);
|
|
21
|
-
}
|
|
22
|
-
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
|
|
3
|
-
export function useProjectNameEditing({ defaultProjectName } = {}) {
|
|
4
|
-
const initialName = String(defaultProjectName || '').trim() || '本地演示项目';
|
|
5
|
-
const [projectNameEditing, setProjectNameEditing] = useState(false);
|
|
6
|
-
const [projectNameDraft, setProjectNameDraft] = useState(initialName);
|
|
7
|
-
const [projectDisplayName, setProjectDisplayName] = useState(initialName);
|
|
8
|
-
|
|
9
|
-
const handleProjectNameStartEdit = (projectSlot) => {
|
|
10
|
-
if (!projectSlot?.key) {
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
|
-
setProjectNameDraft(String(projectDisplayName || '').trim());
|
|
14
|
-
setProjectNameEditing(true);
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const handleProjectNameCancel = () => {
|
|
18
|
-
setProjectNameEditing(false);
|
|
19
|
-
setProjectNameDraft('');
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const handleProjectNameSave = () => {
|
|
23
|
-
const nextName = String(projectNameDraft || '').trim();
|
|
24
|
-
if (nextName) {
|
|
25
|
-
setProjectDisplayName(nextName);
|
|
26
|
-
}
|
|
27
|
-
handleProjectNameCancel();
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
projectNameEditing,
|
|
32
|
-
projectNameDraft,
|
|
33
|
-
setProjectNameDraft,
|
|
34
|
-
projectDisplayName,
|
|
35
|
-
handleProjectNameStartEdit,
|
|
36
|
-
handleProjectNameCancel,
|
|
37
|
-
handleProjectNameSave,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export default useProjectNameEditing;
|
|
@@ -1,341 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
-
|
|
3
|
-
const projectSourceListSingleflight = new Map();
|
|
4
|
-
const SOURCE_STATUS_TERMINAL = new Set(['ready', 'failed', 'error']);
|
|
5
|
-
|
|
6
|
-
function normalizeError(error, fallbackMessage) {
|
|
7
|
-
if (error instanceof Error) {
|
|
8
|
-
return error;
|
|
9
|
-
}
|
|
10
|
-
return new Error(String(error || fallbackMessage));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function buildSourceItem(file, index, scopedProjectId) {
|
|
14
|
-
return {
|
|
15
|
-
id: `project-source-${Date.now()}-${index}`,
|
|
16
|
-
project_id: scopedProjectId,
|
|
17
|
-
message_id: null,
|
|
18
|
-
scope: 'project',
|
|
19
|
-
source: 'local_upload',
|
|
20
|
-
filename: String(file?.name || '').trim() || `资料 ${index + 1}`,
|
|
21
|
-
mime_type: String(file?.type || '').trim(),
|
|
22
|
-
size: Number.isFinite(file?.size) ? file.size : null,
|
|
23
|
-
created_at: new Date().toISOString(),
|
|
24
|
-
status: 'ready',
|
|
25
|
-
error_message: '',
|
|
26
|
-
persisted_to_project: false,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function buildSourceFileSignature(item = {}) {
|
|
31
|
-
return `${item.filename || ''}::${item.size || ''}::${item.mime_type || ''}`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function parseSourceList(response) {
|
|
35
|
-
if (Array.isArray(response?.sources)) {
|
|
36
|
-
return response.sources;
|
|
37
|
-
}
|
|
38
|
-
if (Array.isArray(response?.data?.sources)) {
|
|
39
|
-
return response.data.sources;
|
|
40
|
-
}
|
|
41
|
-
return [];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function normalizeSourceStatus(status) {
|
|
45
|
-
return String(status || '').trim().toLowerCase();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function hasPendingSourceProcessing(items = []) {
|
|
49
|
-
if (!Array.isArray(items) || items.length === 0) {
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
return items.some((item) => !SOURCE_STATUS_TERMINAL.has(normalizeSourceStatus(item?.status)));
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function createUploadProgressItem(file, index) {
|
|
56
|
-
return {
|
|
57
|
-
id: `upload-progress-${Date.now()}-${index}`,
|
|
58
|
-
filename: String(file?.name || '').trim() || `资料 ${index + 1}`,
|
|
59
|
-
progress: 0,
|
|
60
|
-
stage: 'reading',
|
|
61
|
-
error_message: '',
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function readFileWithProgress(file, onProgress) {
|
|
66
|
-
return new Promise((resolve, reject) => {
|
|
67
|
-
const reader = new FileReader();
|
|
68
|
-
reader.onprogress = (event) => {
|
|
69
|
-
if (!event.lengthComputable) {
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
const percent = (event.loaded / event.total) * 80;
|
|
73
|
-
onProgress(percent);
|
|
74
|
-
};
|
|
75
|
-
reader.onload = () => {
|
|
76
|
-
onProgress(80);
|
|
77
|
-
resolve();
|
|
78
|
-
};
|
|
79
|
-
reader.onerror = () => {
|
|
80
|
-
reject(reader.error || new Error('读取文件失败'));
|
|
81
|
-
};
|
|
82
|
-
reader.readAsArrayBuffer(file);
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function useProjectSourceUpload({
|
|
87
|
-
projectId = '',
|
|
88
|
-
userId = '',
|
|
89
|
-
sourceAPI = null,
|
|
90
|
-
shouldLoadSources = true,
|
|
91
|
-
} = {}) {
|
|
92
|
-
const fileInputRef = useRef(null);
|
|
93
|
-
const [sourceItems, setSourceItems] = useState([]);
|
|
94
|
-
const [sourceActionError, setSourceActionError] = useState(null);
|
|
95
|
-
const [sourceUploadLoading, setSourceUploadLoading] = useState(false);
|
|
96
|
-
const [sourceUploadProgressItems, setSourceUploadProgressItems] = useState([]);
|
|
97
|
-
const [sourceSyncLoading, setSourceSyncLoading] = useState(false);
|
|
98
|
-
const [sourceRemovingId, setSourceRemovingId] = useState('');
|
|
99
|
-
const [sourceEndpointUnavailable, setSourceEndpointUnavailable] = useState(false);
|
|
100
|
-
|
|
101
|
-
const canUsePersistentSourceAPI = (
|
|
102
|
-
typeof sourceAPI?.listProjectSources === 'function'
|
|
103
|
-
&& typeof sourceAPI?.uploadSourceFile === 'function'
|
|
104
|
-
&& typeof sourceAPI?.deleteProjectSource === 'function'
|
|
105
|
-
&& !sourceEndpointUnavailable
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
const loadProjectSources = useCallback(async ({ silent = false } = {}) => {
|
|
109
|
-
const scopedProjectId = String(projectId || '').trim();
|
|
110
|
-
if (!canUsePersistentSourceAPI || !scopedProjectId) {
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
if (!silent) {
|
|
114
|
-
setSourceSyncLoading(true);
|
|
115
|
-
setSourceActionError(null);
|
|
116
|
-
}
|
|
117
|
-
try {
|
|
118
|
-
const requestKey = scopedProjectId;
|
|
119
|
-
let pendingRequest = projectSourceListSingleflight.get(requestKey);
|
|
120
|
-
if (!pendingRequest) {
|
|
121
|
-
pendingRequest = sourceAPI.listProjectSources(scopedProjectId)
|
|
122
|
-
.finally(() => {
|
|
123
|
-
projectSourceListSingleflight.delete(requestKey);
|
|
124
|
-
});
|
|
125
|
-
projectSourceListSingleflight.set(requestKey, pendingRequest);
|
|
126
|
-
}
|
|
127
|
-
const response = await pendingRequest;
|
|
128
|
-
setSourceItems(parseSourceList(response));
|
|
129
|
-
setSourceEndpointUnavailable(false);
|
|
130
|
-
} catch (error) {
|
|
131
|
-
if (Number(error?.status) === 404) {
|
|
132
|
-
setSourceEndpointUnavailable(true);
|
|
133
|
-
if (!silent) {
|
|
134
|
-
setSourceActionError(null);
|
|
135
|
-
}
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
if (!silent) {
|
|
139
|
-
setSourceActionError(normalizeError(error, '加载资料失败'));
|
|
140
|
-
}
|
|
141
|
-
} finally {
|
|
142
|
-
if (!silent) {
|
|
143
|
-
setSourceSyncLoading(false);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}, [canUsePersistentSourceAPI, projectId, sourceAPI]);
|
|
147
|
-
|
|
148
|
-
useEffect(() => {
|
|
149
|
-
if (!shouldLoadSources) {
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
loadProjectSources();
|
|
153
|
-
}, [loadProjectSources, shouldLoadSources]);
|
|
154
|
-
|
|
155
|
-
useEffect(() => {
|
|
156
|
-
if (!shouldLoadSources) {
|
|
157
|
-
return undefined;
|
|
158
|
-
}
|
|
159
|
-
if (!canUsePersistentSourceAPI) {
|
|
160
|
-
return undefined;
|
|
161
|
-
}
|
|
162
|
-
if (!hasPendingSourceProcessing(sourceItems)) {
|
|
163
|
-
return undefined;
|
|
164
|
-
}
|
|
165
|
-
const timer = setInterval(() => {
|
|
166
|
-
void loadProjectSources({ silent: true });
|
|
167
|
-
}, 1500);
|
|
168
|
-
return () => {
|
|
169
|
-
clearInterval(timer);
|
|
170
|
-
};
|
|
171
|
-
}, [canUsePersistentSourceAPI, loadProjectSources, shouldLoadSources, sourceItems]);
|
|
172
|
-
|
|
173
|
-
const appendLocalSourceFiles = useCallback((nativeFiles = []) => {
|
|
174
|
-
setSourceItems((prev) => {
|
|
175
|
-
const existingSignatures = new Set(prev.map((item) => buildSourceFileSignature(item)));
|
|
176
|
-
const nextItems = nativeFiles
|
|
177
|
-
.map((file, index) => buildSourceItem(file, index, projectId))
|
|
178
|
-
.filter((item) => {
|
|
179
|
-
const signature = buildSourceFileSignature(item);
|
|
180
|
-
if (existingSignatures.has(signature)) {
|
|
181
|
-
return false;
|
|
182
|
-
}
|
|
183
|
-
existingSignatures.add(signature);
|
|
184
|
-
return true;
|
|
185
|
-
});
|
|
186
|
-
return [...nextItems, ...prev];
|
|
187
|
-
});
|
|
188
|
-
}, [projectId]);
|
|
189
|
-
|
|
190
|
-
const uploadSourceFiles = useCallback(async (nativeFiles = []) => {
|
|
191
|
-
const scopedProjectId = String(projectId || '').trim();
|
|
192
|
-
if (nativeFiles.length === 0) {
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
setSourceUploadLoading(true);
|
|
196
|
-
setSourceActionError(null);
|
|
197
|
-
const progressItems = nativeFiles.map((file, index) => createUploadProgressItem(file, index));
|
|
198
|
-
setSourceUploadProgressItems((prev) => [...progressItems, ...prev]);
|
|
199
|
-
const updateProgressItem = (itemId, patch = {}) => {
|
|
200
|
-
setSourceUploadProgressItems((prev) => prev.map((item) => (item.id === itemId ? { ...item, ...patch } : item)));
|
|
201
|
-
};
|
|
202
|
-
try {
|
|
203
|
-
if (canUsePersistentSourceAPI && scopedProjectId) {
|
|
204
|
-
for (let index = 0; index < nativeFiles.length; index += 1) {
|
|
205
|
-
const file = nativeFiles[index];
|
|
206
|
-
const progressItem = progressItems[index];
|
|
207
|
-
await readFileWithProgress(file, (percent) => {
|
|
208
|
-
updateProgressItem(progressItem.id, {
|
|
209
|
-
stage: 'reading',
|
|
210
|
-
progress: percent,
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
updateProgressItem(progressItem.id, {
|
|
214
|
-
stage: 'uploading',
|
|
215
|
-
progress: 80,
|
|
216
|
-
});
|
|
217
|
-
await sourceAPI.uploadSourceFile({
|
|
218
|
-
project_id: scopedProjectId,
|
|
219
|
-
user_id: String(userId || '').trim() || null,
|
|
220
|
-
file,
|
|
221
|
-
filename: file.name,
|
|
222
|
-
onProgress: (percent) => {
|
|
223
|
-
const safePercent = Number.isFinite(percent) ? percent : 0;
|
|
224
|
-
const mappedPercent = 80 + (Math.max(0, Math.min(100, safePercent)) * 0.2);
|
|
225
|
-
updateProgressItem(progressItem.id, {
|
|
226
|
-
stage: 'uploading',
|
|
227
|
-
progress: mappedPercent,
|
|
228
|
-
});
|
|
229
|
-
},
|
|
230
|
-
});
|
|
231
|
-
updateProgressItem(progressItem.id, {
|
|
232
|
-
stage: 'done',
|
|
233
|
-
progress: 100,
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
await loadProjectSources();
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
for (let index = 0; index < nativeFiles.length; index += 1) {
|
|
240
|
-
const file = nativeFiles[index];
|
|
241
|
-
const progressItem = progressItems[index];
|
|
242
|
-
await readFileWithProgress(file, (percent) => {
|
|
243
|
-
updateProgressItem(progressItem.id, {
|
|
244
|
-
stage: 'reading',
|
|
245
|
-
progress: percent,
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
updateProgressItem(progressItem.id, {
|
|
249
|
-
stage: 'uploading',
|
|
250
|
-
progress: 95,
|
|
251
|
-
});
|
|
252
|
-
appendLocalSourceFiles([file]);
|
|
253
|
-
updateProgressItem(progressItem.id, {
|
|
254
|
-
stage: 'done',
|
|
255
|
-
progress: 100,
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
} catch (error) {
|
|
259
|
-
const errorMessage = String(error?.message || '添加资料失败');
|
|
260
|
-
setSourceUploadProgressItems((prev) => prev.map((item) => (item.stage === 'done'
|
|
261
|
-
? item
|
|
262
|
-
: {
|
|
263
|
-
...item,
|
|
264
|
-
stage: 'error',
|
|
265
|
-
error_message: errorMessage,
|
|
266
|
-
})));
|
|
267
|
-
setSourceActionError(normalizeError(error, '添加资料失败'));
|
|
268
|
-
} finally {
|
|
269
|
-
setSourceUploadLoading(false);
|
|
270
|
-
}
|
|
271
|
-
}, [appendLocalSourceFiles, canUsePersistentSourceAPI, loadProjectSources, projectId, sourceAPI, userId]);
|
|
272
|
-
|
|
273
|
-
const handleOpenSourcePicker = useCallback((nativeFiles = []) => {
|
|
274
|
-
if (sourceUploadLoading) {
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
if (Array.isArray(nativeFiles) && nativeFiles.length > 0) {
|
|
278
|
-
void uploadSourceFiles(nativeFiles);
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
setSourceActionError(null);
|
|
282
|
-
fileInputRef.current?.click();
|
|
283
|
-
}, [sourceUploadLoading, uploadSourceFiles]);
|
|
284
|
-
|
|
285
|
-
const handleSourceFileChange = useCallback((event) => {
|
|
286
|
-
const nativeFiles = Array.from(event?.target?.files || []);
|
|
287
|
-
event.target.value = '';
|
|
288
|
-
void uploadSourceFiles(nativeFiles);
|
|
289
|
-
}, [uploadSourceFiles]);
|
|
290
|
-
|
|
291
|
-
const handleRemoveSource = useCallback(async (sourceId) => {
|
|
292
|
-
const scopedProjectId = String(projectId || '').trim();
|
|
293
|
-
const normalizedSourceId = String(sourceId || '').trim();
|
|
294
|
-
if (!normalizedSourceId) {
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
setSourceActionError(null);
|
|
298
|
-
setSourceRemovingId(normalizedSourceId);
|
|
299
|
-
try {
|
|
300
|
-
if (canUsePersistentSourceAPI && scopedProjectId) {
|
|
301
|
-
await sourceAPI.deleteProjectSource(scopedProjectId, normalizedSourceId);
|
|
302
|
-
await loadProjectSources();
|
|
303
|
-
} else {
|
|
304
|
-
setSourceItems((prev) => prev.filter((item) => {
|
|
305
|
-
const currentSourceId = String(item?.source_id || item?.id || '').trim();
|
|
306
|
-
return currentSourceId !== normalizedSourceId;
|
|
307
|
-
}));
|
|
308
|
-
}
|
|
309
|
-
} catch (error) {
|
|
310
|
-
setSourceActionError(normalizeError(error, '删除资料失败'));
|
|
311
|
-
} finally {
|
|
312
|
-
setSourceRemovingId('');
|
|
313
|
-
}
|
|
314
|
-
}, [canUsePersistentSourceAPI, loadProjectSources, projectId, sourceAPI]);
|
|
315
|
-
|
|
316
|
-
const handleRetrySource = useCallback(() => {
|
|
317
|
-
if (canUsePersistentSourceAPI) {
|
|
318
|
-
loadProjectSources();
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
setSourceActionError(null);
|
|
322
|
-
}, [canUsePersistentSourceAPI, loadProjectSources]);
|
|
323
|
-
|
|
324
|
-
const handleViewSourceDetail = useCallback(() => {}, []);
|
|
325
|
-
|
|
326
|
-
return {
|
|
327
|
-
fileInputRef,
|
|
328
|
-
sourceCapabilityMode: canUsePersistentSourceAPI ? 'persistent' : 'local_fallback',
|
|
329
|
-
sourceActionError,
|
|
330
|
-
sourceItems,
|
|
331
|
-
sourceRemovingId,
|
|
332
|
-
sourceSyncLoading,
|
|
333
|
-
sourceUploadLoading,
|
|
334
|
-
sourceUploadProgressItems,
|
|
335
|
-
handleOpenSourcePicker,
|
|
336
|
-
handleSourceFileChange,
|
|
337
|
-
handleRemoveSource,
|
|
338
|
-
handleRetrySource,
|
|
339
|
-
handleViewSourceDetail,
|
|
340
|
-
};
|
|
341
|
-
}
|