@open-mercato/ui 0.6.2-develop.3406.1.2b403f40da → 0.6.2-develop.3446.1.bd060c6017

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/ai/AiAssistantLauncher.js +11 -10
  3. package/dist/ai/AiAssistantLauncher.js.map +2 -2
  4. package/dist/ai/AiChat.js +218 -83
  5. package/dist/ai/AiChat.js.map +2 -2
  6. package/dist/ai/AiIcon.js +1 -1
  7. package/dist/ai/AiIcon.js.map +2 -2
  8. package/dist/ai/parts/FieldDiffCard.js +4 -3
  9. package/dist/ai/parts/FieldDiffCard.js.map +2 -2
  10. package/dist/ai/useAiChat.js +98 -2
  11. package/dist/ai/useAiChat.js.map +2 -2
  12. package/dist/backend/DataTable.js +4 -13
  13. package/dist/backend/DataTable.js.map +2 -2
  14. package/dist/primitives/date-format.js +53 -0
  15. package/dist/primitives/date-format.js.map +7 -0
  16. package/dist/primitives/date-picker.js +3 -26
  17. package/dist/primitives/date-picker.js.map +2 -2
  18. package/dist/primitives/date-range-picker.js +3 -22
  19. package/dist/primitives/date-range-picker.js.map +2 -2
  20. package/package.json +4 -4
  21. package/src/ai/AiAssistantLauncher.tsx +11 -10
  22. package/src/ai/AiChat.tsx +200 -26
  23. package/src/ai/AiIcon.tsx +1 -1
  24. package/src/ai/__tests__/AiChat.task-plan.test.tsx +356 -0
  25. package/src/ai/__tests__/AiChat.test.tsx +44 -2
  26. package/src/ai/parts/FieldDiffCard.tsx +4 -3
  27. package/src/ai/parts/__tests__/FieldDiffCard.test.tsx +24 -0
  28. package/src/ai/parts/types.ts +3 -0
  29. package/src/ai/useAiChat.ts +154 -1
  30. package/src/backend/DataTable.tsx +12 -22
  31. package/src/primitives/__tests__/date-format.test.ts +43 -0
  32. package/src/primitives/date-format.ts +60 -0
  33. package/src/primitives/date-picker.tsx +5 -17
  34. package/src/primitives/date-range-picker.tsx +3 -13
package/src/ai/AiChat.tsx CHANGED
@@ -2,17 +2,32 @@
2
2
 
3
3
  import * as React from 'react'
