@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/ai/AiAssistantLauncher.js +11 -10
- package/dist/ai/AiAssistantLauncher.js.map +2 -2
- package/dist/ai/AiChat.js +218 -83
- package/dist/ai/AiChat.js.map +2 -2
- package/dist/ai/AiIcon.js +1 -1
- package/dist/ai/AiIcon.js.map +2 -2
- package/dist/ai/parts/FieldDiffCard.js +4 -3
- package/dist/ai/parts/FieldDiffCard.js.map +2 -2
- package/dist/ai/useAiChat.js +98 -2
- package/dist/ai/useAiChat.js.map +2 -2
- package/dist/backend/DataTable.js +4 -13
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/primitives/date-format.js +53 -0
- package/dist/primitives/date-format.js.map +7 -0
- package/dist/primitives/date-picker.js +3 -26
- package/dist/primitives/date-picker.js.map +2 -2
- package/dist/primitives/date-range-picker.js +3 -22
- package/dist/primitives/date-range-picker.js.map +2 -2
- package/package.json +4 -4
- package/src/ai/AiAssistantLauncher.tsx +11 -10
- package/src/ai/AiChat.tsx +200 -26
- package/src/ai/AiIcon.tsx +1 -1
- package/src/ai/__tests__/AiChat.task-plan.test.tsx +356 -0
- package/src/ai/__tests__/AiChat.test.tsx +44 -2
- package/src/ai/parts/FieldDiffCard.tsx +4 -3
- package/src/ai/parts/__tests__/FieldDiffCard.test.tsx +24 -0
- package/src/ai/parts/types.ts +3 -0
- package/src/ai/useAiChat.ts +154 -1
- package/src/backend/DataTable.tsx +12 -22
- package/src/primitives/__tests__/date-format.test.ts +43 -0
- package/src/primitives/date-format.ts +60 -0
- package/src/primitives/date-picker.tsx +5 -17
- 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
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
<
|
|
522
|
+
<li
|
|
406
523
|
key={call.id}
|
|
407
524
|
className={cn(
|
|
408
|
-
|
|
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
|
-
<
|
|
530
|
+
<Button
|
|
531
|
+
variant="ghost"
|
|
532
|
+
size="2xs"
|
|
415
533
|
type="button"
|
|
416
534
|
onClick={() => setOpenId(isOpen ? null : call.id)}
|
|
417
|
-
className="
|
|
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="
|
|
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
|
-
</
|
|
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
|
-
</
|
|
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
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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
|
-
{
|
|
955
|
+
{suggestionIcon.icon}
|
|
782
956
|
</span>
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
)
|
|
957
|
+
<span>{suggestion.label}</span>
|
|
958
|
+
</Button>
|
|
959
|
+
)
|
|
960
|
+
})}
|
|
787
961
|
</div>
|
|
788
962
|
) : null}
|
|
789
963
|
</div>
|