Skip to content

Commit e815fb0

Browse files
author
xiaowei.mao
committed
fix: resolve toggle_commands state handling in starters
Fixed the following issues: 1. When starter.toggle_commands is an empty array, all toggleables (including persistent ones) are now set to inactive, making behavior consistent with commands handling 2. Fixed UI state and backend message synchronization issues, ensuring toggleables sent to backend match UI display 3. Improved state update logic, distinguishing between empty array and undefined toggle_commands 4. Added detailed logging for debugging and tracking state changes Changes: - Optimized toggle_commands handling logic in Starter.tsx - Enhanced state monitoring in ToggleableButtons component - Added debug logs to track toggleables state changes - Fixed synchronization issues between state updates and message sending ```
1 parent 332d51e commit e815fb0

File tree

6 files changed

+196
-46
lines changed

6 files changed

+196
-46
lines changed

backend/chainlit/types.py

+2
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,8 @@ class Starter(DataClassJsonMixin):
257257
label: str
258258
message: str
259259
icon: Optional[str] = None
260+
commands: Optional[List[str]] = None
261+
toggle_commands: Optional[List[str]] = None
260262

261263

262264
@dataclass

frontend/src/components/chat/MessageComposer/ToggleableButtons.tsx

+49-25
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { cn } from '@/lib/utils';
22
import { useRecoilState, useRecoilValue } from 'recoil';
3+
import { useEffect } from 'react';
34

45
import { IToggleCommand, toggleCommandsState } from '@chainlit/react-client';
56

@@ -21,6 +22,17 @@ export const ToggleableButtons = ({ disabled = false }: Props) => {
2122
const toggleCommands = useRecoilValue(toggleCommandsState);
2223
const [toggleables, setToggleables] = useRecoilState(toggleableStates);
2324

25+
// 添加useEffect以监听toggleables的变化
26+
useEffect(() => {
27+
console.log("ToggleableButtons: toggleables状态变化:", toggleables);
28+
29+
// 重新渲染每个按钮以确保UI状态一致
30+
toggleCommands.forEach(cmd => {
31+
const isToggleActive = toggleables.some(t => t.id === cmd.id && t.active);
32+
console.log(`Toggle命令 ${cmd.id} 激活状态更新为:`, isToggleActive);
33+
});
34+
}, [toggleables, toggleCommands]);
35+
2436
if (!toggleCommands.length) return null;
2537

2638
const handleToggle = (command: IToggleCommand) => {
@@ -29,18 +41,21 @@ export const ToggleableButtons = ({ disabled = false }: Props) => {
2941

3042
if (index === -1) {
3143
// 不存在则添加,默认为激活状态
32-
setToggleables([...toggleables, {
44+
const newToggleables = [...toggleables, {
3345
id: command.id,
3446
active: true,
3547
persistent: command.persistent
36-
}]);
48+
}];
49+
console.log("添加新的toggle:", newToggleables);
50+
setToggleables(newToggleables);
3751
} else {
3852
// 存在则切换状态
3953
const newToggleables = [...toggleables];
4054
newToggleables[index] = {
4155
...newToggleables[index],
4256
active: !newToggleables[index].active
4357
};
58+
console.log("切换toggle状态:", newToggleables);
4459
setToggleables(newToggleables);
4560
}
4661
};
@@ -51,32 +66,41 @@ export const ToggleableButtons = ({ disabled = false }: Props) => {
5166
return toggleable ? toggleable.active : false;
5267
};
5368

69+
// DEBUG: 添加控制台日志以查看toggleables状态
70+
console.log("ToggleableButtons渲染时的toggleables状态:", toggleables);
71+
5472
return (
5573
<div className="flex gap-2 ml-1 flex-wrap">
5674
<TooltipProvider>
57-
{toggleCommands.map((command) => (
58-
<Tooltip key={command.id}>
59-
<TooltipTrigger asChild>
60-
<Button
61-
id={`toggleable-${command.id}`}
62-
variant="ghost"
63-
disabled={disabled}
64-
className={cn(
65-
'p-2 h-9 text-[13px] font-medium rounded-full',
66-
isActive(command.id) &&
67-
'border-transparent text-[#08f] hover:text-[#08f] bg-[#DAEEFF] hover:bg-[#BDDCF4] dark:bg-[#2A4A6D] dark:text-[#48AAFF] dark:hover:bg-[#1A416A]'
68-
)}
69-
onClick={() => handleToggle(command)}
70-
>
71-
<Icon name={command.icon} className="!h-5 !w-5" />
72-
{command.id}
73-
</Button>
74-
</TooltipTrigger>
75-
<TooltipContent>
76-
<p>{command.description}</p>
77-
</TooltipContent>
78-
</Tooltip>
79-
))}
75+
{toggleCommands.map((command) => {
76+
const active = isActive(command.id);
77+
// DEBUG: 输出每个命令的状态
78+
console.log(`Toggle命令 ${command.id} 激活状态:`, active);
79+
80+
return (
81+
<Tooltip key={command.id}>
82+
<TooltipTrigger asChild>
83+
<Button
84+
id={`toggleable-${command.id}`}
85+
variant="ghost"
86+
disabled={disabled}
87+
className={cn(
88+
'p-2 h-9 text-[13px] font-medium rounded-full',
89+
active &&
90+
'border-transparent text-[#08f] hover:text-[#08f] bg-[#DAEEFF] hover:bg-[#BDDCF4] dark:bg-[#2A4A6D] dark:text-[#48AAFF] dark:hover:bg-[#1A416A]'
91+
)}
92+
onClick={() => handleToggle(command)}
93+
>
94+
<Icon name={command.icon} className="!h-5 !w-5" />
95+
{command.id}
96+
</Button>
97+
</TooltipTrigger>
98+
<TooltipContent>
99+
<p>{command.description}</p>
100+
</TooltipContent>
101+
</Tooltip>
102+
);
103+
})}
80104
</TooltipProvider>
81105
</div>
82106
);

frontend/src/components/chat/MessageComposer/index.tsx

+13-3
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,14 @@ export default function MessageComposer({
6464

6565
// 在新对话开始时重置toggleables
6666
useEffect(() => {
67+
console.log("MessageComposer useEffect 执行,重置非持久化toggleables");
6768
// 只重置非persistent的toggles
68-
setToggleables(prevToggles => prevToggles.filter(toggle => toggle.persistent));
69-
}, []);
69+
setToggleables(prevToggles => {
70+
const persistentOnly = prevToggles.filter(toggle => toggle.persistent);
71+
console.log("保留的持久化toggleables:", persistentOnly);
72+
return persistentOnly;
73+
});
74+
}, []); // 空依赖数组,只在组件挂载时执行一次
7075

7176
const onPaste = useCallback((event: ClipboardEvent) => {
7277
if (event.clipboardData && event.clipboardData.items) {
@@ -144,7 +149,12 @@ export default function MessageComposer({
144149
} else {
145150
onSubmit(value, attachments, selectedCommand?.id, toggleables);
146151
// 重置非persistent的toggles
147-
setToggleables(prevToggles => prevToggles.filter(toggle => toggle.persistent));
152+
console.log("提交消息后重置非持久化toggleables");
153+
setToggleables(prevToggles => {
154+
const persistentOnly = prevToggles.filter(toggle => toggle.persistent);
155+
console.log("保留的持久化toggleables:", persistentOnly);
156+
return persistentOnly;
157+
});
148158
}
149159
setAttachments([]);
150160
inputRef.current?.reset();

frontend/src/components/chat/Messages/Message/AskActionButtons.tsx

+26-3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
TooltipProvider,
1212
TooltipTrigger
1313
} from '@/components/ui/tooltip';
14+
import { cn } from '@/lib/utils';
1415

1516
const AskActionButton = ({ action }: { action: IAction }) => {
1617
const { loading, askUser } = useContext(MessageContext);
@@ -28,14 +29,30 @@ const AskActionButton = ({ action }: { action: IAction }) => {
2829
return null;
2930
}, [action]);
3031

32+
// 创建自定义样式对象
33+
const customStyle = {};
34+
if (action.bgColor) {
35+
customStyle['backgroundColor'] = action.bgColor;
36+
}
37+
if (action.textColor) {
38+
customStyle['color'] = action.textColor;
39+
}
40+
3141
const button = (
3242
<Button
33-
className="break-words h-auto min-h-10 whitespace-normal"
43+
className={cn(
44+
"break-words h-auto min-h-10 whitespace-normal",
45+
action.fullWidth ? "w-full justify-start" : "",
46+
action.className,
47+
icon ? "gap-2" : ""
48+
)}
3449
id={action.id}
3550
onClick={() => {
3651
askUser?.callback(action);
3752
}}
38-
variant="outline"
53+
variant={action.variant as any || "outline"}
54+
size={action.size as any || "default"}
55+
style={customStyle}
3956
disabled={loading}
4057
>
4158
{icon}
@@ -76,8 +93,14 @@ const AskActionButtons = ({
7693

7794
if (!belongsToMessage || !isAskingAction || !actions.length) return null;
7895

96+
// 检查是否有任何按钮设置了fullWidth属性
97+
const hasFullWidthButtons = filteredActions.some(a => a.fullWidth);
98+
7999
return (
80-
<div className="flex items-center gap-1 flex-wrap">
100+
<div className={cn(
101+
"flex gap-1",
102+
hasFullWidthButtons ? "flex-col w-full" : "items-center flex-wrap"
103+
)}>
81104
{filteredActions.map((a) => (
82105
<AskActionButton key={a.id} action={a} />
83106
))}

frontend/src/components/chat/Starter.tsx

+104-15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useCallback, useContext } from 'react';
2-
import { useRecoilValue } from 'recoil';
2+
import { useRecoilState, useRecoilValue } from 'recoil';
33
import { v4 as uuidv4 } from 'uuid';
44

55
import {
@@ -8,40 +8,125 @@ import {
88
IStep,
99
useAuth,
1010
useChatData,
11-
useChatInteract
11+
useChatInteract,
12+
commandsState,
13+
toggleCommandsState
1214
} from '@chainlit/react-client';
1315

16+
import Icon from '@/components/Icon';
1417
import { Button } from '@/components/ui/button';
1518

16-
import { persistentCommandState } from '@/state/chat';
19+
import { IToggleable, persistentCommandState, toggleableStates } from '@/state/chat';
1720

1821
interface StarterProps {
1922
starter: IStarter;
2023
}
2124

2225
export default function Starter({ starter }: StarterProps) {
2326
const apiClient = useContext(ChainlitContext);
24-
const selectedCommand = useRecoilValue(persistentCommandState);
27+
const [selectedCommand, setSelectedCommand] = useRecoilState(persistentCommandState);
28+
const [toggleables, setToggleables] = useRecoilState(toggleableStates);
2529
const { sendMessage } = useChatInteract();
2630
const { loading, connected } = useChatData();
2731
const { user } = useAuth();
32+
const commands = useRecoilValue(commandsState);
33+
const toggleCommands = useRecoilValue(toggleCommandsState);
2834

2935
const disabled = loading || !connected;
3036

3137
const onSubmit = useCallback(async () => {
38+
console.log("点击Starter,当前toggle状态:", toggleables);
39+
40+
// 处理命令激活
41+
if (starter.commands && starter.commands.length > 0) {
42+
const commandToActivate = commands.find(cmd => cmd.id === starter.commands![0]);
43+
if (commandToActivate) {
44+
setSelectedCommand(commandToActivate);
45+
}
46+
} else {
47+
// 如果starter没有指定commands,则清除当前选中的command
48+
setSelectedCommand(undefined);
49+
}
50+
51+
// 处理可切换命令激活
52+
let newToggleables: IToggleable[] = [];
53+
54+
// 根据starter.toggle_commands处理toggleables
55+
if (starter.toggle_commands) {
56+
if (starter.toggle_commands.length > 0) {
57+
// 如果有指定的toggle_commands,先保留所有持久化的toggleables
58+
const persistentToggles = toggleables.filter(t => t.persistent);
59+
newToggleables.push(...persistentToggles);
60+
61+
// 添加starter中指定的toggle_commands,确保它们被激活
62+
starter.toggle_commands.forEach(cmdId => {
63+
const toggleCmd = toggleCommands.find(cmd => cmd.id === cmdId);
64+
if (toggleCmd) {
65+
// 检查是否已经存在于新的toggleables中
66+
const existingIndex = newToggleables.findIndex(t => t.id === cmdId);
67+
68+
if (existingIndex >= 0) {
69+
// 如果已存在,确保它是激活状态
70+
newToggleables[existingIndex] = {
71+
...newToggleables[existingIndex],
72+
active: true
73+
};
74+
} else {
75+
// 如果不存在,添加新的toggleable
76+
newToggleables.push({
77+
id: toggleCmd.id,
78+
active: true,
79+
persistent: toggleCmd.persistent
80+
});
81+
}
82+
}
83+
});
84+
} else {
85+
// 如果toggle_commands是空数组,保留所有toggleables但设置为非激活状态
86+
newToggleables = toggleables.map(toggle => ({
87+
...toggle,
88+
active: false
89+
}));
90+
console.log("重置所有toggleables为非激活状态:", newToggleables);
91+
}
92+
} else {
93+
// 如果toggle_commands不存在,保留所有持久化的toggleables
94+
const persistentToggles = toggleables.filter(t => t.persistent);
95+
newToggleables.push(...persistentToggles);
96+
}
97+
98+
console.log("即将设置的新toggle状态:", newToggleables);
99+
100+
// 直接构造消息对象并发送,不等待状态更新
32101
const message: IStep = {
33102
threadId: '',
34103
id: uuidv4(),
35-
command: selectedCommand?.id,
104+
command: starter.commands && starter.commands.length > 0 ? starter.commands[0] : undefined,
105+
toggleables: starter.toggle_commands && starter.toggle_commands.length > 0
106+
? starter.toggle_commands
107+
: [],
36108
name: user?.identifier || 'User',
37109
type: 'user_message',
38110
output: starter.message,
39111
createdAt: new Date().toISOString(),
40112
metadata: { location: window.location.href }
41113
};
42114

115+
// 发送消息
43116
sendMessage(message, []);
44-
}, [user, selectedCommand, sendMessage, starter]);
117+
118+
// 然后更新UI状态,这样不会影响消息发送
119+
setToggleables(newToggleables);
120+
121+
}, [user, selectedCommand, toggleables, commands, toggleCommands, sendMessage, starter, setSelectedCommand, setToggleables]);
122+
123+
// 检查是否是图片URL
124+
const isImageUrl = (url: string): boolean => {
125+
return url.startsWith('http') ||
126+
url.startsWith('/') ||
127+
url.startsWith('./') ||
128+
url.startsWith('../');
129+
};
45130

46131
return (
47132
<Button
@@ -53,15 +138,19 @@ export default function Starter({ starter }: StarterProps) {
53138
>
54139
<div className="flex gap-2">
55140
{starter.icon ? (
56-
<img
57-
className="h-5 w-5 rounded-md"
58-
src={
59-
starter.icon?.startsWith('/public')
60-
? apiClient.buildEndpoint(starter.icon)
61-
: starter.icon
62-
}
63-
alt={starter.label}
64-
/>
141+
isImageUrl(starter.icon) ? (
142+
<img
143+
className="h-5 w-5 rounded-md"
144+
src={
145+
starter.icon?.startsWith('/public')
146+
? apiClient.buildEndpoint(starter.icon)
147+
: starter.icon
148+
}
149+
alt={starter.label}
150+
/>
151+
) : (
152+
<Icon name={starter.icon} className="!h-5 !w-5" />
153+
)
65154
) : null}
66155
<p className="text-sm text-muted-foreground truncate">
67156
{starter.label}

libs/react-client/src/types/config.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ export interface IStarter {
22
label: string;
33
message: string;
44
icon?: string;
5+
commands?: string[];
6+
toggle_commands?: string[];
57
}
68

79
export interface ChatProfile {

0 commit comments

Comments
 (0)