ai_agents_admin.js 3.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. 'use client';
  2. import { useCallback, useState } from 'react';
  3. import { useTranslation } from 'react-i18next';
  4. import { View, ScrollView } from 'app/design/view';
  5. import { cn } from 'app/lib/util';
  6. import { BlockWrapper } from 'app/components/block-wrapper';
  7. import { components } from 'app/components/registry';
  8. import { lazyComponent } from 'app/lib/lazy-component';
  9. import { useLocalSearchParams } from 'app/lib/hooks/router';
  10. import { hiddenFirstMessageFromData } from 'app/ui/molecules/ai-agent/hidden-first-message';
  11. // Operators only — the server returns an empty block for everyone else — so keep it out of the main chunk.
  12. // The chat widget's default frame (AiAgent `height`); the list tab takes the same one so the block does not jump.
  13. const TAB_HEIGHT = 'h-[28rem]';
  14. const AiAgentsAdmin = lazyComponent(() => import('app/ui/molecules/ai-agents-admin'), { name: 'AiAgentsAdmin' });
  15. /**
  16. * `system/get_block_ai_agents_admin` — one block with two tabs in its header:
  17. * the operator chat (`data.chat`, the same payload as the `ai_agent` block) and
  18. * the agents list. With no operator agent configured only the list is shown.
  19. *
  20. * Both tabs stay mounted once opened, so switching does not lose a half-typed
  21. * message or an expanded history, and both share one frame height — the list
  22. * scrolls inside it — so the block keeps its size when the tab changes.
  23. */
  24. export default function ElementAiAgentsAdmin({ data, blockWrapperProps }) {
  25. const { t } = useTranslation();
  26. const params = useLocalSearchParams();
  27. const chat = data?.chat && typeof data.chat === 'object' ? data.chat : null;
  28. const [tab, setTab] = useState(chat ? 'chat' : 'agents');
  29. const [opened, setOpened] = useState(() => ({ [chat ? 'chat' : 'agents']: true }));
  30. const select = useCallback((name) => {
  31. setTab(name);
  32. setOpened((prev) => (prev[name] ? prev : { ...prev, [name]: true }));
  33. }, []);
  34. // The header switcher is the block menu; BlockWrapper renders `onPress` items as buttons.
  35. const base = blockWrapperProps?.block;
  36. const block = base && chat
  37. ? {
  38. ...base,
  39. menu: {
  40. items: [
  41. { name: 'chat', title: data.chat_title || t('operator_agent_title'), pressed: tab === 'chat', onPress: () => select('chat') },
  42. { name: 'agents', title: t('agents_admin_tab'), pressed: tab === 'agents', onPress: () => select('agents') },
  43. ],
  44. },
  45. }
  46. : base;
  47. const AiAgent = components['molecule']['ai_agent'];
  48. const initialMessage = chat ? hiddenFirstMessageFromData(chat, params) : '';
  49. const chatAgentId = chat?.agent_id ?? '';
  50. const chatContextPid = Number(chat?.context_profile_id) || 0;
  51. return (
  52. <BlockWrapper {...blockWrapperProps} block={block}>
  53. {chat && opened.chat ? (
  54. <View className={tab === 'chat' ? '' : 'hidden'}>
  55. <AiAgent
  56. key={`${chatAgentId}:${chatContextPid}`}
  57. data={chat}
  58. initialMessage={initialMessage}
  59. hideInitialMessage={!!initialMessage}
  60. historyAlways
  61. />
  62. </View>
  63. ) : null}
  64. {opened.agents && chat ? (
  65. <ScrollView className={cn(TAB_HEIGHT, tab === 'agents' ? '' : 'hidden')} contentContainerClassName="pr-1 pb-1">
  66. <AiAgentsAdmin data={data} />
  67. </ScrollView>
  68. ) : null}
  69. {opened.agents && !chat ? <AiAgentsAdmin data={data} /> : null}
  70. </BlockWrapper>
  71. );
  72. }