action-translations.ts 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. import { Badge } from '@/components/ui/badge';
  2. import { CheckCircleIcon, CircleIcon, ClockIcon, XCircleIcon } from 'lucide-react';
  3. import type { ReactNode } from 'react';
  4. import { createElement } from 'react';
  5. /**
  6. * Map SSE status strings to i18n keys under `actions.status.*`
  7. */
  8. const statusKeyMap: Record<string, string> = {
  9. 'input-streaming': 'inputStreaming',
  10. 'input-available': 'inputAvailable',
  11. 'output-available': 'outputAvailable',
  12. 'output-error': 'outputError',
  13. 'output-denied': 'outputDenied',
  14. running: 'running',
  15. result: 'result',
  16. error: 'error',
  17. };
  18. /**
  19. * Resolve an action name to its i18n display name.
  20. * Falls back to the raw actionName if no translation exists.
  21. */
  22. export function getActionDisplayName(t: (key: string) => string, actionName: string): string {
  23. const translated = t(`actions.names.${actionName}`);
  24. // t() returns the key itself when translation is missing
  25. return translated === `actions.names.${actionName}` ? actionName : translated;
  26. }
  27. /**
  28. * Get a localized status badge for action state.
  29. */
  30. export function getStatusBadge(t: (key: string) => string, state: string): ReactNode {
  31. const iconMap: Record<string, ReactNode> = {
  32. 'input-streaming': createElement(CircleIcon, { className: 'size-4' }),
  33. 'input-available': createElement(ClockIcon, {
  34. className: 'size-4 animate-pulse',
  35. }),
  36. 'output-available': createElement(CheckCircleIcon, {
  37. className: 'size-4 text-green-600',
  38. }),
  39. 'output-error': createElement(XCircleIcon, {
  40. className: 'size-4 text-red-600',
  41. }),
  42. 'output-denied': createElement(XCircleIcon, {
  43. className: 'size-4 text-orange-600',
  44. }),
  45. running: createElement(ClockIcon, { className: 'size-4 animate-pulse' }),
  46. result: createElement(CheckCircleIcon, {
  47. className: 'size-4 text-green-600',
  48. }),
  49. error: createElement(XCircleIcon, { className: 'size-4 text-red-600' }),
  50. };
  51. const i18nKey = statusKeyMap[state];
  52. const label = i18nKey ? t(`actions.status.${i18nKey}`) : state;
  53. return createElement(
  54. Badge,
  55. {
  56. className: 'gap-1.5 rounded-full text-xs',
  57. variant: 'secondary' as const,
  58. },
  59. iconMap[state] || createElement(CircleIcon, { className: 'size-4' }),
  60. label,
  61. );
  62. }
  63. /**
  64. * Extract text parts from a message
  65. */
  66. export function getMessageTextParts(message: {
  67. parts?: Array<{ type: string; text?: string; [key: string]: unknown }>;
  68. }) {
  69. if (!message.parts || message.parts.length === 0) {
  70. return [];
  71. }
  72. return message.parts.filter((part) => part.type === 'text' || part.type === 'step-start');
  73. }
  74. /**
  75. * Extract action parts from a message
  76. */
  77. export function getMessageActionParts(message: {
  78. parts?: Array<{ type: string; [key: string]: unknown }>;
  79. }) {
  80. if (!message.parts || message.parts.length === 0) {
  81. return [];
  82. }
  83. return message.parts.filter((part) => part.type && part.type.startsWith('action-'));
  84. }