mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
refactor: extract RoutingRulesEditor component, revert log levels to debug
- Extract ~250 lines of inline routing rules UI from BotForm into a dedicated RoutingRulesEditor component - Revert stage interrupt and event prevented-default log levels from warning back to debug (these are normal flow, not errors) - Remove message content from log lines to avoid leaking user data
This commit is contained in:
@@ -247,8 +247,8 @@ class RuntimePipeline:
|
|||||||
await self._check_output(query, result)
|
await self._check_output(query, result)
|
||||||
|
|
||||||
if result.result_type == pipeline_entities.ResultType.INTERRUPT:
|
if result.result_type == pipeline_entities.ResultType.INTERRUPT:
|
||||||
self.ap.logger.warning(
|
self.ap.logger.debug(
|
||||||
f'Stage {stage_container.inst_name} interrupted query {query.query_id}, message: {str(query.message_chain)[:100]}'
|
f'Stage {stage_container.inst_name} interrupted query {query.query_id}'
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
elif result.result_type == pipeline_entities.ResultType.CONTINUE:
|
elif result.result_type == pipeline_entities.ResultType.CONTINUE:
|
||||||
@@ -263,8 +263,8 @@ class RuntimePipeline:
|
|||||||
await self._check_output(query, sub_result)
|
await self._check_output(query, sub_result)
|
||||||
|
|
||||||
if sub_result.result_type == pipeline_entities.ResultType.INTERRUPT:
|
if sub_result.result_type == pipeline_entities.ResultType.INTERRUPT:
|
||||||
self.ap.logger.warning(
|
self.ap.logger.debug(
|
||||||
f'Stage {stage_container.inst_name} interrupted query {query.query_id}, message: {str(query.message_chain)[:100]}'
|
f'Stage {stage_container.inst_name} interrupted query {query.query_id}'
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
elif sub_result.result_type == pipeline_entities.ResultType.CONTINUE:
|
elif sub_result.result_type == pipeline_entities.ResultType.CONTINUE:
|
||||||
@@ -327,8 +327,8 @@ class RuntimePipeline:
|
|||||||
event_ctx = await self.ap.plugin_connector.emit_event(event_obj, bound_plugins)
|
event_ctx = await self.ap.plugin_connector.emit_event(event_obj, bound_plugins)
|
||||||
|
|
||||||
if event_ctx.is_prevented_default():
|
if event_ctx.is_prevented_default():
|
||||||
self.ap.logger.warning(
|
self.ap.logger.debug(
|
||||||
f'MessageReceived event prevented default for query {query.query_id}, pipeline={pipeline_name}, message: {str(query.message_chain)[:100]}'
|
f'MessageReceived event prevented default for query {query.query_id}, pipeline={pipeline_name}'
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ class ChatMessageHandler(handler.MessageHandler):
|
|||||||
|
|
||||||
yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)
|
yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)
|
||||||
else:
|
else:
|
||||||
self.ap.logger.warning(
|
self.ap.logger.debug(
|
||||||
f'NormalMessageReceived event prevented default for query {query.query_id} without reply, message: {str(query.message_chain)[:100]}'
|
f'NormalMessageReceived event prevented default for query {query.query_id} without reply'
|
||||||
)
|
)
|
||||||
yield entities.StageProcessResult(result_type=entities.ResultType.INTERRUPT, new_query=query)
|
yield entities.StageProcessResult(result_type=entities.ResultType.INTERRUPT, new_query=query)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -13,13 +13,10 @@ import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic';
|
|||||||
import { UUID } from 'uuidjs';
|
import { UUID } from 'uuidjs';
|
||||||
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
||||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
import {
|
import { Bot } from '@/app/infra/entities/api';
|
||||||
Bot,
|
|
||||||
PipelineRoutingRule,
|
|
||||||
RoutingRuleOperator,
|
|
||||||
} from '@/app/infra/entities/api';
|
|
||||||
import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
|
import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
|
||||||
import { ExternalLink, Plus, Trash2 } from 'lucide-react';
|
import { ExternalLink } from 'lucide-react';
|
||||||
|
import RoutingRulesEditor from './RoutingRulesEditor';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
@@ -492,303 +489,10 @@ export default function BotForm({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Pipeline Routing Rules */}
|
{/* Pipeline Routing Rules */}
|
||||||
<div className="mt-6">
|
<RoutingRulesEditor
|
||||||
<div className="flex items-center justify-between mb-2">
|
form={form}
|
||||||
<div>
|
pipelineNameList={pipelineNameList}
|
||||||
<FormLabel>{t('bots.routingRules')}</FormLabel>
|
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
|
||||||
{t('bots.routingRulesDescription')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
const rules =
|
|
||||||
form.getValues('pipeline_routing_rules') || [];
|
|
||||||
form.setValue(
|
|
||||||
'pipeline_routing_rules',
|
|
||||||
[
|
|
||||||
...rules,
|
|
||||||
{
|
|
||||||
type: 'launcher_type' as const,
|
|
||||||
operator: 'eq' as const,
|
|
||||||
value: '',
|
|
||||||
pipeline_uuid: '',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{ shouldDirty: true },
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4 mr-1" />
|
|
||||||
{t('bots.addRoutingRule')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{(form.watch('pipeline_routing_rules') || []).map(
|
|
||||||
(rule, index) => {
|
|
||||||
// Determine which operators are available for the current type
|
|
||||||
const operatorsForType: {
|
|
||||||
value: RoutingRuleOperator;
|
|
||||||
labelKey: string;
|
|
||||||
}[] =
|
|
||||||
rule.type === 'launcher_type'
|
|
||||||
? [
|
|
||||||
{ value: 'eq', labelKey: 'bots.operatorEq' },
|
|
||||||
{ value: 'neq', labelKey: 'bots.operatorNeq' },
|
|
||||||
]
|
|
||||||
: rule.type === 'launcher_id'
|
|
||||||
? [
|
|
||||||
{ value: 'eq', labelKey: 'bots.operatorEq' },
|
|
||||||
{ value: 'neq', labelKey: 'bots.operatorNeq' },
|
|
||||||
{
|
|
||||||
value: 'contains',
|
|
||||||
labelKey: 'bots.operatorContains',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'not_contains',
|
|
||||||
labelKey: 'bots.operatorNotContains',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'regex',
|
|
||||||
labelKey: 'bots.operatorRegex',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
{ value: 'eq', labelKey: 'bots.operatorEq' },
|
|
||||||
{ value: 'neq', labelKey: 'bots.operatorNeq' },
|
|
||||||
{
|
|
||||||
value: 'contains',
|
|
||||||
labelKey: 'bots.operatorContains',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'not_contains',
|
|
||||||
labelKey: 'bots.operatorNotContains',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'starts_with',
|
|
||||||
labelKey: 'bots.operatorStartsWith',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'regex',
|
|
||||||
labelKey: 'bots.operatorRegex',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="flex items-center gap-2 mt-2 p-3 border rounded-md bg-muted/30"
|
|
||||||
>
|
|
||||||
{/* Field selector */}
|
|
||||||
<Select
|
|
||||||
value={rule.type}
|
|
||||||
onValueChange={(val) => {
|
|
||||||
const rules = [
|
|
||||||
...(form.getValues('pipeline_routing_rules') ||
|
|
||||||
[]),
|
|
||||||
];
|
|
||||||
const newType = val as PipelineRoutingRule['type'];
|
|
||||||
// Reset operator to 'eq' when switching type
|
|
||||||
rules[index] = {
|
|
||||||
...rules[index],
|
|
||||||
type: newType,
|
|
||||||
operator: 'eq',
|
|
||||||
value: '',
|
|
||||||
};
|
|
||||||
form.setValue('pipeline_routing_rules', rules, {
|
|
||||||
shouldDirty: true,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-[130px]">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="launcher_type">
|
|
||||||
{t('bots.ruleTypeLauncherType')}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="launcher_id">
|
|
||||||
{t('bots.ruleTypeLauncherId')}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="message_content">
|
|
||||||
{t('bots.ruleTypeMessageContent')}
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
{/* Operator selector */}
|
|
||||||
<Select
|
|
||||||
value={rule.operator || 'eq'}
|
|
||||||
onValueChange={(val) => {
|
|
||||||
const rules = [
|
|
||||||
...(form.getValues('pipeline_routing_rules') ||
|
|
||||||
[]),
|
|
||||||
];
|
|
||||||
rules[index] = {
|
|
||||||
...rules[index],
|
|
||||||
operator: val as RoutingRuleOperator,
|
|
||||||
};
|
|
||||||
form.setValue('pipeline_routing_rules', rules, {
|
|
||||||
shouldDirty: true,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-[120px]">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{operatorsForType.map((op) => (
|
|
||||||
<SelectItem key={op.value} value={op.value}>
|
|
||||||
{t(op.labelKey)}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
{/* Value input */}
|
|
||||||
{rule.type === 'launcher_type' ? (
|
|
||||||
<Select
|
|
||||||
value={rule.value}
|
|
||||||
onValueChange={(val) => {
|
|
||||||
const rules = [
|
|
||||||
...(form.getValues('pipeline_routing_rules') ||
|
|
||||||
[]),
|
|
||||||
];
|
|
||||||
rules[index] = { ...rules[index], value: val };
|
|
||||||
form.setValue('pipeline_routing_rules', rules, {
|
|
||||||
shouldDirty: true,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-[100px]">
|
|
||||||
<SelectValue
|
|
||||||
placeholder={t('bots.ruleValuePlaceholder')}
|
|
||||||
/>
|
/>
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="person">
|
|
||||||
{t('bots.sessionTypePerson')}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="group">
|
|
||||||
{t('bots.sessionTypeGroup')}
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
) : (
|
|
||||||
<Input
|
|
||||||
className="flex-1"
|
|
||||||
placeholder={
|
|
||||||
rule.type === 'launcher_id'
|
|
||||||
? t('bots.ruleValueLauncherIdPlaceholder')
|
|
||||||
: rule.operator === 'regex'
|
|
||||||
? t('bots.ruleValueRegexpPlaceholder')
|
|
||||||
: t('bots.ruleValueMessagePlaceholder')
|
|
||||||
}
|
|
||||||
value={rule.value}
|
|
||||||
onChange={(e) => {
|
|
||||||
const rules = [
|
|
||||||
...(form.getValues('pipeline_routing_rules') ||
|
|
||||||
[]),
|
|
||||||
];
|
|
||||||
rules[index] = {
|
|
||||||
...rules[index],
|
|
||||||
value: e.target.value,
|
|
||||||
};
|
|
||||||
form.setValue('pipeline_routing_rules', rules, {
|
|
||||||
shouldDirty: true,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<span className="text-sm text-muted-foreground shrink-0">
|
|
||||||
→
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* Pipeline selector */}
|
|
||||||
<Select
|
|
||||||
value={rule.pipeline_uuid}
|
|
||||||
onValueChange={(val) => {
|
|
||||||
const rules = [
|
|
||||||
...(form.getValues('pipeline_routing_rules') ||
|
|
||||||
[]),
|
|
||||||
];
|
|
||||||
rules[index] = {
|
|
||||||
...rules[index],
|
|
||||||
pipeline_uuid: val,
|
|
||||||
};
|
|
||||||
form.setValue('pipeline_routing_rules', rules, {
|
|
||||||
shouldDirty: true,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-[200px]">
|
|
||||||
{rule.pipeline_uuid ? (
|
|
||||||
(() => {
|
|
||||||
const p = pipelineNameList.find(
|
|
||||||
(p) => p.value === rule.pipeline_uuid,
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{p?.emoji && (
|
|
||||||
<span className="text-sm shrink-0">
|
|
||||||
{p.emoji}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span>
|
|
||||||
{p?.label ?? rule.pipeline_uuid}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})()
|
|
||||||
) : (
|
|
||||||
<SelectValue
|
|
||||||
placeholder={t('bots.selectPipeline')}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{pipelineNameList.map((item) => (
|
|
||||||
<SelectItem key={item.value} value={item.value}>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{item.emoji && (
|
|
||||||
<span className="text-sm shrink-0">
|
|
||||||
{item.emoji}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span>{item.label}</span>
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="shrink-0"
|
|
||||||
onClick={() => {
|
|
||||||
const rules = [
|
|
||||||
...(form.getValues('pipeline_routing_rules') ||
|
|
||||||
[]),
|
|
||||||
];
|
|
||||||
rules.splice(index, 1);
|
|
||||||
form.setValue('pipeline_routing_rules', rules, {
|
|
||||||
shouldDirty: true,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|||||||
258
web/src/app/home/bots/components/bot-form/RoutingRulesEditor.tsx
Normal file
258
web/src/app/home/bots/components/bot-form/RoutingRulesEditor.tsx
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { UseFormReturn } from 'react-hook-form';
|
||||||
|
import {
|
||||||
|
PipelineRoutingRule,
|
||||||
|
RoutingRuleOperator,
|
||||||
|
} from '@/app/infra/entities/api';
|
||||||
|
import { Plus, Trash2 } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { FormLabel } from '@/components/ui/form';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
|
||||||
|
interface PipelineOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
emoji?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RoutingRulesEditorProps {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
form: UseFormReturn<any>;
|
||||||
|
pipelineNameList: PipelineOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const OPERATORS_BY_TYPE: Record<
|
||||||
|
PipelineRoutingRule['type'],
|
||||||
|
{ value: RoutingRuleOperator; labelKey: string }[]
|
||||||
|
> = {
|
||||||
|
launcher_type: [
|
||||||
|
{ value: 'eq', labelKey: 'bots.operatorEq' },
|
||||||
|
{ value: 'neq', labelKey: 'bots.operatorNeq' },
|
||||||
|
],
|
||||||
|
launcher_id: [
|
||||||
|
{ value: 'eq', labelKey: 'bots.operatorEq' },
|
||||||
|
{ value: 'neq', labelKey: 'bots.operatorNeq' },
|
||||||
|
{ value: 'contains', labelKey: 'bots.operatorContains' },
|
||||||
|
{ value: 'not_contains', labelKey: 'bots.operatorNotContains' },
|
||||||
|
{ value: 'regex', labelKey: 'bots.operatorRegex' },
|
||||||
|
],
|
||||||
|
message_content: [
|
||||||
|
{ value: 'eq', labelKey: 'bots.operatorEq' },
|
||||||
|
{ value: 'neq', labelKey: 'bots.operatorNeq' },
|
||||||
|
{ value: 'contains', labelKey: 'bots.operatorContains' },
|
||||||
|
{ value: 'not_contains', labelKey: 'bots.operatorNotContains' },
|
||||||
|
{ value: 'starts_with', labelKey: 'bots.operatorStartsWith' },
|
||||||
|
{ value: 'regex', labelKey: 'bots.operatorRegex' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
function getValuePlaceholder(
|
||||||
|
t: (key: string) => string,
|
||||||
|
rule: PipelineRoutingRule,
|
||||||
|
): string {
|
||||||
|
if (rule.type === 'launcher_id') return t('bots.ruleValueLauncherIdPlaceholder');
|
||||||
|
if (rule.operator === 'regex') return t('bots.ruleValueRegexpPlaceholder');
|
||||||
|
return t('bots.ruleValueMessagePlaceholder');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RoutingRulesEditor({
|
||||||
|
form,
|
||||||
|
pipelineNameList,
|
||||||
|
}: RoutingRulesEditorProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const rules: PipelineRoutingRule[] =
|
||||||
|
form.watch('pipeline_routing_rules') || [];
|
||||||
|
|
||||||
|
const updateRules = (newRules: PipelineRoutingRule[]) => {
|
||||||
|
form.setValue('pipeline_routing_rules', newRules, { shouldDirty: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const addRule = () => {
|
||||||
|
updateRules([
|
||||||
|
...rules,
|
||||||
|
{
|
||||||
|
type: 'launcher_type',
|
||||||
|
operator: 'eq',
|
||||||
|
value: '',
|
||||||
|
pipeline_uuid: '',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateRule = (index: number, patch: Partial<PipelineRoutingRule>) => {
|
||||||
|
const updated = [...rules];
|
||||||
|
updated[index] = { ...updated[index], ...patch };
|
||||||
|
updateRules(updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeRule = (index: number) => {
|
||||||
|
const updated = [...rules];
|
||||||
|
updated.splice(index, 1);
|
||||||
|
updateRules(updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-6">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div>
|
||||||
|
<FormLabel>{t('bots.routingRules')}</FormLabel>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
{t('bots.routingRulesDescription')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button type="button" variant="outline" size="sm" onClick={addRule}>
|
||||||
|
<Plus className="h-4 w-4 mr-1" />
|
||||||
|
{t('bots.addRoutingRule')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{rules.map((rule, index) => {
|
||||||
|
const operatorsForType = OPERATORS_BY_TYPE[rule.type] || OPERATORS_BY_TYPE.message_content;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center gap-2 mt-2 p-3 border rounded-md bg-muted/30"
|
||||||
|
>
|
||||||
|
{/* Field selector */}
|
||||||
|
<Select
|
||||||
|
value={rule.type}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
updateRule(index, {
|
||||||
|
type: val as PipelineRoutingRule['type'],
|
||||||
|
operator: 'eq',
|
||||||
|
value: '',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[130px]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="launcher_type">
|
||||||
|
{t('bots.ruleTypeLauncherType')}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="launcher_id">
|
||||||
|
{t('bots.ruleTypeLauncherId')}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="message_content">
|
||||||
|
{t('bots.ruleTypeMessageContent')}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{/* Operator selector */}
|
||||||
|
<Select
|
||||||
|
value={rule.operator || 'eq'}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
updateRule(index, { operator: val as RoutingRuleOperator });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[120px]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{operatorsForType.map((op) => (
|
||||||
|
<SelectItem key={op.value} value={op.value}>
|
||||||
|
{t(op.labelKey)}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{/* Value input */}
|
||||||
|
{rule.type === 'launcher_type' ? (
|
||||||
|
<Select
|
||||||
|
value={rule.value}
|
||||||
|
onValueChange={(val) => updateRule(index, { value: val })}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[100px]">
|
||||||
|
<SelectValue
|
||||||
|
placeholder={t('bots.ruleValuePlaceholder')}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="person">
|
||||||
|
{t('bots.sessionTypePerson')}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="group">
|
||||||
|
{t('bots.sessionTypeGroup')}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
className="flex-1"
|
||||||
|
placeholder={getValuePlaceholder(t, rule)}
|
||||||
|
value={rule.value}
|
||||||
|
onChange={(e) => updateRule(index, { value: e.target.value })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span className="text-sm text-muted-foreground shrink-0">→</span>
|
||||||
|
|
||||||
|
{/* Pipeline selector */}
|
||||||
|
<Select
|
||||||
|
value={rule.pipeline_uuid}
|
||||||
|
onValueChange={(val) =>
|
||||||
|
updateRule(index, { pipeline_uuid: val })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[200px]">
|
||||||
|
{rule.pipeline_uuid ? (
|
||||||
|
(() => {
|
||||||
|
const p = pipelineNameList.find(
|
||||||
|
(p) => p.value === rule.pipeline_uuid,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{p?.emoji && (
|
||||||
|
<span className="text-sm shrink-0">{p.emoji}</span>
|
||||||
|
)}
|
||||||
|
<span>{p?.label ?? rule.pipeline_uuid}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()
|
||||||
|
) : (
|
||||||
|
<SelectValue placeholder={t('bots.selectPipeline')} />
|
||||||
|
)}
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{pipelineNameList.map((item) => (
|
||||||
|
<SelectItem key={item.value} value={item.value}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{item.emoji && (
|
||||||
|
<span className="text-sm shrink-0">{item.emoji}</span>
|
||||||
|
)}
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="shrink-0"
|
||||||
|
onClick={() => removeRule(index)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user