Skip to content

Commit 8f16477

Browse files
authored
feat: add and update SDK demos & add useXConversations activeConversationKey (#1252)
* docs: x-sdk * docs: x-sdk * docs: x-sdk * fix: md eror * feat: activeConversationKey * feat: activeConversationKey * feat: activeConversationKey * feat: activeConversationKey * feat: activeConversationKey * feat: activeConversationKey * feat: activeConversationKey * docs: 更新文档
1 parent 9fad224 commit 8f16477

39 files changed

+1146
-182
lines changed

packages/x-markdown/src/XMarkdown/AnimationNode.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ const AnimationText = React.memo<AnimationTextProps>((props) => {
5858
});
5959

6060
const AnimationNode: React.FC<AnimationNodeProps> = (props) => {
61-
const { nodeTag, children, animationConfig, domNode, streamStatus, ...restProps } = props;
61+
const { nodeTag, children, animationConfig, domNode: _, streamStatus: __, ...restProps } = props;
6262

6363
const renderChildren = (): React.ReactNode | React.ReactNode[] => {
6464
if (!children) return null;

packages/x-sdk/src/x-conversations/__test__/index.test.tsx

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,29 @@ describe('useXConversations tests', () => {
2020
React.forwardRef((config: any, ref: any) => {
2121
const {
2222
conversations,
23+
activeConversationKey,
24+
setActiveConversationKey,
2325
addConversation,
2426
removeConversation,
2527
setConversation,
2628
getConversation,
29+
setConversations,
30+
getMessages,
2731
} = useXConversations({
2832
defaultConversations: config.defaultConversations || [],
33+
defaultActiveConversationKey: config.defaultActiveConversationKey,
2934
});
3035

3136
useImperativeHandle(ref, () => ({
3237
conversations,
38+
activeConversationKey,
39+
setActiveConversationKey,
3340
addConversation,
3441
removeConversation,
3542
setConversation,
3643
getConversation,
44+
setConversations,
45+
getMessages,
3746
}));
3847
return (
3948
<>
@@ -42,6 +51,7 @@ describe('useXConversations tests', () => {
4251
<li key={item.key}>{item.label}</li>
4352
))}
4453
</ul>
54+
<div data-testid="active-key">{activeConversationKey}</div>
4555
</>
4656
);
4757
});
@@ -61,6 +71,25 @@ describe('useXConversations tests', () => {
6171
expect(ref.current?.conversations).toEqual(list);
6272
});
6373

74+
it('should init with defaultActiveConversationKey', async () => {
75+
const list: ConversationData[] = [
76+
{
77+
key: '1',
78+
label: 'Chat 1',
79+
},
80+
{
81+
key: '2',
82+
label: 'Chat 2',
83+
},
84+
];
85+
const ref = React.createRef<any>();
86+
const { getByTestId } = render(
87+
<Demo ref={ref} defaultConversations={list} defaultActiveConversationKey="2" />,
88+
);
89+
expect(getByTestId('active-key').textContent).toBe('2');
90+
expect(ref.current?.activeConversationKey).toBe('2');
91+
});
92+
6493
it('should addConversation, setConversation, removeConversation, getConversation work correctly', async () => {
6594
const list: ConversationData[] = [
6695
{
@@ -127,4 +156,118 @@ describe('useXConversations tests', () => {
127156
const conversation = conversationStoreHelper.getConversation('1');
128157
expect(conversation).toEqual(list[0]);
129158
});
159+
160+
it('should set and get activeConversationKey correctly', async () => {
161+
const list: ConversationData[] = [
162+
{ key: '1', label: 'Chat 1' },
163+
{ key: '2', label: 'Chat 2' },
164+
];
165+
const ref = React.createRef<any>();
166+
const { getByTestId } = render(<Demo ref={ref} defaultConversations={list} />);
167+
168+
expect(ref.current?.activeConversationKey).toBe('');
169+
170+
ref.current?.setActiveConversationKey('1');
171+
await sleep(500);
172+
expect(ref.current?.activeConversationKey).toBe('1');
173+
expect(getByTestId('active-key').textContent).toBe('1');
174+
175+
ref.current?.setActiveConversationKey('2');
176+
await sleep(500);
177+
expect(ref.current?.activeConversationKey).toBe('2');
178+
expect(getByTestId('active-key').textContent).toBe('2');
179+
});
180+
181+
it('should addConversation with prepend placement', async () => {
182+
const list: ConversationData[] = [{ key: '1', label: 'Chat 1' }];
183+
const ref = React.createRef<any>();
184+
render(<Demo ref={ref} defaultConversations={list} />);
185+
186+
ref.current?.addConversation({ key: '2', label: 'Chat 2' }, 'prepend');
187+
await sleep(500);
188+
189+
expect(ref.current?.conversations?.length).toEqual(2);
190+
expect(ref.current?.conversations[0].key).toBe('2');
191+
expect(ref.current?.conversations[1].key).toBe('1');
192+
});
193+
194+
it('should addConversation with append placement (default)', async () => {
195+
const list: ConversationData[] = [{ key: '1', label: 'Chat 1' }];
196+
const ref = React.createRef<any>();
197+
render(<Demo ref={ref} defaultConversations={list} />);
198+
199+
ref.current?.addConversation({ key: '2', label: 'Chat 2' }, 'append');
200+
await sleep(500);
201+
202+
expect(ref.current?.conversations?.length).toEqual(2);
203+
expect(ref.current?.conversations[0].key).toBe('1');
204+
expect(ref.current?.conversations[1].key).toBe('2');
205+
});
206+
207+
it('should setConversations replace all conversations', async () => {
208+
const initialList: ConversationData[] = [
209+
{ key: '1', label: 'Chat 1' },
210+
{ key: '2', label: 'Chat 2' },
211+
];
212+
const newList: ConversationData[] = [
213+
{ key: '3', label: 'Chat 3' },
214+
{ key: '4', label: 'Chat 4' },
215+
];
216+
const ref = React.createRef<any>();
217+
render(<Demo ref={ref} defaultConversations={initialList} />);
218+
219+
expect(ref.current?.conversations?.length).toEqual(2);
220+
221+
ref.current?.setConversations(newList);
222+
await sleep(500);
223+
224+
expect(ref.current?.conversations?.length).toEqual(2);
225+
expect(ref.current?.conversations).toEqual(newList);
226+
expect(ref.current?.conversations.some((c: ConversationData) => c.key === '1')).toBe(false);
227+
expect(ref.current?.conversations.some((c: ConversationData) => c.key === '3')).toBe(true);
228+
});
229+
230+
it('should getMessages return messages for conversation', async () => {
231+
const list: ConversationData[] = [{ key: 'test-conversation', label: 'Test Chat' }];
232+
const ref = React.createRef<any>();
233+
render(<Demo ref={ref} defaultConversations={list} />);
234+
235+
const messages = ref.current?.getMessages('test-conversation');
236+
// getMessages should return array or undefined when no chat store exists
237+
expect(messages === undefined || Array.isArray(messages)).toBe(true);
238+
});
239+
240+
it('should not add duplicate conversation', async () => {
241+
const list: ConversationData[] = [{ key: '1', label: 'Chat 1' }];
242+
const ref = React.createRef<any>();
243+
render(<Demo ref={ref} defaultConversations={list} />);
244+
245+
const result = ref.current?.addConversation({ key: '1', label: 'Duplicate Chat' });
246+
await sleep(500);
247+
248+
expect(result).toBe(false);
249+
expect(ref.current?.conversations?.length).toEqual(1);
250+
expect(ref.current?.conversations[0].label).toBe('Chat 1');
251+
});
252+
253+
it('should return false when setting non-existent conversation', async () => {
254+
const list: ConversationData[] = [{ key: '1', label: 'Chat 1' }];
255+
const ref = React.createRef<any>();
256+
render(<Demo ref={ref} defaultConversations={list} />);
257+
258+
const result = ref.current?.setConversation('non-existent', {
259+
key: 'non-existent',
260+
label: 'New Chat',
261+
});
262+
expect(result).toBe(false);
263+
});
264+
265+
it('should return false when removing non-existent conversation', async () => {
266+
const list: ConversationData[] = [{ key: '1', label: 'Chat 1' }];
267+
const ref = React.createRef<any>();
268+
render(<Demo ref={ref} defaultConversations={list} />);
269+
270+
const result = ref.current?.removeConversation('non-existent');
271+
expect(result).toBe(false);
272+
});
130273
});

packages/x-sdk/src/x-conversations/index.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@ export interface ConversationData extends AnyObject {
88

99
interface XConversationConfig {
1010
defaultConversations?: ConversationData[];
11+
defaultActiveConversationKey?: string;
1112
}
1213

1314
export default function useXConversations(config: XConversationConfig) {
1415
const [store] = useState(() => {
15-
const store = new ConversationStore(config?.defaultConversations || []);
16+
const store = new ConversationStore(
17+
config?.defaultConversations || [],
18+
config?.defaultActiveConversationKey || '',
19+
);
1620
return store;
1721
});
1822

@@ -23,9 +27,16 @@ export default function useXConversations(config: XConversationConfig) {
2327
}, []);
2428

2529
const conversations = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
30+
const activeConversationKey = useSyncExternalStore(
31+
store.subscribe,
32+
store.getActiveConversationKey,
33+
store.getActiveConversationKey,
34+
);
2635

2736
return {
2837
conversations,
38+
activeConversationKey: activeConversationKey,
39+
setActiveConversationKey: store.setActiveConversationKey,
2940
addConversation: store.addConversation,
3041
removeConversation: store.removeConversation,
3142
setConversation: store.setConversation,

packages/x-sdk/src/x-conversations/store.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,26 @@ export class ConversationStore {
2828
private conversations: ConversationData[] = [];
2929
private listeners: (() => void)[] = [];
3030
private storeKey: string;
31+
private activeConversationKey: string;
3132

3233
private emitListeners() {
3334
this.listeners.forEach((listener) => {
3435
listener();
3536
});
3637
}
3738

38-
constructor(defaultConversations: ConversationData[]) {
39+
constructor(defaultConversations: ConversationData[], defaultActiveConversationKey: string) {
3940
this.setConversations(defaultConversations);
4041
this.storeKey = Math.random().toString();
4142
conversationStoreHelper.set(this.storeKey, this);
43+
this.activeConversationKey = defaultActiveConversationKey;
4244
}
4345

46+
setActiveConversationKey = (key: string) => {
47+
this.activeConversationKey = key;
48+
this.emitListeners();
49+
return true;
50+
};
4451
setConversations = (list: ConversationData[]) => {
4552
this.conversations = [...list];
4653
this.emitListeners();
@@ -55,9 +62,9 @@ export class ConversationStore {
5562
const exist = this.getConversation(conversation.key);
5663
if (!exist) {
5764
this.setConversations(
58-
placement === 'append'
59-
? [...this.conversations, conversation]
60-
: [conversation, ...this.conversations],
65+
placement === 'prepend'
66+
? [conversation, ...this.conversations]
67+
: [...this.conversations, conversation],
6168
);
6269
return true;
6370
}
@@ -92,6 +99,10 @@ export class ConversationStore {
9299
return this.conversations;
93100
};
94101

102+
getActiveConversationKey = () => {
103+
return this.activeConversationKey;
104+
};
105+
95106
subscribe = (callback: () => void) => {
96107
this.listeners.push(callback);
97108
return () => {

packages/x/.dumi/pages/index/components/SceneIntroduction/Assistant.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const roles: GetProp<typeof Bubble.List, 'role'> = {
3535
background: '#3877FF',
3636
},
3737
},
38-
contentRender(content: any) {
38+
contentRender(content: ChatInput) {
3939
return content?.query;
4040
},
4141
},

packages/x/.dumi/pages/index/components/SceneIntroduction/Independent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const roles: GetProp<typeof Bubble.List, 'role'> = {
3232
background: '#3877FF',
3333
},
3434
},
35-
contentRender(content: any) {
35+
contentRender(content: ChatInput) {
3636
return content?.query;
3737
},
3838
},

packages/x/docs/playground/agent-tbox.tsx

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -555,12 +555,16 @@ const AgentTbox: React.FC = () => {
555555
const locale = isZhCN ? { ...zhCN_antd, ...zhCN_X } : { ...enUS_antd, ...enUS_X };
556556
// ==================== State ====================
557557

558-
const { conversations, addConversation, setConversations } = useXConversations({
558+
const {
559+
conversations,
560+
activeConversationKey,
561+
setActiveConversationKey,
562+
addConversation,
563+
setConversations,
564+
} = useXConversations({
559565
defaultConversations: DEFAULT_CONVERSATIONS_ITEMS,
566+
defaultActiveConversationKey: DEFAULT_CONVERSATIONS_ITEMS[0].key,
560567
});
561-
const [curConversation, setCurConversation] = useState<string>(
562-
DEFAULT_CONVERSATIONS_ITEMS[0].key,
563-
);
564568

565569
const [messageApi, contextHolder] = message.useMessage();
566570

@@ -573,8 +577,8 @@ const AgentTbox: React.FC = () => {
573577
// ==================== Runtime ====================
574578

575579
const { onRequest, messages, isRequesting, abort, onReload } = useXChat({
576-
provider: providerFactory(curConversation), // every conversation has its own provider
577-
conversationKey: curConversation,
580+
provider: providerFactory(activeConversationKey), // every conversation has its own provider
581+
conversationKey: activeConversationKey,
578582
requestPlaceholder: () => {
579583
return {
580584
content: t.noData,
@@ -620,18 +624,16 @@ const AgentTbox: React.FC = () => {
620624
label: `${t.newConversation} ${conversations.length + 1}`,
621625
group: t.today,
622626
});
623-
setCurConversation(now);
627+
setActiveConversationKey(now);
624628
},
625629
}}
626630
items={conversations.map(({ key, label }) => ({
627631
key,
628-
label: key === curConversation ? `[${t.curConversation}]${label}` : label,
632+
label: key === activeConversationKey ? `[${t.curConversation}]${label}` : label,
629633
}))}
630634
className={styles.conversations}
631-
activeKey={curConversation}
632-
onActiveChange={async (val) => {
633-
setCurConversation(val);
634-
}}
635+
activeKey={activeConversationKey}
636+
onActiveChange={setActiveConversationKey}
635637
groupable
636638
styles={{ item: { padding: '0 8px' } }}
637639
menu={(conversation) => ({
@@ -650,8 +652,8 @@ const AgentTbox: React.FC = () => {
650652
const newList = conversations.filter((item) => item.key !== conversation.key);
651653
const newKey = newList?.[0]?.key;
652654
setConversations(newList);
653-
if (conversation.key === curConversation) {
654-
setCurConversation(newKey);
655+
if (conversation.key === activeConversationKey) {
656+
setActiveConversationKey(newKey);
655657
}
656658
},
657659
},

0 commit comments

Comments
 (0)