Skip to content

Commit 7325359

Browse files
intervention system
1 parent 33a26e9 commit 7325359

34 files changed

Lines changed: 5126 additions & 604 deletions

migration/MIGRATION_PLAN.md

Lines changed: 0 additions & 591 deletions
This file was deleted.

packages/aipex-react/src/components/chatbot/components/message-list.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ export function DefaultMessageList({
5454
{/* Loading indicator */}
5555
{status === "submitted" &&
5656
(slots.loadingIndicator ? slots.loadingIndicator() : <Loader />)}
57+
{/* After messages slot - for platform-specific content */}
58+
{slots.afterMessages?.()}
5759
</ConversationContent>
5860
<ConversationScrollButton />
5961
</Conversation>
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/**
2+
* Intervention Card
3+
*
4+
* Modern intervention card container
5+
*/
6+
7+
import {
8+
AlertCircleIcon,
9+
CheckCircleIcon,
10+
ClockIcon,
11+
XIcon,
12+
} from "lucide-react";
13+
import type React from "react";
14+
import { useTranslation } from "../../i18n/hooks.js";
15+
import type { TranslationKey } from "../../i18n/types.js";
16+
import { Button } from "../ui/button.js";
17+
18+
export type InterventionStatus =
19+
| "pending"
20+
| "active"
21+
| "completed"
22+
| "cancelled"
23+
| "timeout"
24+
| "error";
25+
26+
interface InterventionCardProps {
27+
status: InterventionStatus;
28+
title: string;
29+
reason?: string;
30+
timeout?: number;
31+
onCancel?: () => void;
32+
children?: React.ReactNode;
33+
}
34+
35+
const STATUS_BASE_CONFIG = {
36+
pending: {
37+
icon: ClockIcon,
38+
accentColor: "bg-amber-400",
39+
iconBg: "bg-amber-50",
40+
iconColor: "text-amber-600",
41+
badgeBg: "bg-amber-100",
42+
badgeText: "text-amber-700",
43+
},
44+
active: {
45+
icon: ClockIcon,
46+
accentColor: "bg-blue-400",
47+
iconBg: "bg-blue-50",
48+
iconColor: "text-blue-600",
49+
badgeBg: "bg-blue-100",
50+
badgeText: "text-blue-700",
51+
},
52+
completed: {
53+
icon: CheckCircleIcon,
54+
accentColor: "bg-emerald-400",
55+
iconBg: "bg-emerald-50",
56+
iconColor: "text-emerald-600",
57+
badgeBg: "bg-emerald-100",
58+
badgeText: "text-emerald-700",
59+
},
60+
cancelled: {
61+
icon: XIcon,
62+
accentColor: "bg-gray-400",
63+
iconBg: "bg-gray-50",
64+
iconColor: "text-gray-600",
65+
badgeBg: "bg-gray-100",
66+
badgeText: "text-gray-700",
67+
},
68+
timeout: {
69+
icon: AlertCircleIcon,
70+
accentColor: "bg-orange-400",
71+
iconBg: "bg-orange-50",
72+
iconColor: "text-orange-600",
73+
badgeBg: "bg-orange-100",
74+
badgeText: "text-orange-700",
75+
},
76+
error: {
77+
icon: AlertCircleIcon,
78+
accentColor: "bg-rose-400",
79+
iconBg: "bg-rose-50",
80+
iconColor: "text-rose-600",
81+
badgeBg: "bg-rose-100",
82+
badgeText: "text-rose-700",
83+
},
84+
};
85+
86+
export const InterventionCard: React.FC<InterventionCardProps> = ({
87+
status,
88+
title,
89+
reason,
90+
timeout,
91+
onCancel,
92+
children,
93+
}) => {
94+
const { t } = useTranslation();
95+
const config = STATUS_BASE_CONFIG[status];
96+
const Icon = config.icon;
97+
const isActive = status === "active" || status === "pending";
98+
99+
const getStatusLabel = () => {
100+
return t(`interventions.status.${status}` as TranslationKey);
101+
};
102+
103+
return (
104+
<div
105+
className={`
106+
relative overflow-hidden rounded-2xl
107+
bg-white dark:bg-gray-900
108+
border border-gray-200 dark:border-gray-800
109+
shadow-sm hover:shadow-md transition-all duration-200
110+
${isActive ? "animate-in slide-in-from-bottom-4" : ""}
111+
`}
112+
>
113+
{/* Colorful accent bar */}
114+
<div className={`h-1 ${config.accentColor}`} />
115+
116+
{/* Header */}
117+
<div className="px-5 py-4 border-b border-gray-100 dark:border-gray-800">
118+
<div className="flex items-start justify-between">
119+
<div className="flex items-center gap-3 flex-1">
120+
<div className={`p-2 rounded-xl ${config.iconBg}`}>
121+
<Icon className={`w-5 h-5 ${config.iconColor}`} />
122+
</div>
123+
<div className="flex-1">
124+
<div className="flex items-center gap-2 flex-wrap">
125+
<h3 className="text-base font-semibold text-gray-900 dark:text-gray-100">
126+
{title}
127+
</h3>
128+
<span
129+
className={`
130+
text-xs font-medium px-2.5 py-1 rounded-full
131+
${config.badgeBg} ${config.badgeText}
132+
`}
133+
>
134+
{getStatusLabel()}
135+
</span>
136+
</div>
137+
{reason && (
138+
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1.5 leading-relaxed">
139+
{reason}
140+
</p>
141+
)}
142+
</div>
143+
</div>
144+
{isActive && onCancel && (
145+
<Button
146+
variant="ghost"
147+
size="sm"
148+
onClick={onCancel}
149+
className="ml-2 h-8 w-8 p-0 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
150+
>
151+
<XIcon className="w-4 h-4" />
152+
</Button>
153+
)}
154+
</div>
155+
</div>
156+
157+
{/* Content */}
158+
<div className="px-5 py-4 bg-gray-50/50 dark:bg-gray-900/50">
159+
{children}
160+
</div>
161+
162+
{/* Footer - Timeout indicator */}
163+
{isActive && timeout && (
164+
<div className="px-5 py-3 bg-gray-50 dark:bg-gray-900 border-t border-gray-100 dark:border-gray-800">
165+
<div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
166+
<ClockIcon className="w-3.5 h-3.5" />
167+
<span>
168+
{t("interventions.common.timeoutLabel" as TranslationKey)}:{" "}
169+
{timeout}{" "}
170+
{t("interventions.common.timeoutUnit" as TranslationKey)}
171+
</span>
172+
</div>
173+
</div>
174+
)}
175+
</div>
176+
);
177+
};
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/**
2+
* Intervention Mode Toggle
3+
*
4+
* Intervention mode switch button at the top of conversation
5+
*
6+
* Features:
7+
* - Switch intervention mode (disabled/passive)
8+
* - Save to current conversation state
9+
* - Clear visual indication
10+
* - Cancel all ongoing interventions when switching to disabled
11+
*/
12+
13+
import type React from "react";
14+
import { useTranslation } from "../../i18n/hooks.js";
15+
import type { TranslationKey } from "../../i18n/types.js";
16+
import { Button } from "../ui/button.js";
17+
import {
18+
DropdownMenu,
19+
DropdownMenuContent,
20+
DropdownMenuItem,
21+
DropdownMenuTrigger,
22+
} from "../ui/dropdown-menu.js";
23+
24+
export type InterventionMode = "disabled" | "passive";
25+
26+
interface InterventionModeToggleProps {
27+
mode: InterventionMode;
28+
onChange: (mode: InterventionMode) => void;
29+
className?: string;
30+
}
31+
32+
const MODE_BASE_CONFIG = {
33+
disabled: {
34+
icon: "🚫",
35+
color: "text-gray-600",
36+
bgColor: "bg-gray-100 hover:bg-gray-200",
37+
},
38+
passive: {
39+
icon: "🤝",
40+
color: "text-blue-600",
41+
bgColor: "bg-blue-100 hover:bg-blue-200",
42+
},
43+
};
44+
45+
export const InterventionModeToggle: React.FC<InterventionModeToggleProps> = ({
46+
mode,
47+
onChange,
48+
className = "",
49+
}) => {
50+
const { t } = useTranslation();
51+
const currentBaseConfig = MODE_BASE_CONFIG[mode];
52+
53+
const handleModeChange = (newMode: InterventionMode) => {
54+
if (newMode === mode) return;
55+
56+
// Notify parent component (which will handle intervention manager update)
57+
onChange(newMode);
58+
};
59+
60+
const getModeLabel = (modeKey: InterventionMode) => {
61+
return t(`interventions.mode.${modeKey}` as TranslationKey);
62+
};
63+
64+
const getModeDescription = (modeKey: InterventionMode) => {
65+
return t(`interventions.mode.${modeKey}Description` as TranslationKey);
66+
};
67+
68+
return (
69+
<DropdownMenu>
70+
<DropdownMenuTrigger asChild>
71+
<Button
72+
variant="outline"
73+
size="sm"
74+
className={`${currentBaseConfig.bgColor} ${currentBaseConfig.color} border-none ${className}`}
75+
>
76+
<span className="mr-1">{currentBaseConfig.icon}</span>
77+
<span className="font-medium">{getModeLabel(mode)}</span>
78+
<svg
79+
className="ml-1 w-4 h-4"
80+
fill="none"
81+
stroke="currentColor"
82+
viewBox="0 0 24 24"
83+
aria-hidden="true"
84+
>
85+
<path
86+
strokeLinecap="round"
87+
strokeLinejoin="round"
88+
strokeWidth={2}
89+
d="M19 9l-7 7-7-7"
90+
/>
91+
</svg>
92+
</Button>
93+
</DropdownMenuTrigger>
94+
<DropdownMenuContent align="end" className="w-56">
95+
{(Object.keys(MODE_BASE_CONFIG) as InterventionMode[]).map(
96+
(modeKey) => {
97+
const baseConfig = MODE_BASE_CONFIG[modeKey];
98+
const isSelected = modeKey === mode;
99+
100+
return (
101+
<DropdownMenuItem
102+
key={modeKey}
103+
onClick={() => handleModeChange(modeKey)}
104+
className={`cursor-pointer ${isSelected ? "bg-gray-100" : ""}`}
105+
>
106+
<div className="flex items-start gap-2 py-1">
107+
<span className="text-lg mt-0.5">{baseConfig.icon}</span>
108+
<div className="flex-1">
109+
<div className="flex items-center gap-2">
110+
<span className="font-medium text-sm">
111+
{getModeLabel(modeKey)}
112+
</span>
113+
{isSelected && (
114+
<span className="text-xs text-blue-600"></span>
115+
)}
116+
</div>
117+
<span className="text-xs text-gray-500">
118+
{getModeDescription(modeKey)}
119+
</span>
120+
</div>
121+
</div>
122+
</DropdownMenuItem>
123+
);
124+
},
125+
)}
126+
</DropdownMenuContent>
127+
</DropdownMenu>
128+
);
129+
};

0 commit comments

Comments
 (0)