mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-04 08:37:14 +00:00
feat(wizard): rework agent onboarding flow (#2471)
* feat(wizard): rework agent onboarding flow * fix(web): support LAN development access * fix(wizard): parse ranked model selection entries * feat(wizard): add inbound bot verification * feat(wizard): add floating page bot verification * fix(wizard): repair HTTP bot inbound test setup * feat(wizard): streamline custom model onboarding * feat(wizard): label page bot test preview * style(space): apply ruff formatting * fix(wizard): polish AI engine onboarding * fix(wizard): clarify local account message test * feat(wizard): animate AI engine transitions * fix(wizard): align AI engine setup headers --------- Co-authored-by: langbot-dev <langbot@users.noreply.github.com> Co-authored-by: RockChinQ <rockchinq@gmail.com>
This commit is contained in:
@@ -20,6 +20,7 @@ export function BotLogListComponent({
|
||||
autoExpandImages = false,
|
||||
hideDetailedLogsLink = false,
|
||||
hideToolbar = false,
|
||||
onMessageReceived,
|
||||
}: {
|
||||
botId: string;
|
||||
/** When true, log entries with images are rendered expanded by default */
|
||||
@@ -28,6 +29,8 @@ export function BotLogListComponent({
|
||||
hideDetailedLogsLink?: boolean;
|
||||
/** When true, hides the entire toolbar (auto-refresh, level filter, detailed logs link) */
|
||||
hideToolbar?: boolean;
|
||||
/** Called after an inbound person/group message appears in the bot log. */
|
||||
onMessageReceived?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
@@ -41,6 +44,8 @@ export function BotLogListComponent({
|
||||
]);
|
||||
const listContainerRef = useRef<HTMLDivElement>(null);
|
||||
const botLogListRef = useRef<BotLog[]>(botLogList);
|
||||
const onMessageReceivedRef = useRef(onMessageReceived);
|
||||
onMessageReceivedRef.current = onMessageReceived;
|
||||
|
||||
const logLevels = [
|
||||
{ value: 'error', label: 'ERROR' },
|
||||
@@ -108,6 +113,9 @@ export function BotLogListComponent({
|
||||
manager.subscribeLogPush(handleBotLogPush);
|
||||
manager.loadFirstPage().then((response) => {
|
||||
setBotLogList(response.reverse());
|
||||
if (response.some((log) => Boolean(log.message_session_id))) {
|
||||
onMessageReceivedRef.current?.();
|
||||
}
|
||||
});
|
||||
listenScroll();
|
||||
}
|
||||
@@ -138,6 +146,9 @@ export function BotLogListComponent({
|
||||
|
||||
function handleBotLogPush(response: BotLog[]) {
|
||||
setBotLogList(response.reverse());
|
||||
if (response.some((log) => Boolean(log.message_session_id))) {
|
||||
onMessageReceivedRef.current?.();
|
||||
}
|
||||
}
|
||||
|
||||
const handleScroll = useCallback(
|
||||
|
||||
@@ -15,7 +15,10 @@ import {
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import DynamicFormItemComponent from '@/app/home/components/dynamic-form/DynamicFormItemComponent';
|
||||
import { normalizeDynamicFormValuesForSave } from '@/app/home/components/dynamic-form/DynamicFormSaveValues';
|
||||
import {
|
||||
normalizeDynamicFormFieldValue,
|
||||
normalizeDynamicFormValuesForSave,
|
||||
} from '@/app/home/components/dynamic-form/DynamicFormSaveValues';
|
||||
import QrCodeLoginDialog, {
|
||||
QrLoginPlatform,
|
||||
} from '@/app/home/components/qrcode-login/QrCodeLoginDialog';
|
||||
@@ -464,61 +467,6 @@ export default function DynamicFormComponent({
|
||||
const previousInitialValues = useRef(initialValues);
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
// Normalize a form value according to its field type.
|
||||
// This ensures legacy/malformed data (e.g. a plain string for
|
||||
// model-fallback-selector) is coerced to the expected shape
|
||||
// so that downstream components never crash.
|
||||
const normalizeFieldValue = (
|
||||
item: DynamicFormValueSpec,
|
||||
value: unknown,
|
||||
): unknown => {
|
||||
if (
|
||||
item.name === 'mcp-resources' ||
|
||||
item.type === DynamicFormItemType.RESOURCES_SELECTOR ||
|
||||
item.type === DynamicFormItemType.RICH_TOOLS_SELECTOR
|
||||
) {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
if (item.type === 'model-fallback-selector') {
|
||||
if (value != null && typeof value === 'object' && !Array.isArray(value)) {
|
||||
const obj = value as Record<string, unknown>;
|
||||
return {
|
||||
primary: typeof obj.primary === 'string' ? obj.primary : '',
|
||||
fallbacks: Array.isArray(obj.fallbacks)
|
||||
? (obj.fallbacks as unknown[]).filter(
|
||||
(v): v is string => typeof v === 'string',
|
||||
)
|
||||
: [],
|
||||
reasoning:
|
||||
obj.reasoning != null &&
|
||||
typeof obj.reasoning === 'object' &&
|
||||
!Array.isArray(obj.reasoning)
|
||||
? Object.fromEntries(
|
||||
Object.entries(obj.reasoning).filter(
|
||||
(entry): entry is [string, string] =>
|
||||
typeof entry[1] === 'string',
|
||||
),
|
||||
)
|
||||
: {},
|
||||
};
|
||||
}
|
||||
// Legacy string format or any other unexpected type
|
||||
return {
|
||||
primary: typeof value === 'string' ? value : '',
|
||||
fallbacks: [],
|
||||
reasoning: {},
|
||||
};
|
||||
}
|
||||
if (item.type === 'prompt-editor') {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
// Default to a single empty system prompt entry
|
||||
return [{ role: 'system', content: '' }];
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
// Filter out display-only fields (webhook-url/embed-code/qr-code-login types
|
||||
// and `__system.*`-named fields) that should not participate in form state,
|
||||
// validation, or value emission.
|
||||
@@ -574,7 +522,7 @@ export default function DynamicFormComponent({
|
||||
const rawValue = initialValues?.[item.name] ?? item.default;
|
||||
return {
|
||||
...acc,
|
||||
[item.name]: normalizeFieldValue(item, rawValue),
|
||||
[item.name]: normalizeDynamicFormFieldValue(item, rawValue),
|
||||
};
|
||||
}, {} as FormValues),
|
||||
});
|
||||
@@ -611,7 +559,10 @@ export default function DynamicFormComponent({
|
||||
const mergedValues = editableValueSpecs.reduce(
|
||||
(acc, item) => {
|
||||
const rawValue = initialValues[item.name] ?? item.default;
|
||||
acc[item.name] = normalizeFieldValue(item, rawValue) as object;
|
||||
acc[item.name] = normalizeDynamicFormFieldValue(
|
||||
item,
|
||||
rawValue,
|
||||
) as object;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, object>,
|
||||
|
||||
@@ -5,6 +5,80 @@ export type DynamicFormSaveValueSpec = Pick<
|
||||
'default' | 'name' | 'type'
|
||||
>;
|
||||
|
||||
const ARRAY_FIELD_TYPES = new Set([
|
||||
'array[string]',
|
||||
'array[file]',
|
||||
'knowledge-base-multi-selector',
|
||||
'resources-selector',
|
||||
'rich-tools-selector',
|
||||
'tools-selector',
|
||||
]);
|
||||
|
||||
const STRING_FIELD_TYPES = new Set([
|
||||
'string',
|
||||
'text',
|
||||
'select',
|
||||
'llm-model-selector',
|
||||
'embedding-model-selector',
|
||||
'rerank-model-selector',
|
||||
'knowledge-base-selector',
|
||||
'bot-selector',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Coerce empty dynamic-form defaults into controlled React values.
|
||||
* Metadata from older adapters and runners can omit `default`; inputs must
|
||||
* still receive a stable value from their first render.
|
||||
*/
|
||||
export function normalizeDynamicFormFieldValue(
|
||||
spec: DynamicFormSaveValueSpec,
|
||||
value: unknown,
|
||||
): unknown {
|
||||
if (spec.name === 'mcp-resources' || ARRAY_FIELD_TYPES.has(spec.type)) {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
if (spec.type === 'boolean') {
|
||||
return typeof value === 'boolean' ? value : false;
|
||||
}
|
||||
if (STRING_FIELD_TYPES.has(spec.type)) {
|
||||
return typeof value === 'string' ? value : '';
|
||||
}
|
||||
if (spec.type === 'model-fallback-selector') {
|
||||
if (value != null && typeof value === 'object' && !Array.isArray(value)) {
|
||||
const objectValue = value as Record<string, unknown>;
|
||||
return {
|
||||
primary:
|
||||
typeof objectValue.primary === 'string' ? objectValue.primary : '',
|
||||
fallbacks: Array.isArray(objectValue.fallbacks)
|
||||
? objectValue.fallbacks.filter(
|
||||
(fallback): fallback is string => typeof fallback === 'string',
|
||||
)
|
||||
: [],
|
||||
reasoning:
|
||||
objectValue.reasoning != null &&
|
||||
typeof objectValue.reasoning === 'object' &&
|
||||
!Array.isArray(objectValue.reasoning)
|
||||
? Object.fromEntries(
|
||||
Object.entries(objectValue.reasoning).filter(
|
||||
(entry): entry is [string, string] =>
|
||||
typeof entry[1] === 'string',
|
||||
),
|
||||
)
|
||||
: {},
|
||||
};
|
||||
}
|
||||
return {
|
||||
primary: typeof value === 'string' ? value : '',
|
||||
fallbacks: [],
|
||||
reasoning: {},
|
||||
};
|
||||
}
|
||||
if (spec.type === 'prompt-editor') {
|
||||
return Array.isArray(value) ? value : [{ role: 'system', content: '' }];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
const reasoningLevels = new Set([
|
||||
'disabled',
|
||||
'enabled',
|
||||
|
||||
@@ -33,7 +33,7 @@ const getFormSchema = (t: (key: string) => string) =>
|
||||
|
||||
interface ProviderFormProps {
|
||||
providerId?: string;
|
||||
onFormSubmit: () => void;
|
||||
onFormSubmit: (providerUuid: string) => void | Promise<void>;
|
||||
onFormCancel: () => void;
|
||||
}
|
||||
|
||||
@@ -171,14 +171,16 @@ export default function ProviderForm({
|
||||
};
|
||||
|
||||
try {
|
||||
let savedProviderUuid = providerId;
|
||||
if (providerId) {
|
||||
await httpClient.updateModelProvider(providerId, data);
|
||||
toast.success(t('models.providerSaved'));
|
||||
} else {
|
||||
await httpClient.createModelProvider(data);
|
||||
const response = await httpClient.createModelProvider(data);
|
||||
savedProviderUuid = response.uuid;
|
||||
toast.success(t('models.providerCreated'));
|
||||
}
|
||||
onFormSubmit();
|
||||
await onFormSubmit(savedProviderUuid as string);
|
||||
} catch (err) {
|
||||
toast.error(t('models.providerSaveError') + (err as CustomApiError).msg);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user