flare-chat-core 0.2.2 → 0.2.3

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "flare-chat-core",
3
3
  "private": false,
4
- "version": "0.2.2",
4
+ "version": "0.2.3",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite --mode development",
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import { Drawer } from 'antd';
2
3
  import {
3
4
  WORKSPACE_CONTENT_MAX_WIDTH,
4
5
  RESOLVED_UI_LABELS,
@@ -7,6 +8,8 @@ import {
7
8
  import WorkspaceSessionPane from './components/WorkspaceSessionPane.jsx';
8
9
  import WorkspaceMainPane from './components/WorkspaceMainPane.jsx';
9
10
 
11
+ const COMPACT_LAYOUT_QUERY = '(max-width: 767px)';
12
+
10
13
  export default function WorkspaceLayout({
11
14
  themeTokens,
12
15
  viewModel,
@@ -52,22 +55,58 @@ export default function WorkspaceLayout({
52
55
  sourceFileInputRef,
53
56
  handleSourceFileChange,
54
57
  }) {
58
+ const [isCompactLayout, setIsCompactLayout] = React.useState(false);
59
+ const [sidebarDrawerOpen, setSidebarDrawerOpen] = React.useState(false);
60
+
61
+ React.useEffect(() => {
62
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
63
+ return undefined;
64
+ }
65
+
66
+ const mediaQuery = window.matchMedia(COMPACT_LAYOUT_QUERY);
67
+ const handleChange = (event) => {
68
+ const nextCompact = Boolean(event.matches);
69
+ setIsCompactLayout(nextCompact);
70
+ if (!nextCompact) {
71
+ setSidebarDrawerOpen(false);
72
+ }
73
+ };
74
+
75
+ setIsCompactLayout(mediaQuery.matches);
76
+
77
+ if (typeof mediaQuery.addEventListener === 'function') {
78
+ mediaQuery.addEventListener('change', handleChange);
79
+ return () => mediaQuery.removeEventListener('change', handleChange);
80
+ }
81
+
82
+ mediaQuery.addListener(handleChange);
83
+ return () => mediaQuery.removeListener(handleChange);
84
+ }, []);
85
+
86
+ const showInlineSidebar = !isCompactLayout;
87
+ const workspaceStyle = {
88
+ ...createWorkspaceStyle(themeTokens),
89
+ gridTemplateColumns: showInlineSidebar ? '300px minmax(0, 1fr)' : 'minmax(0, 1fr)',
90
+ };
91
+
55
92
  return (
56
93
  <div
57
94
  className="flare-chat-workspace"
58
- style={createWorkspaceStyle(themeTokens)}
95
+ style={workspaceStyle}
59
96
  >
60
- <WorkspaceSessionPane
61
- themeTokens={themeTokens}
62
- viewModel={viewModel}
63
- actionGuards={actionGuards}
64
- apiReadinessGate={apiReadinessGate}
65
- hasProject={hasProject}
66
- projectItems={projectItems}
67
- projectSlot={projectSlot}
68
- sessions={sessions}
69
- resolvedUILabels={RESOLVED_UI_LABELS}
70
- />
97
+ {showInlineSidebar ? (
98
+ <WorkspaceSessionPane
99
+ themeTokens={themeTokens}
100
+ viewModel={viewModel}
101
+ actionGuards={actionGuards}
102
+ apiReadinessGate={apiReadinessGate}
103
+ hasProject={hasProject}
104
+ projectItems={projectItems}
105
+ projectSlot={projectSlot}
106
+ sessions={sessions}
107
+ resolvedUILabels={RESOLVED_UI_LABELS}
108
+ />
109
+ ) : null}
71
110
 
72
111
  <WorkspaceMainPane
73
112
  themeTokens={themeTokens}
@@ -111,8 +150,34 @@ export default function WorkspaceLayout({
111
150
  showAllScenarios={showAllScenarios}
112
151
  setShowAllScenarios={setShowAllScenarios}
113
152
  visibleScenarios={visibleScenarios}
153
+ isCompactLayout={isCompactLayout}
154
+ showSidebarMenuButton={isCompactLayout}
155
+ handleOpenSidebarMenu={() => setSidebarDrawerOpen(true)}
114
156
  />
115
157
 
158
+ <Drawer
159
+ onClose={() => setSidebarDrawerOpen(false)}
160
+ open={isCompactLayout && sidebarDrawerOpen}
161
+ placement="left"
162
+ styles={{
163
+ body: { padding: 0 },
164
+ wrapper: { width: '86vw' },
165
+ }}
166
+ >
167
+ <WorkspaceSessionPane
168
+ themeTokens={themeTokens}
169
+ viewModel={viewModel}
170
+ actionGuards={actionGuards}
171
+ apiReadinessGate={apiReadinessGate}
172
+ hasProject={hasProject}
173
+ projectItems={projectItems}
174
+ projectSlot={projectSlot}
175
+ sessions={sessions}
176
+ resolvedUILabels={RESOLVED_UI_LABELS}
177
+ onAfterSidebarAction={() => setSidebarDrawerOpen(false)}
178
+ />
179
+ </Drawer>
180
+
116
181
  <input
117
182
  ref={sourceFileInputRef}
118
183
  type="file"
@@ -8,6 +8,7 @@ import {
8
8
  export default function WorkspaceBodySection({
9
9
  hasProject,
10
10
  activeWorkspaceTab,
11
+ setActiveWorkspaceTab,
11
12
  themeTokens,
12
13
  projectItems,
13
14
  resolvedUILabels,
@@ -36,11 +37,51 @@ export default function WorkspaceBodySection({
36
37
  onToggleShowAllScenarios,
37
38
  visibleScenarios,
38
39
  onScenarioCardClick,
40
+ isCompactLayout,
39
41
  }) {
42
+ const tabItems = [
43
+ { label: resolvedUILabels.tab_chats || '会话', value: 'chats' },
44
+ { label: resolvedUILabels.tab_sources || '资料', value: 'sources' },
45
+ ];
46
+ const activeTabIndex = Math.max(
47
+ 0,
48
+ tabItems.findIndex((item) => item.value === activeWorkspaceTab),
49
+ );
50
+
51
+ const compactTabSwitchNode = (hasProject && isCompactLayout) ? (
52
+ <div style={{ padding: '8px 12px 6px' }}>
53
+ <div
54
+ className="flare-workspace-tab-switch"
55
+ role="tablist"
56
+ aria-label="workspace tabs"
57
+ >
58
+ <span
59
+ className="flare-workspace-tab-switch-thumb"
60
+ style={{ transform: `translateX(${activeTabIndex * 100}%)` }}
61
+ />
62
+ {tabItems.map((item) => {
63
+ const active = item.value === activeWorkspaceTab;
64
+ return (
65
+ <button
66
+ key={item.value}
67
+ aria-selected={active}
68
+ className={`flare-workspace-tab-switch-item${active ? ' is-active' : ''}`}
69
+ onClick={() => setActiveWorkspaceTab(item.value)}
70
+ role="tab"
71
+ type="button"
72
+ >
73
+ {item.label}
74
+ </button>
75
+ );
76
+ })}
77
+ </div>
78
+ </div>
79
+ ) : null;
80
+
40
81
  if (!hasProject) {
41
82
  return (
42
83
  <ChatWorkspaceProjectInitView
43
- isCompactLayout={false}
84
+ isCompactLayout={isCompactLayout}
44
85
  themeTokens={themeTokens}
45
86
  resolvedProductName="F.L.A.R.E"
46
87
  resolvedProductTag="项目协同工作台"
@@ -53,57 +94,63 @@ export default function WorkspaceBodySection({
53
94
 
54
95
  if (activeWorkspaceTab === 'sources') {
55
96
  return (
56
- <ChatWorkspaceSourcesPanel
57
- composerNode={composerNode}
58
- contentMaxWidth={contentMaxWidth}
59
- handleOpenSourcePicker={handleOpenSourcePicker}
60
- handleRemoveSource={handleRemoveSource}
61
- handleRetrySource={handleRetrySource}
62
- handleViewSourceDetail={handleViewSourceDetail}
63
- isCompactLayout={false}
64
- resolvedUILabels={resolvedUILabels}
65
- sourceActionError={sourceActionError}
66
- sourceItems={sourceItems}
67
- sourceRemovingId={sourceRemovingId}
68
- sourceSyncLoading={sourceSyncLoading}
69
- sourceUploadLoading={sourceUploadLoading}
70
- themeTokens={themeTokens}
71
- />
97
+ <>
98
+ {compactTabSwitchNode}
99
+ <ChatWorkspaceSourcesPanel
100
+ composerNode={composerNode}
101
+ contentMaxWidth={contentMaxWidth}
102
+ handleOpenSourcePicker={handleOpenSourcePicker}
103
+ handleRemoveSource={handleRemoveSource}
104
+ handleRetrySource={handleRetrySource}
105
+ handleViewSourceDetail={handleViewSourceDetail}
106
+ isCompactLayout={isCompactLayout}
107
+ resolvedUILabels={resolvedUILabels}
108
+ sourceActionError={sourceActionError}
109
+ sourceItems={sourceItems}
110
+ sourceRemovingId={sourceRemovingId}
111
+ sourceSyncLoading={sourceSyncLoading}
112
+ sourceUploadLoading={sourceUploadLoading}
113
+ themeTokens={themeTokens}
114
+ />
115
+ </>
72
116
  );
73
117
  }
74
118
 
75
119
  return (
76
- <ChatWorkspaceConversationPane
77
- themeTokens={themeTokens}
78
- isCompactLayout={false}
79
- resolvedActiveModeKey="auto"
80
- canvasWorkspaceFullscreenMode={false}
81
- showCanvasPanel={showCanvasPanel}
82
- canvasPanelNode={canvasPanelNode}
83
- composerNode={composerNode}
84
- contentMaxWidth={contentMaxWidth}
85
- streamError={streamError}
86
- streamLoading={streamLoading}
87
- onStreamRetry={onStreamRetry}
88
- renderedTimeline={{
89
- isEmpty: renderedTimelineItems.length === 0,
90
- items: renderedTimelineItems,
91
- }}
92
- onUICardAction={onUICardAction}
93
- registry={registry}
94
- resolvedUILabels={resolvedUILabels}
95
- showWorkspaceDebugPanel={false}
96
- resolvedDevMode="full_effects"
97
- workspaceEventLog={[]}
98
- workspaceDocSnapshot={{}}
99
- shouldRenderRecommendationPanel={renderedTimelineItems.length === 0}
100
- currentInputValue={currentInputValue}
101
- recommendationPanelTitle="起步入口"
102
- canExpandScenarios
103
- showAllScenarios={showAllScenarios}
104
- onToggleShowAllScenarios={onToggleShowAllScenarios}
105
- visibleScenarios={visibleScenarios}
106
- onScenarioCardClick={onScenarioCardClick}
107
- />
120
+ <>
121
+ {compactTabSwitchNode}
122
+ <ChatWorkspaceConversationPane
123
+ themeTokens={themeTokens}
124
+ isCompactLayout={isCompactLayout}
125
+ resolvedActiveModeKey="auto"
126
+ canvasWorkspaceFullscreenMode={false}
127
+ showCanvasPanel={showCanvasPanel}
128
+ canvasPanelNode={canvasPanelNode}
129
+ composerNode={composerNode}
130
+ contentMaxWidth={contentMaxWidth}
131
+ streamError={streamError}
132
+ streamLoading={streamLoading}
133
+ onStreamRetry={onStreamRetry}
134
+ renderedTimeline={{
135
+ isEmpty: renderedTimelineItems.length === 0,
136
+ items: renderedTimelineItems,
137
+ }}
138
+ onUICardAction={onUICardAction}
139
+ registry={registry}
140
+ resolvedUILabels={resolvedUILabels}
141
+ showWorkspaceDebugPanel={false}
142
+ resolvedDevMode="full_effects"
143
+ workspaceEventLog={[]}
144
+ workspaceDocSnapshot={{}}
145
+ shouldRenderRecommendationPanel={renderedTimelineItems.length === 0}
146
+ currentInputValue={currentInputValue}
147
+ recommendationPanelTitle="起步入口"
148
+ canExpandScenarios
149
+ showAllScenarios={showAllScenarios}
150
+ onToggleShowAllScenarios={onToggleShowAllScenarios}
151
+ visibleScenarios={visibleScenarios}
152
+ onScenarioCardClick={onScenarioCardClick}
153
+ />
154
+ </>
108
155
  );
109
156
  }
@@ -44,6 +44,9 @@ export default function WorkspaceMainPane({
44
44
  showAllScenarios,
45
45
  setShowAllScenarios,
46
46
  visibleScenarios,
47
+ isCompactLayout,
48
+ showSidebarMenuButton,
49
+ handleOpenSidebarMenu,
47
50
  }) {
48
51
  const resolvedSessionDigestText = activeSession
49
52
  ? `当前会话:${String(activeSession.preview || activeSession.title || '').trim()}`
@@ -73,12 +76,16 @@ export default function WorkspaceMainPane({
73
76
  handleOpenKnowledgeHub={handleOpenKnowledgeHub}
74
77
  showCanvasPanel={showCanvasPanel}
75
78
  handleToggleWorkspacePanel={handleToggleWorkspacePanel}
79
+ isCompactLayout={isCompactLayout}
80
+ showSidebarMenuButton={showSidebarMenuButton}
81
+ handleOpenSidebarMenu={handleOpenSidebarMenu}
76
82
  />
77
83
  ) : null}
78
84
 
79
85
  <WorkspaceBodySection
80
86
  hasProject={hasProject}
81
87
  activeWorkspaceTab={activeWorkspaceTab}
88
+ setActiveWorkspaceTab={setActiveWorkspaceTab}
82
89
  themeTokens={themeTokens}
83
90
  projectItems={projectItems}
84
91
  resolvedUILabels={resolvedUILabels}
@@ -107,6 +114,7 @@ export default function WorkspaceMainPane({
107
114
  onToggleShowAllScenarios={() => setShowAllScenarios((prev) => !prev)}
108
115
  visibleScenarios={visibleScenarios}
109
116
  onScenarioCardClick={(entry) => viewModel.composer.onChange({ target: { value: entry.prompt } })}
117
+ isCompactLayout={isCompactLayout}
110
118
  />
111
119
  </div>
112
120
  );
@@ -11,19 +11,41 @@ export default function WorkspaceSessionPane({
11
11
  projectSlot,
12
12
  sessions,
13
13
  resolvedUILabels,
14
+ onAfterSidebarAction,
14
15
  }) {
16
+ const handleAfterAction = () => {
17
+ if (typeof onAfterSidebarAction === 'function') {
18
+ onAfterSidebarAction();
19
+ }
20
+ };
21
+
15
22
  return (
16
23
  <div style={{ minWidth: 0 }}>
17
24
  <SessionListPane
18
25
  activeSessionId={viewModel.sessionList.activeSessionId}
19
26
  error={actionGuards.sessionPaneError}
20
27
  loading={viewModel.timeline.loading || apiReadinessGate.checking}
21
- onCreateProject={actionGuards.handleCreateProject}
22
- onCreateSession={actionGuards.handleCreateSession}
23
- onNewSession={actionGuards.handleCreateSession}
28
+ onCreateProject={() => {
29
+ actionGuards.handleCreateProject?.();
30
+ handleAfterAction();
31
+ }}
32
+ onCreateSession={(project) => {
33
+ actionGuards.handleCreateSession?.(project);
34
+ handleAfterAction();
35
+ }}
36
+ onNewSession={(project) => {
37
+ actionGuards.handleCreateSession?.(project);
38
+ handleAfterAction();
39
+ }}
24
40
  onOperationClick={() => {}}
25
- onProjectSelect={() => {}}
26
- onSelectSession={viewModel.sessionList.onSelectSession}
41
+ onProjectSelect={(project) => {
42
+ viewModel.sessionList.onProjectSelect?.(project);
43
+ handleAfterAction();
44
+ }}
45
+ onSelectSession={(sessionId) => {
46
+ viewModel.sessionList.onSelectSession?.(sessionId);
47
+ handleAfterAction();
48
+ }}
27
49
  onRenameSession={viewModel.sessionList.onRenameSession}
28
50
  onArchiveSession={viewModel.sessionList.onArchiveSession}
29
51
  operations={[]}
@@ -26,6 +26,9 @@ export default function WorkspaceTopBarSection({
26
26
  handleOpenKnowledgeHub,
27
27
  showCanvasPanel,
28
28
  handleToggleWorkspacePanel,
29
+ isCompactLayout,
30
+ showSidebarMenuButton,
31
+ handleOpenSidebarMenu,
29
32
  }) {
30
33
  return (
31
34
  <ChatWorkspaceTopBar
@@ -38,7 +41,7 @@ export default function WorkspaceTopBarSection({
38
41
  handleProjectNameCancel={handleProjectNameCancel}
39
42
  resolvedProjectDisplayName={resolvedProjectDisplayName}
40
43
  handleProjectNameStartEdit={handleProjectNameStartEdit}
41
- isCompactLayout={false}
44
+ isCompactLayout={isCompactLayout}
42
45
  activeWorkspaceTab={activeWorkspaceTab}
43
46
  setActiveWorkspaceTab={setActiveWorkspaceTab}
44
47
  segmentedThemeStyle={{}}
@@ -60,6 +63,9 @@ export default function WorkspaceTopBarSection({
60
63
  hasUnseenWorkspaceUpdate={false}
61
64
  showCanvasPanel={showCanvasPanel}
62
65
  handleToggleWorkspacePanel={handleToggleWorkspacePanel}
66
+ showSidebarMenuButton={showSidebarMenuButton}
67
+ handleOpenSidebarMenu={handleOpenSidebarMenu}
68
+ showWorkspaceTabSwitch={!isCompactLayout}
63
69
  />
64
70
  );
65
71
  }
@@ -36,15 +36,6 @@ export function buildTimelineItems(state = {}, options = {}) {
36
36
  const loading = Boolean(options?.loading);
37
37
 
38
38
  const items = [];
39
- let latestAssistantMessageId = '';
40
- for (let index = messages.length - 1; index >= 0; index -= 1) {
41
- const candidate = messages[index];
42
- if (String(candidate?.role || '').trim() === 'assistant') {
43
- latestAssistantMessageId = String(candidate?.message_id || `msg-fallback-${index}`).trim();
44
- break;
45
- }
46
- }
47
-
48
39
  if (messages.length === 0) {
49
40
  return {
50
41
  isEmpty: true,
@@ -89,29 +80,6 @@ export function buildTimelineItems(state = {}, options = {}) {
89
80
  });
90
81
 
91
82
  if (msg.role === 'assistant') {
92
- const historicalThinking = Boolean(
93
- msg.agent_status
94
- || msg.execution_trace
95
- || String(msg.thinking_trace || '').trim()
96
- );
97
- if (historicalThinking && messageId === latestAssistantMessageId) {
98
- items.push({
99
- id: `thinking-${messageId}`,
100
- type: 'thinking',
101
- agentStatus: msg.agent_status || {
102
- status: 'completed',
103
- agent: '助手',
104
- },
105
- thinkingTrace: String(msg.thinking_trace || '').trim(),
106
- executionTrace: msg.execution_trace || {
107
- status: 'completed',
108
- step_id: 'historical_trace',
109
- },
110
- executionCards: [],
111
- roundKey: String(msg.round_key || authoritativeRoundId || '').trim(),
112
- loading: false,
113
- });
114
- }
115
83
  if (msg.knowledge_search && typeof msg.knowledge_search === 'object') {
116
84
  items.push({
117
85
  id: `knowledge-search-${messageId}`,
@@ -144,7 +144,7 @@ test('buildTimelineItems keeps executionCards on thinking item for step-by-step
144
144
  assert.equal(thinkingItem.executionCards[0].step_id, 'context_binding');
145
145
  });
146
146
 
147
- test('buildTimelineItems keeps only latest assistant historical thinking item', () => {
147
+ test('buildTimelineItems does not append historical assistant thinking item', () => {
148
148
  const timeline = buildTimelineItems({
149
149
  messages: [
150
150
  {
@@ -178,6 +178,5 @@ test('buildTimelineItems keeps only latest assistant historical thinking item',
178
178
  });
179
179
 
180
180
  const thinkingItems = timeline.items.filter((item) => item.type === 'thinking');
181
- assert.equal(thinkingItems.length, 1);
182
- assert.equal(thinkingItems[0].id, 'thinking-a2');
181
+ assert.equal(thinkingItems.length, 0);
183
182
  });