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.
- package/README.md +28 -0
- package/docs/CAPABILITY-INVENTORY.md +42 -0
- package/docs/CHAT-CORE-BOUNDARY.md +47 -0
- package/docs/CORE-APP-REALIGNMENT-WORKLOAD-2026-04-18.md +86 -0
- package/docs/SSOT-CHAT-CORE-BOUNDARY.md +73 -0
- package/docs/SSOT-CHAT-CORE-DATAFLOW.md +97 -0
- package/index.html +12 -0
- package/package.json +24 -2
- package/src/adapters/index.js +6 -0
- package/src/adapters/message-api.adapter.js +59 -0
- package/src/adapters/session-api.adapter.js +133 -0
- package/src/adapters/session-message-api.http.js +161 -0
- package/src/adapters/session-message-api.js +34 -0
- package/src/adapters/session-message-api.normalize-source-record.test.mjs +180 -0
- package/src/adapters/session-message-api.normalizers.js +153 -0
- package/src/adapters/source-api.adapter.js +135 -0
- package/src/adapters/sse-client.js +244 -0
- package/src/adapters/sse-event-dispatcher.js +121 -0
- package/src/app/App.jsx +11 -0
- package/src/app/AppProviders.jsx +12 -0
- package/src/app/ChatWorkspaceScreen.jsx +33 -0
- package/src/app/WorkspaceLayout.jsx +125 -0
- package/src/app/components/AppCanvasPanel.jsx +64 -0
- package/src/app/components/TriggerThresholdPopoverContent.jsx +122 -0
- package/src/app/components/WorkspaceBodySection.jsx +109 -0
- package/src/app/components/WorkspaceMainPane.jsx +113 -0
- package/src/app/components/WorkspaceSessionPane.jsx +48 -0
- package/src/app/components/WorkspaceTopBarSection.jsx +65 -0
- package/src/app/core-chat-entry/ComposerSectionNode.jsx +241 -0
- package/src/app/core-chat-entry/attachmentSendRefs.js +154 -0
- package/src/app/core-chat-entry/attachmentSendRefs.test.mjs +101 -0
- package/src/app/core-chat-entry/composerActionRouter.js +26 -0
- package/src/app/core-chat-entry/constants.js +108 -0
- package/src/app/core-chat-entry/selectors.js +28 -0
- package/src/app/core-chat-entry/useAppActionErrorGuards.js +68 -0
- package/src/app/core-chat-entry/useChatCorePipelines.js +110 -0
- package/src/app/core-chat-entry/useComposerModeSuggestion.js +89 -0
- package/src/app/core-chat-entry/useDevCapabilityStatusNote.js +22 -0
- package/src/app/core-chat-entry/useProjectNameEditing.js +41 -0
- package/src/app/core-chat-entry/useProjectSourceUpload.js +341 -0
- package/src/app/core-chat-entry/useRealApiReadinessGate.js +103 -0
- package/src/app/core-chat-entry/useUnavailableActionError.js +29 -0
- package/src/app/core-chat-entry/useWorkspaceCanvasController.jsx +177 -0
- package/src/app/core-chat-entry/useWorkspaceCanvasProjection.jsx +171 -0
- package/src/app/core-chat-entry/useWorkspaceComposerController.jsx +199 -0
- package/src/app/core-chat-entry/useWorkspaceController.jsx +226 -0
- package/src/app/core-chat-entry/useWorkspacePanels.js +55 -0
- package/src/app/hooks/useComposerAttachmentSync.js +223 -0
- package/src/app/hooks/useComposerChooserHandlers.js +52 -0
- package/src/app/hooks/useSendWithContextRefs.js +140 -0
- package/src/app/hooks/useSendWithContextRefs.test.mjs +29 -0
- package/src/app/hooks/useUserThresholdProfile.js +121 -0
- package/src/app/index.js +1 -0
- package/src/app/selectors/assistantTextSelector.js +73 -0
- package/src/app/selectors/canvasEvidenceSummarySelector.js +28 -0
- package/src/app/selectors/canvasReportTemplateSelector.js +28 -0
- package/src/app/selectors/canvasTabsSelector.js +58 -0
- package/src/app/selectors/evidenceProjectionSelector.js +175 -0
- package/src/app/selectors/evidenceProjectionSelector.test.mjs +107 -0
- package/src/app/selectors/modeSuggestionSelector.js +50 -0
- package/src/chat-core/app/mockRuntime.js +291 -0
- package/src/chat-core/app/useAppStream.js +187 -0
- package/src/chat-core/app/useAppStream.refs.test.mjs +44 -0
- package/src/chat-core/app/useAppStream.request-body.test.mjs +116 -0
- package/src/chat-core/app/useCoreChatApp.js +115 -0
- package/src/chat-core/facade/useBasicConversationFacade.js +280 -0
- package/src/chat-core/index.js +9 -1
- package/src/chat-core/messages/buildTimelineItems.analysis-route.test.mjs +36 -0
- package/src/chat-core/messages/buildTimelineItems.js +170 -12
- package/src/chat-core/messages/buildTimelineItems.knowledge-citation.test.mjs +183 -0
- package/src/chat-core/messages/contextUsageDefaults.js +3 -0
- package/src/chat-core/messages/contextUsageViewModel.js +147 -0
- package/src/chat-core/messages/contextUsageViewModel.test.mjs +74 -0
- package/src/chat-core/messages/useContextUsageViewModel.js +41 -0
- package/src/chat-core/orchestration/useBasicSendHandler.js +55 -0
- package/src/chat-core/pipelines/build-action-request.js +46 -0
- package/src/chat-core/pipelines/build-stream-request.js +74 -0
- package/src/chat-core/pipelines/entity-extraction.js +159 -0
- package/src/chat-core/pipelines/preprocess-message.js +16 -0
- package/src/chat-core/pipelines/stream-persist-utils.js +32 -0
- package/src/chat-core/pipelines/transport/send-mock-stream.js +86 -0
- package/src/chat-core/pipelines/transport/send-real-stream.js +330 -0
- package/src/chat-core/pipelines/transport/send-real-stream.test.mjs +27 -0
- package/src/chat-core/pipelines/transport/send-sourcing-search.js +86 -0
- package/src/chat-core/pipelines/transport/send-sourcing-search.test.mjs +14 -0
- package/src/chat-core/pipelines/transport/sourcing-response-templates.js +55 -0
- package/src/chat-core/pipelines/transport/sourcing-search-api.js +155 -0
- package/src/chat-core/runtime/runtimeMode.js +69 -0
- package/src/chat-core/session/chatSessionActionTypes.js +24 -0
- package/src/chat-core/session/chatSessionReducer.js +352 -0
- package/src/chat-core/session/chatSessionReducer.streaming-done.test.mjs +39 -0
- package/src/chat-core/session/index.js +2 -0
- package/src/chat-core/session/sessionActionsMessages.js +44 -0
- package/src/chat-core/session/sessionActionsSessionCrud.js +131 -0
- package/src/chat-core/session/sessionActionsStreaming.js +80 -0
- package/src/chat-core/session/sessionActionsUiState.js +51 -0
- package/src/chat-core/session/useChatSessionReducer.js +62 -455
- package/src/chat-core/session/useSessionListController.js +67 -0
- package/src/chat-core/stream/sse-client.js +1 -244
- package/src/chat-core/stream/sse-event-dispatcher.js +1 -0
- package/src/chat-core/stream/sse-events.js +1 -867
- package/src/chat-core/stream/useSSEStream.js +1 -356
- package/src/chat-core/stream/useStreamSendController.js +46 -0
- package/src/contracts/context-ssot.js +47 -0
- package/src/contracts/index.js +1 -0
- package/src/contracts/sse-events/base-parsers.js +79 -0
- package/src/contracts/sse-events/domain-parsers.js +3 -0
- package/src/contracts/sse-events/internal-normalizers.js +143 -0
- package/src/contracts/sse-events/parsers-intake.js +235 -0
- package/src/contracts/sse-events/parsers-runtime.js +37 -0
- package/src/contracts/sse-events/parsers-sourcing.js +179 -0
- package/src/contracts/sse-events/patch-event-parser.js +121 -0
- package/src/contracts/sse-events/runtime-parsers.js +79 -0
- package/src/contracts/sse-events.js +4 -0
- package/src/index.js +5 -0
- package/src/main.jsx +28 -0
- package/src/orchestration/index.js +6 -0
- package/src/orchestration/useSSEStream.js +221 -0
- package/src/state/index.js +4 -0
- package/vite.config.js +36 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
function normalizeSourceId(item) {
|
|
2
|
+
if (typeof item === 'string') {
|
|
3
|
+
return String(item).trim();
|
|
4
|
+
}
|
|
5
|
+
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
6
|
+
return String(item.source_id || item.id || '').trim();
|
|
7
|
+
}
|
|
8
|
+
return '';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function mergeSourceReferenceLists(...lists) {
|
|
12
|
+
const refs = [];
|
|
13
|
+
const seen = new Set();
|
|
14
|
+
lists.forEach((list) => {
|
|
15
|
+
if (!Array.isArray(list)) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
list.forEach((item) => {
|
|
19
|
+
const sourceId = normalizeSourceId(item);
|
|
20
|
+
if (!sourceId || seen.has(sourceId)) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
seen.add(sourceId);
|
|
24
|
+
refs.push({ source_id: sourceId });
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
return refs;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function collectSourceIdStrings(...lists) {
|
|
31
|
+
const ids = [];
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
lists.forEach((list) => {
|
|
34
|
+
if (!Array.isArray(list)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
list.forEach((item) => {
|
|
38
|
+
const sourceId = normalizeSourceId(item);
|
|
39
|
+
if (!sourceId || seen.has(sourceId)) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
seen.add(sourceId);
|
|
43
|
+
ids.push(sourceId);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
return ids;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeAttachmentStatus(item) {
|
|
50
|
+
return String(item?.status || '').trim().toLowerCase();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function collectReadyAttachmentRefs(fileList = []) {
|
|
54
|
+
if (!Array.isArray(fileList)) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
const readyRefs = fileList.filter((item) => {
|
|
58
|
+
const sourceId = normalizeSourceId(item);
|
|
59
|
+
if (!sourceId) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return normalizeAttachmentStatus(item) === 'ready';
|
|
63
|
+
});
|
|
64
|
+
return mergeSourceReferenceLists(readyRefs);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function hasNonReadyComposerAttachments(fileList = []) {
|
|
68
|
+
if (!Array.isArray(fileList)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return fileList.some((item) => {
|
|
72
|
+
const sourceId = normalizeSourceId(item);
|
|
73
|
+
const status = normalizeAttachmentStatus(item);
|
|
74
|
+
return Boolean(!sourceId || status !== 'ready');
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function buildAttachmentSnapshot(fileList = []) {
|
|
79
|
+
if (!Array.isArray(fileList)) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
return fileList.map((item) => ({ ...item }));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolveUploadableFile(item) {
|
|
86
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const sourceId = String(item.source_id || '').trim();
|
|
90
|
+
if (sourceId) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const status = String(item.status || '').trim().toLowerCase();
|
|
94
|
+
if (status === 'failed') {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return item.originFileObj || null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function prepareComposerAttachmentSendRefs({
|
|
101
|
+
fileList = [],
|
|
102
|
+
uploadSourceFile,
|
|
103
|
+
projectId = '',
|
|
104
|
+
sessionId = '',
|
|
105
|
+
} = {}) {
|
|
106
|
+
const attachedSessionFilesSnapshot = buildAttachmentSnapshot(fileList);
|
|
107
|
+
const resolvedProjectId = String(projectId || '').trim();
|
|
108
|
+
const canUpload = typeof uploadSourceFile === 'function' && Boolean(resolvedProjectId);
|
|
109
|
+
|
|
110
|
+
if (canUpload) {
|
|
111
|
+
await Promise.all(attachedSessionFilesSnapshot.map(async (attachment, index) => {
|
|
112
|
+
const uploadableFile = resolveUploadableFile(attachment);
|
|
113
|
+
if (!uploadableFile) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const uploaded = await uploadSourceFile({
|
|
118
|
+
file: uploadableFile,
|
|
119
|
+
filename: attachment?.name || attachment?.filename || uploadableFile?.name || 'upload.bin',
|
|
120
|
+
project_id: resolvedProjectId,
|
|
121
|
+
session_id: String(sessionId || '').trim() || null,
|
|
122
|
+
});
|
|
123
|
+
const sourceId = String(uploaded?.source_id || uploaded?.id || '').trim();
|
|
124
|
+
if (!sourceId) {
|
|
125
|
+
attachedSessionFilesSnapshot[index] = {
|
|
126
|
+
...attachment,
|
|
127
|
+
status: 'failed',
|
|
128
|
+
error_message: '上传成功但未返回 source_id',
|
|
129
|
+
};
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
attachedSessionFilesSnapshot[index] = {
|
|
133
|
+
...attachment,
|
|
134
|
+
...uploaded,
|
|
135
|
+
source_id: sourceId,
|
|
136
|
+
status: String(uploaded?.status || attachment?.status || 'ready'),
|
|
137
|
+
error_message: String(uploaded?.error_message || ''),
|
|
138
|
+
};
|
|
139
|
+
} catch (error) {
|
|
140
|
+
attachedSessionFilesSnapshot[index] = {
|
|
141
|
+
...attachment,
|
|
142
|
+
status: 'failed',
|
|
143
|
+
error_message: String(error?.message || error || '上传失败'),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const refs = mergeSourceReferenceLists(attachedSessionFilesSnapshot.filter((item) => String(item?.status || '').trim().toLowerCase() !== 'failed'));
|
|
150
|
+
return {
|
|
151
|
+
attachedSessionFilesSnapshot,
|
|
152
|
+
refs,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {
|
|
4
|
+
collectSourceIdStrings,
|
|
5
|
+
collectReadyAttachmentRefs,
|
|
6
|
+
hasNonReadyComposerAttachments,
|
|
7
|
+
mergeSourceReferenceLists,
|
|
8
|
+
prepareComposerAttachmentSendRefs,
|
|
9
|
+
} from './attachmentSendRefs.js';
|
|
10
|
+
|
|
11
|
+
test('mergeSourceReferenceLists keeps unique refs from strings and objects', () => {
|
|
12
|
+
const refs = mergeSourceReferenceLists(
|
|
13
|
+
['src-1', '', 'src-1'],
|
|
14
|
+
[{ source_id: 'src-2' }, { id: 'src-3' }, null],
|
|
15
|
+
);
|
|
16
|
+
assert.deepEqual(refs, [
|
|
17
|
+
{ source_id: 'src-1' },
|
|
18
|
+
{ source_id: 'src-2' },
|
|
19
|
+
{ source_id: 'src-3' },
|
|
20
|
+
]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('collectSourceIdStrings keeps unique source ids from mixed refs', () => {
|
|
24
|
+
const ids = collectSourceIdStrings(
|
|
25
|
+
['src-1', { source_id: 'src-2' }, { id: 'src-3' }],
|
|
26
|
+
[{ source_id: 'src-1' }, '', null, { source_id: '' }],
|
|
27
|
+
);
|
|
28
|
+
assert.deepEqual(ids, ['src-1', 'src-2', 'src-3']);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('prepareComposerAttachmentSendRefs uploads pending files and skips failed refs', async () => {
|
|
32
|
+
const uploaded = [];
|
|
33
|
+
const payload = await prepareComposerAttachmentSendRefs({
|
|
34
|
+
fileList: [
|
|
35
|
+
{
|
|
36
|
+
uid: 'f1',
|
|
37
|
+
name: 'a.docx',
|
|
38
|
+
originFileObj: { name: 'a.docx' },
|
|
39
|
+
status: 'ready',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
uid: 'f2',
|
|
43
|
+
name: 'b.pdf',
|
|
44
|
+
originFileObj: { name: 'b.pdf' },
|
|
45
|
+
status: 'failed',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
uid: 'f3',
|
|
49
|
+
name: 'c.txt',
|
|
50
|
+
source_id: 'src-existing',
|
|
51
|
+
originFileObj: { name: 'c.txt' },
|
|
52
|
+
status: 'ready',
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
uploadSourceFile: async (input) => {
|
|
56
|
+
uploaded.push(input.filename);
|
|
57
|
+
return { source_id: 'src-uploaded' };
|
|
58
|
+
},
|
|
59
|
+
projectId: 'project-1',
|
|
60
|
+
sessionId: 'session-1',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
assert.deepEqual(uploaded, ['a.docx']);
|
|
64
|
+
assert.deepEqual(payload.refs, [
|
|
65
|
+
{ source_id: 'src-uploaded' },
|
|
66
|
+
{ source_id: 'src-existing' },
|
|
67
|
+
]);
|
|
68
|
+
assert.equal(payload.attachedSessionFilesSnapshot[0].source_id, 'src-uploaded');
|
|
69
|
+
assert.equal(payload.attachedSessionFilesSnapshot[1].status, 'failed');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('collectReadyAttachmentRefs includes only ready source_id entries', () => {
|
|
73
|
+
const refs = collectReadyAttachmentRefs([
|
|
74
|
+
{ source_id: 'src-ready-1', status: 'ready' },
|
|
75
|
+
{ source_id: 'src-uploaded', status: 'uploaded' },
|
|
76
|
+
{ source_id: 'src-ready-2', status: 'ready' },
|
|
77
|
+
{ source_id: 'src-failed', status: 'failed' },
|
|
78
|
+
{ source_id: '', status: 'ready' },
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
assert.deepEqual(refs, [
|
|
82
|
+
{ source_id: 'src-ready-1' },
|
|
83
|
+
{ source_id: 'src-ready-2' },
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('hasNonReadyComposerAttachments blocks when any file is not ready', () => {
|
|
88
|
+
assert.equal(hasNonReadyComposerAttachments([
|
|
89
|
+
{ source_id: 'src-1', status: 'ready' },
|
|
90
|
+
{ source_id: 'src-2', status: 'ready' },
|
|
91
|
+
]), false);
|
|
92
|
+
|
|
93
|
+
assert.equal(hasNonReadyComposerAttachments([
|
|
94
|
+
{ source_id: 'src-1', status: 'ready' },
|
|
95
|
+
{ source_id: 'src-2', status: 'indexing' },
|
|
96
|
+
]), true);
|
|
97
|
+
|
|
98
|
+
assert.equal(hasNonReadyComposerAttachments([
|
|
99
|
+
{ source_id: '', status: 'ready' },
|
|
100
|
+
]), true);
|
|
101
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
function toText(value) {
|
|
2
|
+
return String(value || '').trim();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function buildComposerDraftRoundKey({
|
|
6
|
+
activeSessionId,
|
|
7
|
+
currentInput,
|
|
8
|
+
}) {
|
|
9
|
+
const sessionPart = toText(activeSessionId) || 'draft';
|
|
10
|
+
const inputPart = toText(currentInput);
|
|
11
|
+
return `${sessionPart}:${inputPart}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveComposerActionIntent(choice) {
|
|
15
|
+
const actionPayload = choice?.action && typeof choice.action === 'object'
|
|
16
|
+
? choice.action
|
|
17
|
+
: null;
|
|
18
|
+
const actionKey = toText(actionPayload?.action_key);
|
|
19
|
+
if (actionKey === 'open_sourcing') {
|
|
20
|
+
return { kind: 'open_sourcing' };
|
|
21
|
+
}
|
|
22
|
+
if (actionKey === 'dismiss_sourcing_once') {
|
|
23
|
+
return { kind: 'dismiss_sourcing_once' };
|
|
24
|
+
}
|
|
25
|
+
return { kind: 'noop' };
|
|
26
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export const WORKSPACE_CONTENT_MAX_WIDTH = 980;
|
|
2
|
+
|
|
3
|
+
export const RESOLVED_UI_LABELS = {
|
|
4
|
+
tab_chats: '会话',
|
|
5
|
+
tab_sources: '资料',
|
|
6
|
+
edit_project_button: '编辑',
|
|
7
|
+
sidebar_operations_title: '常规操作',
|
|
8
|
+
project_select_hint: '请从左侧项目列表选择一个项目。',
|
|
9
|
+
project_create_hint: '先创建一个项目,然后开始会话。',
|
|
10
|
+
project_list_empty: '还没有项目,点击“创建项目”开始。',
|
|
11
|
+
new_project_button: '创建项目',
|
|
12
|
+
new_session_button: '新会话',
|
|
13
|
+
project_sort_by_name: '按名称',
|
|
14
|
+
project_sort_by_updated: '按最近更新',
|
|
15
|
+
empty_state_title: '欢迎使用 FLARE',
|
|
16
|
+
empty_state_description: '开始一个新对话。',
|
|
17
|
+
scenario_expand_label: '查看更多',
|
|
18
|
+
scenario_collapse_label: '收起',
|
|
19
|
+
composer_attach_label: '上传文件',
|
|
20
|
+
composer_hint: '描述你的目标、背景或约束,系统会先帮你判断下一步。',
|
|
21
|
+
canvas_workspace_title: '需求梳理工作区',
|
|
22
|
+
sources_add_button: '添加资料',
|
|
23
|
+
sources_loading: '资料加载中...',
|
|
24
|
+
sources_empty_description: '上传到项目资料池后,后续该项目下的对话和分析都可以复用这些资料。',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const STARTER_SCENARIOS = [
|
|
28
|
+
{
|
|
29
|
+
key: 'starter-1',
|
|
30
|
+
label: '帮我梳理一个需求',
|
|
31
|
+
description: '先补齐目标、约束和验收标准。',
|
|
32
|
+
prompt: '帮我梳理一个需求',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: 'starter-2',
|
|
36
|
+
label: '帮我做方案对比',
|
|
37
|
+
description: '从关键维度对比候选方案。',
|
|
38
|
+
prompt: '帮我做方案对比',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
key: 'starter-3',
|
|
42
|
+
label: '帮我整理执行计划',
|
|
43
|
+
description: '拆分阶段任务并明确里程碑。',
|
|
44
|
+
prompt: '帮我整理执行计划',
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
export const MODE_SWITCH_OPTIONS = [
|
|
49
|
+
{ value: 'requirement_canvas', label: 'Plan 模式' },
|
|
50
|
+
{ value: 'intelligent_sourcing', label: '寻源模式' },
|
|
51
|
+
{ value: 'analysis_mode', label: '分析模式' },
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
export const MODE_SUGGESTION_TIP_RULES = [
|
|
55
|
+
{
|
|
56
|
+
id: 'analysis',
|
|
57
|
+
modeKey: 'analysis_mode',
|
|
58
|
+
label: '分析模式',
|
|
59
|
+
reason: '识别到分析或模板诉求,建议开启分析模式。',
|
|
60
|
+
actionText: '开始需求分析',
|
|
61
|
+
keywords: ['分析', '模板', '模版', '清单模板', '报告'],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'sourcing',
|
|
65
|
+
modeKey: 'intelligent_sourcing',
|
|
66
|
+
label: '寻源模式',
|
|
67
|
+
reason: '识别到找供应商/检索诉求,建议开启寻源模式。',
|
|
68
|
+
actionText: '启用寻源模式',
|
|
69
|
+
keywords: ['找', '寻源', '搜索', '检索', '供应商', '比价'],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'plan',
|
|
73
|
+
modeKey: 'requirement_canvas',
|
|
74
|
+
label: 'Plan 模式',
|
|
75
|
+
reason: '识别到需求规划/整理诉求,建议开启 Plan 模式。',
|
|
76
|
+
actionText: '开始需求梳理',
|
|
77
|
+
keywords: ['规划', '整理', '梳理', '需求', '采购', '收口'],
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
export const RESPONSE_STYLE_POLICY = {
|
|
82
|
+
friendlyPrefix: '好的,我们先把关键信息理清楚:',
|
|
83
|
+
questionReplyHint: '你可以直接回复序号或补充信息,我来继续整理。',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export function createWorkspaceStyle(themeTokens) {
|
|
87
|
+
return {
|
|
88
|
+
'--flare-accent': themeTokens.bubbleUserBg,
|
|
89
|
+
'--flare-accent-hover': themeTokens.surfaceBorderActive,
|
|
90
|
+
'--flare-focus-outline': themeTokens.focusOutline,
|
|
91
|
+
'--flare-font-body': themeTokens.fontFamilyBody,
|
|
92
|
+
'--flare-font-heading': themeTokens.fontFamilyHeading,
|
|
93
|
+
'--flare-hover-bg': themeTokens.surfaceBgActive,
|
|
94
|
+
'--flare-scrollbar-thumb': themeTokens.divider,
|
|
95
|
+
'--flare-surface-bg-active': themeTokens.surfaceBgActive,
|
|
96
|
+
'--flare-surface-border-active': themeTokens.surfaceBorderActive,
|
|
97
|
+
'--flare-text-primary': themeTokens.textPrimary,
|
|
98
|
+
'--flare-text-secondary': themeTokens.textSecondary,
|
|
99
|
+
background: themeTokens.appBg,
|
|
100
|
+
boxShadow: themeTokens.appShadow,
|
|
101
|
+
display: 'grid',
|
|
102
|
+
gridTemplateColumns: '300px minmax(0, 1fr)',
|
|
103
|
+
gridTemplateRows: 'minmax(0, 1fr)',
|
|
104
|
+
height: '100vh',
|
|
105
|
+
overflow: 'hidden',
|
|
106
|
+
position: 'relative',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function getSessionId(item) {
|
|
2
|
+
return String(item?.sessionId || item?.session_id || item?.id || '').trim();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function selectActiveSession(sessions, activeSessionId) {
|
|
6
|
+
return sessions.find((item) => {
|
|
7
|
+
const sessionId = getSessionId(item);
|
|
8
|
+
return Boolean(sessionId && sessionId === activeSessionId);
|
|
9
|
+
}) || null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function selectProjectItems({
|
|
13
|
+
projectId,
|
|
14
|
+
projectDisplayName,
|
|
15
|
+
sessions,
|
|
16
|
+
}) {
|
|
17
|
+
const sessionItems = Array.isArray(sessions) ? sessions : [];
|
|
18
|
+
if (sessionItems.length <= 0) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
return [{
|
|
22
|
+
key: 'local_demo_project',
|
|
23
|
+
project_id: projectId,
|
|
24
|
+
name: projectDisplayName,
|
|
25
|
+
updatedAt: sessionItems[0]?.updatedAt || sessionItems[0]?.updated_at || new Date().toISOString(),
|
|
26
|
+
createdAt: sessionItems[0]?.createdAt || sessionItems[0]?.created_at || new Date().toISOString(),
|
|
27
|
+
}];
|
|
28
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
}
|