4
4
  import {
5
+ Activity,
6
+ AlertTriangle,
7
+ BadgeQuestionMark,
5
8
  Bot,
9
+ ChartNoAxesCombined,
6
10
  Check,
7
11
  ChevronDown,
8
12
  ChevronRight,
13
+ Circle,
14
+ CircleAlert,
15
+ CircleCheck,
16
+ CircleDashed,
9
17
  Copy,
18
+ Database,
19
+ FileQuestion,
20
+ GitBranch,
21
+ Handshake,
10
22
  Lightbulb,
23
+ ListChecks,
11
24
  Loader2,
12
25
  Paperclip,
13
26
  Plus,
27
+ Search,
14
28
  Send,
15
29
  Square,
30
+ TrendingDown,
16
31
  User,
17
32
  Wrench,
18
33
  X,
@@ -42,6 +57,7 @@ import {
42
57
  } from './ui-part-registry'
43
58
  import {
44
59
  useAiChat,
60
+ type AiAgentTaskSnapshot,
45
61
  type AiChatMessage,
46
62
  type AiChatMessageFile,
47
63
  type AiChatMessageUiPart,
@@ -50,6 +66,7 @@ import {
50
66
  import { useAiChatUpload } from './useAiChatUpload'
51
67
  import { useAiShortcuts } from './useAiShortcuts'
52
68
  import { LoopTracePanel, type LoopTracePanelTrace } from './LoopTracePanel'
69
+ import { AiIcon } from './AiIcon'
53
70
 
54
71
  // Cap inline previews so we do not blow past localStorage quota (~5MB on most
55
72
  // browsers). Images larger than this still upload + send to the LLM as inline
@@ -381,16 +398,116 @@ function safeStringify(value: unknown): string {
381
398
  }
382
399
  }
383
400
 
401
+ function titleWords(value: string): string {
402
+ return value.replace(/\b[a-z]/g, (char) => char.toUpperCase())
403
+ }
404
+
405
+ function formatToolCaption(call: AiChatToolCallSnapshot): string {
406
+ const rawCaption = call.caption?.trim()
407
+ if (!rawCaption) return call.toolName
408
+ const caption = titleWords(rawCaption.replace(/\s*·\s*/g, ' - '))
409
+ return `${caption} (${call.toolName})`
410
+ }
411
+
412
+ /**
413
+ * Visible agent task plan rendered above raw tool-call rows. Phase 1
414
+ * implementation of spec `.ai/specs/2026-05-13-ai-chat-visible-task-plan.md`:
415
+ * compact, fixed-height rows with icon + label + status badge so the
416
+ * panel does not jump while streaming. Source of truth is the
417
+ * client-local `taskPlan` array on the assistant message.
418
+ */
419
+ function AiChatTaskPlan({ tasks }: { tasks: AiAgentTaskSnapshot[] }) {
420
+ const t = useT()
421
+ if (!tasks || tasks.length === 0) return null
422
+ return (
423
+ <div
424
+ className="rounded-md border border-border bg-muted/30"
425
+ data-ai-chat-task-plan=""
426
+ data-ai-chat-task-plan-count={tasks.length}
427
+ >
428
+ <div className="flex items-center gap-2 border-b border-border/60 px-2 py-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
429
+ <ListChecks className="size-3.5 text-muted-foreground" aria-hidden />
430
+ <span>{t('ai_assistant.chat.taskPlanTitle', 'Plan')}</span>
431
+ <span className="ml-auto font-mono">{tasks.length}</span>
432
+ </div>
433
+ <ul className="divide-y divide-border/60" data-ai-chat-task-plan-list="">
434
+ {tasks.map((task) => (
435
+ <AiChatTaskRow key={task.id} task={task} />
436
+ ))}
437
+ </ul>
438
+ </div>
439
+ )
440
+ }
441
+
442
+ function AiChatTaskRow({ task }: { task: AiAgentTaskSnapshot }) {
443
+ const t = useT()
444
+ const statusLabel =
445
+ task.state === 'running'
446
+ ? t('ai_assistant.chat.taskRunning', 'running…')
447
+ : task.state === 'done'
448
+ ? t('ai_assistant.chat.taskDone', 'done')
449
+ : task.state === 'failed'
450
+ ? t('ai_assistant.chat.taskFailed', 'failed')
451
+ : task.state === 'skipped'
452
+ ? t('ai_assistant.chat.taskSkipped', 'skipped')
453
+ : t('ai_assistant.chat.taskPending', 'pending')
454
+ const Icon =
455
+ task.state === 'running'
456
+ ? Loader2
457
+ : task.state === 'done'
458
+ ? CircleCheck
459
+ : task.state === 'failed'
460
+ ? CircleAlert
461
+ : task.state === 'skipped'
462
+ ? CircleDashed
463
+ : Circle
464
+ const iconClassName = cn(
465
+ 'size-3.5 shrink-0',
466
+ task.state === 'running' && 'animate-spin text-muted-foreground',
467
+ task.state === 'done' && 'text-status-success-icon',
468
+ task.state === 'failed' && 'text-destructive',
469
+ task.state === 'skipped' && 'text-muted-foreground',
470
+ task.state === 'pending' && 'text-muted-foreground',
471
+ )
472
+ const statusBadgeClassName = cn(
473
+ 'ml-auto text-[10px] uppercase tracking-wide',
474
+ task.state === 'done' && 'text-status-success-text',
475
+ task.state === 'failed' && 'text-destructive',
476
+ task.state !== 'done' && task.state !== 'failed' && 'text-muted-foreground',
477
+ )
478
+ return (
479
+ <li
480
+ className="flex h-7 items-center gap-2 px-2 text-xs"
481
+ data-ai-chat-task-id={task.id}
482
+ data-ai-chat-task-state={task.state}
483
+ data-ai-chat-task-source={task.source}
484
+ data-ai-chat-task-tool-call-id={task.toolCallId}
485
+ >
486
+ <Icon className={iconClassName} aria-hidden />
487
+ <span className="truncate" title={task.detail ?? task.label}>
488
+ {task.label}
489
+ </span>
490
+ <span className={statusBadgeClassName}>{statusLabel}</span>
491
+ </li>
492
+ )
493
+ }
494
+
384
495
  function ToolCallList({ toolCalls }: { toolCalls: AiChatToolCallSnapshot[] }) {
385
496
  const t = useT()
386
497
  const [openId, setOpenId] = React.useState<string | null>(null)
387
498
  if (!toolCalls || toolCalls.length === 0) return null
388
499
  return (
389
- <div className="space-y-1" data-ai-chat-tool-calls="">
390
- <div className="flex items-center justify-between px-0.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
391
- <span>{t('ai_assistant.chat.agentTasksTitle', 'Agent tasks')}</span>
392
- <span>{toolCalls.length}</span>
500
+ <div
501
+ className="rounded-md border border-border bg-muted/30"
502
+ data-ai-chat-tool-calls=""
503
+ data-ai-chat-tool-call-count={toolCalls.length}
504
+ >
505
+ <div className="flex items-center gap-2 border-b border-border/60 px-2 py-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
506
+ <Wrench className="size-3.5 text-muted-foreground" aria-hidden />
507
+ <span>{t('ai_assistant.chat.agentTasksTitle', 'Tool calls')}</span>
508
+ <span className="ml-auto font-mono">{toolCalls.length}</span>
393
509
  </div>
510
+ <ul className="divide-y divide-border/60">
394
511
  {toolCalls.map((call) => {
395
512
  const isOpen = openId === call.id
396
513
  const isError = call.state === 'error'
@@ -402,19 +519,20 @@ function ToolCallList({ toolCalls }: { toolCalls: AiChatToolCallSnapshot[] }) {
402
519
  ? t('ai_assistant.chat.toolRunning', 'running…')
403
520
  : t('ai_assistant.chat.toolDone', 'done')
404
521
  return (
405
- <div
522
+ <li
406
523
  key={call.id}
407
524
  className={cn(
408
- 'rounded-md border border-border bg-muted/30',
409
- isError ? 'border-destructive/40 bg-destructive/5' : '',
525
+ isError ? 'bg-destructive/5' : '',
410
526
  )}
411
527
  data-ai-chat-tool-call={call.toolName}
412
528
  data-ai-chat-tool-state={call.state}
413
529
  >
414
- <button
530
+ <Button
531
+ variant="ghost"
532
+ size="2xs"
415
533
  type="button"
416
534
  onClick={() => setOpenId(isOpen ? null : call.id)}
417
- className="flex w-full items-center gap-2 px-2 py-1.5 text-left text-xs font-medium hover:bg-muted/60"
535
+ className="w-full justify-start rounded-none px-2 text-left hover:bg-muted/60"
418
536
  >
419
537
  {isOpen ? (
420
538
  <ChevronDown className="size-3.5 text-muted-foreground" aria-hidden />
@@ -432,7 +550,9 @@ function ToolCallList({ toolCalls }: { toolCalls: AiChatToolCallSnapshot[] }) {
432
550
  aria-hidden
433
551
  />
434
552
  )}
435
- <span className="font-mono">{call.toolName}</span>
553
+ <span className="truncate" title={formatToolCaption(call)}>
554
+ {formatToolCaption(call)}
555
+ </span>
436
556
  <span
437
557
  className={cn(
438
558
  'ml-auto text-[10px] uppercase tracking-wide',
@@ -445,7 +565,7 @@ function ToolCallList({ toolCalls }: { toolCalls: AiChatToolCallSnapshot[] }) {
445
565
  >
446
566
  {statusLabel}
447
567
  </span>
448
- </button>
568
+ </Button>
449
569
  {isOpen ? (
450
570
  <div className="space-y-1 border-t border-border/60 px-2 py-1.5 text-xs">
451
571
  {call.input !== undefined ? (
@@ -473,9 +593,10 @@ function ToolCallList({ toolCalls }: { toolCalls: AiChatToolCallSnapshot[] }) {
473
593
  ) : null}
474
594
  </div>
475
595
  ) : null}
476
- </div>
596
+ </li>
477
597
  )
478
598
  })}
599
+ </ul>
479
600
  </div>
480
601
  )
481
602
  }
@@ -534,6 +655,10 @@ function MessageRow({
534
655
  const Icon = isAssistant ? Bot : User
535
656
  const [copied, setCopied] = React.useState(false)
536
657
  const copyTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
658
+ const visibleTaskPlan =
659
+ isAssistant && message.taskPlan
660
+ ? message.taskPlan.filter((task) => task.source === 'agent')
661
+ : []
537
662
 
538
663
  React.useEffect(() => {
539
664
  return () => {
@@ -638,6 +763,9 @@ function MessageRow({
638
763
  streaming={message.reasoningStreaming === true}
639
764
  />
640
765
  ) : null}
766
+ {visibleTaskPlan.length > 0 ? (
767
+ <AiChatTaskPlan tasks={visibleTaskPlan} />
768
+ ) : null}
641
769
  {isAssistant && message.toolCalls && message.toolCalls.length > 0 ? (
642
770
  <ToolCallList toolCalls={message.toolCalls} />
643
771
  ) : null}
@@ -735,6 +863,49 @@ function AiUiPartRenderer({
735
863
  )
736
864
  }
737
865
 
866
+ function getSuggestionIcon(suggestion: AiChatSuggestion): {
867
+ icon: React.ReactNode
868
+ name: string
869
+ } {
870
+ if (suggestion.icon) return { icon: suggestion.icon, name: 'custom' }
871
+ const text = `${suggestion.label} ${suggestion.prompt}`.toLowerCase()
872
+ const iconClassName = 'size-4 text-foreground'
873
+ if (/\b(analy[sz]e|analysis|health|score|summary|summari[sz]e)\b/.test(text)) {
874
+ return { icon: <ChartNoAxesCombined className={iconClassName} />, name: 'analysis' }
875
+ }
876
+ if (/\b(at[-\s]?risk|risk|stalled|stale|overdue|danger)\b/.test(text)) {
877
+ return { icon: <AlertTriangle className={iconClassName} />, name: 'risk' }
878
+ }
879
+ if (/\bpipeline|stage|funnel\b/.test(text)) {
880
+ return { icon: <GitBranch className={iconClassName} />, name: 'pipeline' }
881
+ }
882
+ if (/\bdeal|opportunit(?:y|ies)\b/.test(text)) {
883
+ return { icon: <Handshake className={iconClassName} />, name: 'deal' }
884
+ }
885
+ if (/\bdata|access|record|records|schema\b/.test(text)) {
886
+ return { icon: <Database className={iconClassName} />, name: 'data' }
887
+ }
888
+ if (/\bwhat can you help|help me|capabilit(?:y|ies)\b/.test(text)) {
889
+ return { icon: <BadgeQuestionMark className={iconClassName} />, name: 'capabilities' }
890
+ }
891
+ if (/\bsuggest|try|idea|ideas|recommend\b/.test(text)) {
892
+ return { icon: <Lightbulb className={iconClassName} />, name: 'ideas' }
893
+ }
894
+ if (/\bhow do i|how to|use this|instructions?|guide\b/.test(text)) {
895
+ return { icon: <FileQuestion className={iconClassName} />, name: 'guide' }
896
+ }
897
+ if (/\bfind|search|show|list|lookup\b/.test(text)) {
898
+ return { icon: <Search className={iconClassName} />, name: 'search' }
899
+ }
900
+ if (/\bactivity|activities|timeline|recent\b/.test(text)) {
901
+ return { icon: <Activity className={iconClassName} />, name: 'activity' }
902
+ }
903
+ if (/\bdown|drop|declin|loss|lost\b/.test(text)) {
904
+ return { icon: <TrendingDown className={iconClassName} />, name: 'trend-down' }
905
+ }
906
+ return { icon: <AiIcon className={iconClassName} />, name: 'ai' }
907
+ }
908
+
738
909
  function WelcomeState({
739
910
  title,
740
911
  description,
@@ -768,22 +939,25 @@ function WelcomeState({
768
939
  </div>
769
940
  {suggestions && suggestions.length > 0 ? (
770
941
  <div className="flex w-full max-w-md flex-col gap-2" data-ai-chat-suggestions="">
771
- {suggestions.map((suggestion, index) => (
772
- <button
773
- key={index}
774
- type="button"
775
- className="flex items-center gap-2 rounded-lg border border-border bg-card px-3 py-2.5 text-left text-sm transition-colors hover:bg-accent hover:text-accent-foreground"
776
- onClick={() => onSuggestionClick(suggestion.prompt)}
777
- data-ai-chat-suggestion={index}
778
- >
779
- {suggestion.icon ? (
942
+ {suggestions.map((suggestion, index) => {
943
+ const suggestionIcon = getSuggestionIcon(suggestion)
944
+ return (
945
+ <Button
946
+ variant="outline"
947
+ key={index}
948
+ type="button"
949
+ className="h-auto w-full justify-start whitespace-normal rounded-lg bg-card px-3 py-2.5 text-left text-sm"
950
+ onClick={() => onSuggestionClick(suggestion.prompt)}
951
+ data-ai-chat-suggestion={index}
952
+ data-ai-chat-suggestion-icon={suggestionIcon.name}
953
+ >
780
954
  <span className="shrink-0 text-muted-foreground" aria-hidden>
781
- {suggestion.icon}
955
+ {suggestionIcon.icon}
782
956
  </span>
783
- ) : null}
784
- <span>{suggestion.label}</span>
785
- </button>
786
- ))}
957
+ <span>{suggestion.label}</span>
958
+ </Button>
959
+ )
960
+ })}
787
961
  </div>
788
962
  ) : null}
789
963
  </div>
package/src/ai/AiIcon.tsx CHANGED
@@ -6,7 +6,7 @@ export function AiIcon({ className, ...props }: LucideProps) {
6
6
  <Sparkles
7
7
  aria-hidden
8
8
  {...props}
9
- className={cn(className, 'text-accent-indigo')}
9
+ className={cn('text-accent-indigo', className)}
10
10
  />
11
11
  )
12
12
  }