@nocobase/portal-template-default 1.0.3 → 1.0.4
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/README.MD +10 -5
- package/package.json +1 -1
- package/registry/nocobase-ai/components/chat/chat-message.tsx +8 -6
- package/registry/nocobase-ai/components/chat/chat-messages.tsx +8 -3
- package/registry/nocobase-ai/components/chat/markdown-message.tsx +26 -0
- package/registry/nocobase-ai/components/chat/sub-agent-conversation.tsx +3 -3
- package/registry/nocobase-ai/demo/configuration-gate.tsx +83 -35
- package/registry/nocobase-i18n/README.md +23 -32
- package/registry/nocobase-i18n/components/language-switcher.tsx +1 -1
- package/registry/nocobase-i18n/demo/index.tsx +2 -2
- package/registry/nocobase-i18n/demo/prompt-generator.tsx +3 -2
- package/registry/nocobase-i18n/extension.tsx +3 -4
- package/registry/nocobase-i18n/index.ts +4 -3
- package/registry/nocobase-i18n/locales/en-US.ts +2 -52
- package/registry/nocobase-i18n/locales/index.ts +8 -0
- package/registry/nocobase-i18n/locales/zh-CN.ts +2 -52
- package/registry/nocobase-i18n/provider.tsx +18 -6
- package/registry/nocobase-i18n/server-resources.ts +13 -31
- package/registry/nocobase-i18n/tests/i18n-regression.mjs +14 -5
- package/registry/nocobase-route-surfaces/README.md +21 -0
- package/registry/nocobase-route-surfaces/demo/index.tsx +266 -0
- package/{src/extensions/nocobase-acl → registry/nocobase-route-surfaces}/demo/lazy-route.tsx +2 -1
- package/registry/nocobase-route-surfaces/demo/prompt-generator.tsx +90 -0
- package/registry/nocobase-route-surfaces/demo/scenarios.ts +95 -0
- package/registry/nocobase-route-surfaces/extension.tsx +119 -0
- package/registry/nocobase-route-surfaces/index.ts +7 -0
- package/registry/nocobase-route-surfaces/route-dialog.tsx +145 -0
- package/registry/nocobase-route-surfaces/route-drawer.tsx +186 -0
- package/registry/nocobase-route-surfaces/route-page.tsx +29 -0
- package/registry/nocobase-route-surfaces/route-surface-context.ts +13 -0
- package/registry/nocobase-route-surfaces/use-refine-unsaved-changes.tsx +106 -0
- package/registry/nocobase-route-surfaces/use-route-surface-close.ts +15 -0
- package/registry/nocobase-route-surfaces/use-route-surface-state.ts +65 -0
- package/registry.config.json +30 -3
- package/scripts/registry.mjs +0 -2
- package/src/App.tsx +65 -35
- package/src/app/extension.ts +1 -2
- package/src/app/extensions.tsx +0 -14
- package/src/components/app-shell/breadcrumb.tsx +67 -2
- package/src/components/app-shell/sidebar.tsx +2 -2
- package/src/components/resources/views/list-view.tsx +3 -2
- package/src/components/ui/sheet.tsx +22 -21
- package/src/lib/nocobase/client.ts +6 -1
- package/src/locales/en-US.ts +81 -0
- package/src/locales/zh-CN.ts +77 -0
- package/src/pages/users/create.tsx +76 -53
- package/src/pages/users/edit.tsx +87 -56
- package/src/pages/users/index.ts +3 -0
- package/src/pages/users/layout.tsx +15 -0
- package/src/pages/users/list.tsx +19 -21
- package/src/pages/users/role-badges.tsx +43 -0
- package/src/pages/users/role-detail.tsx +272 -0
- package/src/pages/users/role-utils.ts +6 -0
- package/src/pages/users/routes.ts +24 -0
- package/src/pages/users/show.tsx +195 -104
- package/src/pages/users/types.ts +13 -0
- package/src/providers/i18n/index.ts +3 -0
- package/{registry/nocobase-i18n → src/providers/i18n}/runtime.ts +63 -16
- package/src/providers/system-settings/context.ts +26 -0
- package/src/providers/system-settings/index.ts +2 -0
- package/src/providers/system-settings/provider.tsx +94 -0
- package/src/extensions/nocobase-acl/README.md +0 -19
- package/src/extensions/nocobase-acl/components/acl-boundary.tsx +0 -106
- package/src/extensions/nocobase-acl/components/index.ts +0 -3
- package/src/extensions/nocobase-acl/components/role-options.ts +0 -36
- package/src/extensions/nocobase-acl/components/role-switcher.tsx +0 -135
- package/src/extensions/nocobase-acl/demo/boundary-api.tsx +0 -186
- package/src/extensions/nocobase-acl/demo/components.tsx +0 -231
- package/src/extensions/nocobase-acl/demo/index.tsx +0 -505
- package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +0 -134
- package/src/extensions/nocobase-acl/demo/role-switcher-prompt-generator.tsx +0 -198
- package/src/extensions/nocobase-acl/demo/scenario-section.tsx +0 -37
- package/src/extensions/nocobase-acl/extension.tsx +0 -71
- package/src/extensions/nocobase-acl/index.ts +0 -1
- package/src/extensions/nocobase-ai/README.md +0 -70
- package/src/extensions/nocobase-ai/adapters/react-hook-form.ts +0 -20
- package/src/extensions/nocobase-ai/assets/nocobase-ai-chat.svg +0 -50
- package/src/extensions/nocobase-ai/components/ai-root-provider.tsx +0 -32
- package/src/extensions/nocobase-ai/components/chat/ai-employee-avatar.tsx +0 -31
- package/src/extensions/nocobase-ai/components/chat/chat-attachment.tsx +0 -92
- package/src/extensions/nocobase-ai/components/chat/chat-compact.tsx +0 -90
- package/src/extensions/nocobase-ai/components/chat/chat-composer.tsx +0 -429
- package/src/extensions/nocobase-ai/components/chat/chat-empty-state.tsx +0 -51
- package/src/extensions/nocobase-ai/components/chat/chat-header.tsx +0 -88
- package/src/extensions/nocobase-ai/components/chat/chat-history-dialog.tsx +0 -77
- package/src/extensions/nocobase-ai/components/chat/chat-message.tsx +0 -252
- package/src/extensions/nocobase-ai/components/chat/chat-messages.tsx +0 -173
- package/src/extensions/nocobase-ai/components/chat/chat-window.tsx +0 -118
- package/src/extensions/nocobase-ai/components/chat/conversation-list.tsx +0 -343
- package/src/extensions/nocobase-ai/components/chat/markdown-message.tsx +0 -165
- package/src/extensions/nocobase-ai/components/chat/model-select-options.tsx +0 -30
- package/src/extensions/nocobase-ai/components/chat/reasoning-panel.tsx +0 -29
- package/src/extensions/nocobase-ai/components/chat/sub-agent-conversation.tsx +0 -142
- package/src/extensions/nocobase-ai/components/chat/tool-call-card.tsx +0 -316
- package/src/extensions/nocobase-ai/components/chat/user-prompt-editor.tsx +0 -99
- package/src/extensions/nocobase-ai/components/chat/work-context-chip.tsx +0 -114
- package/src/extensions/nocobase-ai/components/index.ts +0 -59
- package/src/extensions/nocobase-ai/components/page-elements/ai-form.tsx +0 -44
- package/src/extensions/nocobase-ai/components/page-elements/page-element-provider.tsx +0 -469
- package/src/extensions/nocobase-ai/components/surfaces/chat-dialog.tsx +0 -31
- package/src/extensions/nocobase-ai/components/surfaces/chat-inline.tsx +0 -18
- package/src/extensions/nocobase-ai/components/surfaces/chat-page.tsx +0 -18
- package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +0 -54
- package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel.tsx +0 -74
- package/src/extensions/nocobase-ai/components/surfaces/chat-surface-actions.tsx +0 -54
- package/src/extensions/nocobase-ai/components/tools/builtin-tool-renderers.tsx +0 -31
- package/src/extensions/nocobase-ai/components/tools/business-report-dialog.tsx +0 -364
- package/src/extensions/nocobase-ai/components/tools/business-report-renderer.tsx +0 -122
- package/src/extensions/nocobase-ai/components/tools/business-report-utils.tsx +0 -282
- package/src/extensions/nocobase-ai/components/tools/chart-renderer.tsx +0 -99
- package/src/extensions/nocobase-ai/components/tools/echarts-preview.tsx +0 -59
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-advanced.ts +0 -18
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-common.ts +0 -20
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-components.ts +0 -34
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +0 -24
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime.ts +0 -138
- package/src/extensions/nocobase-ai/components/tools/sub-agent-renderer.tsx +0 -67
- package/src/extensions/nocobase-ai/components/tools/suggestions-renderer.tsx +0 -59
- package/src/extensions/nocobase-ai/components/tools/tool-renderer-provider.tsx +0 -55
- package/src/extensions/nocobase-ai/components/tools/tool-renderer-utils.ts +0 -18
- package/src/extensions/nocobase-ai/components/tools/workflow-renderer.tsx +0 -108
- package/src/extensions/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +0 -73
- package/src/extensions/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +0 -149
- package/src/extensions/nocobase-ai/demo/configuration-gate.tsx +0 -49
- package/src/extensions/nocobase-ai/demo/container-showcase.tsx +0 -210
- package/src/extensions/nocobase-ai/demo/floating.tsx +0 -134
- package/src/extensions/nocobase-ai/demo/index.tsx +0 -346
- package/src/extensions/nocobase-ai/demo/interaction-showcase.tsx +0 -151
- package/src/extensions/nocobase-ai/demo/page-context-prompt-generator.tsx +0 -355
- package/src/extensions/nocobase-ai/demo/page-context.tsx +0 -884
- package/src/extensions/nocobase-ai/demo/page-element-showcase.tsx +0 -197
- package/src/extensions/nocobase-ai/demo/prompt-card.tsx +0 -41
- package/src/extensions/nocobase-ai/demo/prompt-generator.tsx +0 -452
- package/src/extensions/nocobase-ai/demo/shortcut.tsx +0 -1207
- package/src/extensions/nocobase-ai/demo/tool-cards.tsx +0 -530
- package/src/extensions/nocobase-ai/extension.tsx +0 -158
- package/src/extensions/nocobase-ai/global-ai-chat.tsx +0 -213
- package/src/extensions/nocobase-ai/index.ts +0 -3
- package/src/extensions/nocobase-ai/providers/ai-provider.tsx +0 -323
- package/src/extensions/nocobase-ai/providers/avatars.ts +0 -817
- package/src/extensions/nocobase-ai/providers/chat-context.tsx +0 -109
- package/src/extensions/nocobase-ai/providers/chat-controller.ts +0 -84
- package/src/extensions/nocobase-ai/providers/chat-message-utils.ts +0 -198
- package/src/extensions/nocobase-ai/providers/chat-provider.tsx +0 -871
- package/src/extensions/nocobase-ai/providers/chat-reducer.ts +0 -147
- package/src/extensions/nocobase-ai/providers/chat-task-utils.ts +0 -105
- package/src/extensions/nocobase-ai/providers/chat-transport.ts +0 -184
- package/src/extensions/nocobase-ai/providers/form-registry.tsx +0 -271
- package/src/extensions/nocobase-ai/providers/frontend-tool-registry.tsx +0 -231
- package/src/extensions/nocobase-ai/providers/index.ts +0 -61
- package/src/extensions/nocobase-ai/providers/model.ts +0 -32
- package/src/extensions/nocobase-ai/providers/page-context.tsx +0 -136
- package/src/extensions/nocobase-ai/providers/stream-coalescer.ts +0 -60
- package/src/extensions/nocobase-ai/providers/stream-event-utils.ts +0 -82
- package/src/extensions/nocobase-ai/providers/stream-parser.ts +0 -61
- package/src/extensions/nocobase-ai/providers/sub-agent-stream.ts +0 -316
- package/src/extensions/nocobase-ai/providers/types.ts +0 -183
- package/src/extensions/nocobase-ai/providers/ui-message-stream.ts +0 -463
- package/src/extensions/nocobase-ai/providers/use-automatic-tool-approval.ts +0 -94
- package/src/extensions/nocobase-ai/providers/use-chat-attachments.ts +0 -206
- package/src/extensions/nocobase-ai/providers/use-chat-message-actions.ts +0 -554
- package/src/extensions/nocobase-ai/providers/use-chat-runtime.ts +0 -210
- package/src/extensions/nocobase-ai/providers/use-chat-work-context.ts +0 -93
- package/src/extensions/nocobase-ai/providers/use-conversation-catalog.ts +0 -108
- package/src/extensions/nocobase-ai/providers/use-conversation-history.ts +0 -178
- package/src/extensions/nocobase-ai/services/index.ts +0 -7
- package/src/extensions/nocobase-ai/services/nocobase-ai-service.ts +0 -454
- package/src/extensions/nocobase-ai/services/types.ts +0 -82
- /package/{registry/nocobase-i18n → src/providers/i18n}/i18n-provider.ts +0 -0
- /package/{registry/nocobase-i18n → src/providers/i18n}/locale-store.ts +0 -0
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { Button } from "@/components/ui/button";
|
|
2
|
-
import {
|
|
3
|
-
Dialog,
|
|
4
|
-
DialogContent,
|
|
5
|
-
DialogDescription,
|
|
6
|
-
DialogTitle,
|
|
7
|
-
} from "@/components/ui/dialog";
|
|
8
|
-
import { History, Menu } from "lucide-react";
|
|
9
|
-
import { useEffect, useState, type ReactNode } from "react";
|
|
10
|
-
import type { AIToolCallDecision } from "../../providers";
|
|
11
|
-
import { ChatMessages } from "./chat-messages";
|
|
12
|
-
import { ConversationList } from "./conversation-list";
|
|
13
|
-
|
|
14
|
-
export function AIChatHistoryDialog({
|
|
15
|
-
open: controlledOpen,
|
|
16
|
-
onOpenChange,
|
|
17
|
-
trigger,
|
|
18
|
-
onToolCallDecision,
|
|
19
|
-
}: {
|
|
20
|
-
open?: boolean;
|
|
21
|
-
onOpenChange?: (open: boolean) => void;
|
|
22
|
-
trigger?: ReactNode;
|
|
23
|
-
onToolCallDecision?: (decision: AIToolCallDecision) => void | Promise<void>;
|
|
24
|
-
}) {
|
|
25
|
-
const [internalOpen, setInternalOpen] = useState(false);
|
|
26
|
-
const open = controlledOpen ?? internalOpen;
|
|
27
|
-
const setOpen = onOpenChange ?? setInternalOpen;
|
|
28
|
-
const [conversationListVisible, setConversationListVisible] = useState(true);
|
|
29
|
-
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
if (open) setConversationListVisible(true);
|
|
32
|
-
}, [open]);
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<>
|
|
36
|
-
{trigger === null ? null : (
|
|
37
|
-
<span onClick={() => setOpen(true)}>
|
|
38
|
-
{trigger ?? (
|
|
39
|
-
<Button variant="ghost" size="sm">
|
|
40
|
-
<History /> Message history
|
|
41
|
-
</Button>
|
|
42
|
-
)}
|
|
43
|
-
</span>
|
|
44
|
-
)}
|
|
45
|
-
<Dialog open={open} onOpenChange={setOpen}>
|
|
46
|
-
<DialogContent className="flex h-[82svh] w-[min(1040px,calc(100vw-2rem))] max-w-[1040px] flex-col gap-0 overflow-hidden p-0 sm:max-w-[1040px]">
|
|
47
|
-
<div className="flex h-12 shrink-0 items-center gap-2 border-b px-3 pr-12">
|
|
48
|
-
{!conversationListVisible ? (
|
|
49
|
-
<Button
|
|
50
|
-
variant="ghost"
|
|
51
|
-
size="icon-sm"
|
|
52
|
-
aria-label="Show conversations"
|
|
53
|
-
onClick={() => setConversationListVisible(true)}
|
|
54
|
-
>
|
|
55
|
-
<Menu />
|
|
56
|
-
</Button>
|
|
57
|
-
) : null}
|
|
58
|
-
<DialogTitle>Message history</DialogTitle>
|
|
59
|
-
</div>
|
|
60
|
-
<DialogDescription className="sr-only">
|
|
61
|
-
Browse conversations and inspect the full message history.
|
|
62
|
-
</DialogDescription>
|
|
63
|
-
<div className="flex min-h-0 flex-1 overflow-hidden">
|
|
64
|
-
{conversationListVisible ? (
|
|
65
|
-
<aside className="absolute top-12 bottom-0 left-0 z-20 w-[280px] border-r bg-card sm:relative sm:inset-auto sm:z-auto sm:block sm:w-[300px] sm:shrink-0">
|
|
66
|
-
<ConversationList
|
|
67
|
-
onClose={() => setConversationListVisible(false)}
|
|
68
|
-
/>
|
|
69
|
-
</aside>
|
|
70
|
-
) : null}
|
|
71
|
-
<ChatMessages onToolCallDecision={onToolCallDecision} />
|
|
72
|
-
</div>
|
|
73
|
-
</DialogContent>
|
|
74
|
-
</Dialog>
|
|
75
|
-
</>
|
|
76
|
-
);
|
|
77
|
-
}
|
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
import { Button } from "@/components/ui/button";
|
|
2
|
-
import {
|
|
3
|
-
type AIChatMessage as AIChatMessageType,
|
|
4
|
-
type AIToolCallDecision,
|
|
5
|
-
} from "../../providers";
|
|
6
|
-
import { Check, Copy, Pencil, RefreshCcw } from "lucide-react";
|
|
7
|
-
import { memo, useState } from "react";
|
|
8
|
-
import { MarkdownMessage } from "./markdown-message";
|
|
9
|
-
import { ReasoningPanel } from "./reasoning-panel";
|
|
10
|
-
import {
|
|
11
|
-
getToolCallName,
|
|
12
|
-
isToolCallPart,
|
|
13
|
-
ToolCallCard,
|
|
14
|
-
} from "./tool-call-card";
|
|
15
|
-
import { ChatAttachment } from "./chat-attachment";
|
|
16
|
-
import { useAIToolRenderer } from "../tools/tool-renderer-provider";
|
|
17
|
-
import { SubAgentConversation } from "./sub-agent-conversation";
|
|
18
|
-
import { WorkContextChip } from "./work-context-chip";
|
|
19
|
-
|
|
20
|
-
type ChatMessageProps = {
|
|
21
|
-
message: AIChatMessageType;
|
|
22
|
-
onToolCallDecision?: (decision: AIToolCallDecision) => void | Promise<void>;
|
|
23
|
-
showActions?: boolean;
|
|
24
|
-
status?: "submitted" | "streaming" | "ready" | "error";
|
|
25
|
-
retryMessage?: (message: AIChatMessageType) => Promise<void>;
|
|
26
|
-
decideToolCall?: (decision: AIToolCallDecision) => Promise<void>;
|
|
27
|
-
startEditingMessage?: (message: AIChatMessageType) => Promise<void>;
|
|
28
|
-
focusComposer?: () => void;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
function ChatMessageComponent({
|
|
32
|
-
message,
|
|
33
|
-
onToolCallDecision,
|
|
34
|
-
showActions = true,
|
|
35
|
-
status = "ready",
|
|
36
|
-
retryMessage,
|
|
37
|
-
decideToolCall,
|
|
38
|
-
startEditingMessage,
|
|
39
|
-
focusComposer,
|
|
40
|
-
}: ChatMessageProps) {
|
|
41
|
-
const interactionPending = status === "streaming" || status === "submitted";
|
|
42
|
-
const [copied, setCopied] = useState(false);
|
|
43
|
-
const isUser = message.role === "user";
|
|
44
|
-
const text = message.parts
|
|
45
|
-
.filter((part) => part.type === "text")
|
|
46
|
-
.map((part) => part.text)
|
|
47
|
-
.join("\n");
|
|
48
|
-
const toolCalls = message.parts.filter(isToolCallPart);
|
|
49
|
-
const singleToolCall = toolCalls.length === 1 ? toolCalls[0] : undefined;
|
|
50
|
-
const singleToolRenderer = useAIToolRenderer(
|
|
51
|
-
singleToolCall ? getToolCallName(singleToolCall) : ""
|
|
52
|
-
);
|
|
53
|
-
const useInlineToolActions =
|
|
54
|
-
!text && Boolean(singleToolCall) && !singleToolRenderer;
|
|
55
|
-
const assistantParts = message.parts.filter(
|
|
56
|
-
(part) =>
|
|
57
|
-
part.type === "text" ||
|
|
58
|
-
part.type === "reasoning" ||
|
|
59
|
-
part.type === "data-subAgent" ||
|
|
60
|
-
isToolCallPart(part)
|
|
61
|
-
);
|
|
62
|
-
const showGenerating = !assistantParts.length && interactionPending;
|
|
63
|
-
const attachments = message.metadata?.attachments ?? [];
|
|
64
|
-
const workContext = message.metadata?.workContext ?? [];
|
|
65
|
-
if (isUser) {
|
|
66
|
-
return (
|
|
67
|
-
<article className="group/message flex flex-col items-end px-4 py-2 sm:px-5">
|
|
68
|
-
<div className="flex max-w-[80%] flex-col items-end">
|
|
69
|
-
{workContext.length ? (
|
|
70
|
-
<div className="mb-1.5 flex max-w-full flex-wrap justify-end gap-1.5">
|
|
71
|
-
{workContext.map((item, index) => (
|
|
72
|
-
<WorkContextChip
|
|
73
|
-
key={`${item.type}:${item.id ?? item.title ?? index}`}
|
|
74
|
-
item={item}
|
|
75
|
-
className="bg-background"
|
|
76
|
-
/>
|
|
77
|
-
))}
|
|
78
|
-
</div>
|
|
79
|
-
) : null}
|
|
80
|
-
{attachments.length ? (
|
|
81
|
-
<div className="mb-1.5 flex max-w-full flex-wrap justify-end gap-1.5">
|
|
82
|
-
{attachments.map((attachment) => (
|
|
83
|
-
<ChatAttachment key={attachment.uid} attachment={attachment} />
|
|
84
|
-
))}
|
|
85
|
-
</div>
|
|
86
|
-
) : null}
|
|
87
|
-
{text ? (
|
|
88
|
-
<div className="rounded-2xl rounded-br-md bg-secondary px-4 py-2.5 text-sm leading-6 text-secondary-foreground">
|
|
89
|
-
<div className="whitespace-pre-wrap break-words">{text}</div>
|
|
90
|
-
</div>
|
|
91
|
-
) : null}
|
|
92
|
-
</div>
|
|
93
|
-
{showActions ? (
|
|
94
|
-
<div className="pointer-events-none mt-1 flex h-6 items-center gap-1 opacity-0 transition-opacity group-hover/message:pointer-events-auto group-hover/message:opacity-100 focus-within:pointer-events-auto focus-within:opacity-100">
|
|
95
|
-
<Button
|
|
96
|
-
variant="ghost"
|
|
97
|
-
size="icon-xs"
|
|
98
|
-
aria-label="Edit message"
|
|
99
|
-
disabled={interactionPending || !startEditingMessage}
|
|
100
|
-
onClick={() => void startEditingMessage?.(message)}
|
|
101
|
-
>
|
|
102
|
-
<Pencil />
|
|
103
|
-
</Button>
|
|
104
|
-
{text ? (
|
|
105
|
-
<Button
|
|
106
|
-
variant="ghost"
|
|
107
|
-
size="icon-xs"
|
|
108
|
-
aria-label="Copy message"
|
|
109
|
-
onClick={async () => {
|
|
110
|
-
await navigator.clipboard.writeText(text);
|
|
111
|
-
setCopied(true);
|
|
112
|
-
window.setTimeout(() => setCopied(false), 1200);
|
|
113
|
-
}}
|
|
114
|
-
>
|
|
115
|
-
{copied ? <Check /> : <Copy />}
|
|
116
|
-
</Button>
|
|
117
|
-
) : null}
|
|
118
|
-
</div>
|
|
119
|
-
) : null}
|
|
120
|
-
</article>
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const messageActions = (
|
|
125
|
-
<>
|
|
126
|
-
{text ? (
|
|
127
|
-
<Button
|
|
128
|
-
variant="ghost"
|
|
129
|
-
size="icon-xs"
|
|
130
|
-
aria-label="Copy response"
|
|
131
|
-
onClick={async () => {
|
|
132
|
-
await navigator.clipboard.writeText(text);
|
|
133
|
-
setCopied(true);
|
|
134
|
-
window.setTimeout(() => setCopied(false), 1200);
|
|
135
|
-
}}
|
|
136
|
-
>
|
|
137
|
-
{copied ? <Check /> : <Copy />}
|
|
138
|
-
</Button>
|
|
139
|
-
) : null}
|
|
140
|
-
<Button
|
|
141
|
-
variant="ghost"
|
|
142
|
-
size="icon-xs"
|
|
143
|
-
aria-label="Retry response"
|
|
144
|
-
disabled={interactionPending || !retryMessage}
|
|
145
|
-
onClick={() => void retryMessage?.(message)}
|
|
146
|
-
>
|
|
147
|
-
<RefreshCcw />
|
|
148
|
-
</Button>
|
|
149
|
-
</>
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
return (
|
|
153
|
-
<article className="group/message px-4 py-2 sm:px-5">
|
|
154
|
-
<div className="min-w-0 space-y-4">
|
|
155
|
-
{assistantParts.map((part, index) => {
|
|
156
|
-
if (part.type === "reasoning") {
|
|
157
|
-
return (
|
|
158
|
-
<ReasoningPanel
|
|
159
|
-
key={`reasoning-${index}`}
|
|
160
|
-
streaming={part.state === "streaming"}
|
|
161
|
-
>
|
|
162
|
-
{part.text}
|
|
163
|
-
</ReasoningPanel>
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
if (part.type === "text") {
|
|
167
|
-
return (
|
|
168
|
-
<div
|
|
169
|
-
key={`text-${index}`}
|
|
170
|
-
className="ai-markdown text-sm leading-6 text-foreground"
|
|
171
|
-
>
|
|
172
|
-
<MarkdownMessage>{part.text}</MarkdownMessage>
|
|
173
|
-
</div>
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
if (part.type === "data-subAgent") {
|
|
177
|
-
return (
|
|
178
|
-
<SubAgentConversation
|
|
179
|
-
key={part.id ?? part.data.sessionId}
|
|
180
|
-
conversation={part.data}
|
|
181
|
-
onToolCallDecision={onToolCallDecision}
|
|
182
|
-
status={status}
|
|
183
|
-
decideToolCall={decideToolCall}
|
|
184
|
-
focusComposer={focusComposer}
|
|
185
|
-
/>
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
if (!isToolCallPart(part)) return null;
|
|
189
|
-
return (
|
|
190
|
-
<ToolCallCard
|
|
191
|
-
key={part.toolCallId}
|
|
192
|
-
part={part}
|
|
193
|
-
approval={message.metadata?.toolApprovals?.[part.toolCallId]}
|
|
194
|
-
disabled={interactionPending}
|
|
195
|
-
onRevise={focusComposer}
|
|
196
|
-
inlineActions={
|
|
197
|
-
showActions && useInlineToolActions && part === singleToolCall
|
|
198
|
-
? messageActions
|
|
199
|
-
: undefined
|
|
200
|
-
}
|
|
201
|
-
onDecision={async (decision, input) => {
|
|
202
|
-
const toolDecision = {
|
|
203
|
-
messageId: message.id,
|
|
204
|
-
toolCallId: part.toolCallId,
|
|
205
|
-
toolName: getToolCallName(part),
|
|
206
|
-
decision,
|
|
207
|
-
input,
|
|
208
|
-
} satisfies AIToolCallDecision;
|
|
209
|
-
await decideToolCall?.(toolDecision);
|
|
210
|
-
try {
|
|
211
|
-
await onToolCallDecision?.(toolDecision);
|
|
212
|
-
} catch (error) {
|
|
213
|
-
console.error("Tool-call decision callback failed", error);
|
|
214
|
-
}
|
|
215
|
-
}}
|
|
216
|
-
/>
|
|
217
|
-
);
|
|
218
|
-
})}
|
|
219
|
-
{showGenerating ? (
|
|
220
|
-
<div className="min-h-6 text-sm leading-6 text-foreground">
|
|
221
|
-
<span
|
|
222
|
-
className="inline-flex gap-1 py-2"
|
|
223
|
-
aria-label="Generating response"
|
|
224
|
-
>
|
|
225
|
-
<span className="size-1.5 animate-pulse rounded-full bg-muted-foreground/70" />
|
|
226
|
-
<span className="size-1.5 animate-pulse rounded-full bg-muted-foreground/70 [animation-delay:120ms]" />
|
|
227
|
-
<span className="size-1.5 animate-pulse rounded-full bg-muted-foreground/70 [animation-delay:240ms]" />
|
|
228
|
-
</span>
|
|
229
|
-
</div>
|
|
230
|
-
) : null}
|
|
231
|
-
</div>
|
|
232
|
-
{showActions && (text || (toolCalls.length && !useInlineToolActions)) ? (
|
|
233
|
-
<div className="pointer-events-none mt-1 flex h-6 items-center gap-1 opacity-0 transition-opacity group-hover/message:pointer-events-auto group-hover/message:opacity-100 focus-within:pointer-events-auto focus-within:opacity-100">
|
|
234
|
-
{messageActions}
|
|
235
|
-
</div>
|
|
236
|
-
) : null}
|
|
237
|
-
</article>
|
|
238
|
-
);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
export const ChatMessage = memo(
|
|
242
|
-
ChatMessageComponent,
|
|
243
|
-
(previous, next) =>
|
|
244
|
-
previous.message === next.message &&
|
|
245
|
-
previous.status === next.status &&
|
|
246
|
-
previous.showActions === next.showActions &&
|
|
247
|
-
previous.onToolCallDecision === next.onToolCallDecision &&
|
|
248
|
-
previous.retryMessage === next.retryMessage &&
|
|
249
|
-
previous.decideToolCall === next.decideToolCall &&
|
|
250
|
-
previous.startEditingMessage === next.startEditingMessage &&
|
|
251
|
-
previous.focusComposer === next.focusComposer
|
|
252
|
-
);
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import { Button } from "@/components/ui/button";
|
|
2
|
-
import { LoadingState } from "@/components/app-shell/loading-state";
|
|
3
|
-
import {
|
|
4
|
-
useAIChatBase,
|
|
5
|
-
useAIChatMessages,
|
|
6
|
-
useAIChatStatus,
|
|
7
|
-
type AIChatMessage,
|
|
8
|
-
type AIToolCallDecision,
|
|
9
|
-
} from "../../providers";
|
|
10
|
-
import { cn } from "@/lib/utils";
|
|
11
|
-
import { ArrowDown } from "lucide-react";
|
|
12
|
-
import { useEffect, useRef, useState, type ReactNode } from "react";
|
|
13
|
-
import { ChatEmptyState } from "./chat-empty-state";
|
|
14
|
-
import { ChatMessage } from "./chat-message";
|
|
15
|
-
|
|
16
|
-
export function ChatMessages({
|
|
17
|
-
onToolCallDecision,
|
|
18
|
-
}: {
|
|
19
|
-
onToolCallDecision?: (decision: AIToolCallDecision) => void | Promise<void>;
|
|
20
|
-
}) {
|
|
21
|
-
const { messages } = useAIChatMessages();
|
|
22
|
-
const { status, error } = useAIChatStatus();
|
|
23
|
-
const {
|
|
24
|
-
messagesLoading,
|
|
25
|
-
historyError,
|
|
26
|
-
interactionError,
|
|
27
|
-
retryMessage,
|
|
28
|
-
decideToolCall,
|
|
29
|
-
startEditingMessage,
|
|
30
|
-
focusComposer,
|
|
31
|
-
} = useAIChatBase();
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<AIChatMessageList
|
|
35
|
-
messages={messages}
|
|
36
|
-
status={status}
|
|
37
|
-
loading={messagesLoading}
|
|
38
|
-
error={error}
|
|
39
|
-
historyError={historyError}
|
|
40
|
-
interactionError={interactionError}
|
|
41
|
-
onToolCallDecision={onToolCallDecision}
|
|
42
|
-
retryMessage={retryMessage}
|
|
43
|
-
decideToolCall={decideToolCall}
|
|
44
|
-
startEditingMessage={startEditingMessage}
|
|
45
|
-
focusComposer={focusComposer}
|
|
46
|
-
/>
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export type AIChatMessageListProps = {
|
|
51
|
-
messages: AIChatMessage[];
|
|
52
|
-
status?: "submitted" | "streaming" | "ready" | "error";
|
|
53
|
-
loading?: boolean;
|
|
54
|
-
error?: Error | null;
|
|
55
|
-
historyError?: Error | null;
|
|
56
|
-
interactionError?: Error | null;
|
|
57
|
-
emptyState?: ReactNode;
|
|
58
|
-
className?: string;
|
|
59
|
-
onToolCallDecision?: (decision: AIToolCallDecision) => void | Promise<void>;
|
|
60
|
-
showMessageActions?: boolean;
|
|
61
|
-
retryMessage?: (message: AIChatMessage) => Promise<void>;
|
|
62
|
-
decideToolCall?: (decision: AIToolCallDecision) => Promise<void>;
|
|
63
|
-
startEditingMessage?: (message: AIChatMessage) => Promise<void>;
|
|
64
|
-
focusComposer?: () => void;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
export function AIChatMessageList({
|
|
68
|
-
messages,
|
|
69
|
-
status = "ready",
|
|
70
|
-
loading = false,
|
|
71
|
-
error,
|
|
72
|
-
historyError,
|
|
73
|
-
interactionError,
|
|
74
|
-
emptyState,
|
|
75
|
-
className,
|
|
76
|
-
onToolCallDecision,
|
|
77
|
-
showMessageActions = true,
|
|
78
|
-
retryMessage,
|
|
79
|
-
decideToolCall,
|
|
80
|
-
startEditingMessage,
|
|
81
|
-
focusComposer,
|
|
82
|
-
}: AIChatMessageListProps) {
|
|
83
|
-
const viewportRef = useRef<HTMLDivElement>(null);
|
|
84
|
-
const [atBottom, setAtBottom] = useState(true);
|
|
85
|
-
|
|
86
|
-
const scrollToBottom = (behavior: ScrollBehavior = "smooth") => {
|
|
87
|
-
const viewport = viewportRef.current;
|
|
88
|
-
if (!viewport) return;
|
|
89
|
-
viewport.scrollTo({ top: viewport.scrollHeight, behavior });
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
useEffect(() => {
|
|
93
|
-
if (atBottom) scrollToBottom(status === "streaming" ? "auto" : "smooth");
|
|
94
|
-
}, [atBottom, messages, status]);
|
|
95
|
-
|
|
96
|
-
return (
|
|
97
|
-
<div className={cn("relative min-h-0 flex-1 bg-background", className)}>
|
|
98
|
-
<div
|
|
99
|
-
ref={viewportRef}
|
|
100
|
-
role="log"
|
|
101
|
-
aria-live="polite"
|
|
102
|
-
className="absolute inset-0 overflow-y-auto overscroll-contain"
|
|
103
|
-
onScroll={(event) => {
|
|
104
|
-
const element = event.currentTarget;
|
|
105
|
-
setAtBottom(
|
|
106
|
-
element.scrollHeight - element.scrollTop - element.clientHeight < 48
|
|
107
|
-
);
|
|
108
|
-
}}
|
|
109
|
-
>
|
|
110
|
-
{loading ? (
|
|
111
|
-
<LoadingState className="h-full" />
|
|
112
|
-
) : (
|
|
113
|
-
<>
|
|
114
|
-
{messages.length ? (
|
|
115
|
-
<div className="mx-auto w-full max-w-3xl py-2">
|
|
116
|
-
{messages.map((message) => (
|
|
117
|
-
<ChatMessage
|
|
118
|
-
key={message.id}
|
|
119
|
-
message={message}
|
|
120
|
-
onToolCallDecision={onToolCallDecision}
|
|
121
|
-
showActions={showMessageActions}
|
|
122
|
-
status={status}
|
|
123
|
-
retryMessage={retryMessage}
|
|
124
|
-
decideToolCall={decideToolCall}
|
|
125
|
-
startEditingMessage={startEditingMessage}
|
|
126
|
-
focusComposer={focusComposer}
|
|
127
|
-
/>
|
|
128
|
-
))}
|
|
129
|
-
</div>
|
|
130
|
-
) : (
|
|
131
|
-
emptyState ?? <ChatEmptyState />
|
|
132
|
-
)}
|
|
133
|
-
{error ? (
|
|
134
|
-
<div
|
|
135
|
-
role="alert"
|
|
136
|
-
className="mx-5 my-3 rounded-lg border border-destructive/25 bg-destructive/5 px-3 py-2 text-sm text-destructive"
|
|
137
|
-
>
|
|
138
|
-
{error.message}
|
|
139
|
-
</div>
|
|
140
|
-
) : null}
|
|
141
|
-
{historyError ? (
|
|
142
|
-
<div
|
|
143
|
-
role="alert"
|
|
144
|
-
className="mx-5 my-3 rounded-lg border border-destructive/25 bg-destructive/5 px-3 py-2 text-sm text-destructive"
|
|
145
|
-
>
|
|
146
|
-
{historyError.message}
|
|
147
|
-
</div>
|
|
148
|
-
) : null}
|
|
149
|
-
{interactionError ? (
|
|
150
|
-
<div
|
|
151
|
-
role="alert"
|
|
152
|
-
className="mx-5 my-3 rounded-lg border border-destructive/25 bg-destructive/5 px-3 py-2 text-sm text-destructive"
|
|
153
|
-
>
|
|
154
|
-
{interactionError.message}
|
|
155
|
-
</div>
|
|
156
|
-
) : null}
|
|
157
|
-
</>
|
|
158
|
-
)}
|
|
159
|
-
</div>
|
|
160
|
-
{!atBottom ? (
|
|
161
|
-
<Button
|
|
162
|
-
size="icon-sm"
|
|
163
|
-
variant="outline"
|
|
164
|
-
className="absolute bottom-3 left-1/2 z-10 -translate-x-1/2 rounded-full bg-background shadow-sm"
|
|
165
|
-
aria-label="Scroll to bottom"
|
|
166
|
-
onClick={() => scrollToBottom()}
|
|
167
|
-
>
|
|
168
|
-
<ArrowDown />
|
|
169
|
-
</Button>
|
|
170
|
-
) : null}
|
|
171
|
-
</div>
|
|
172
|
-
);
|
|
173
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { cn } from "@/lib/utils";
|
|
2
|
-
import { useAIChatBase, type AIToolCallDecision } from "../../providers";
|
|
3
|
-
import { useRef, useState, type DragEvent, type ReactNode } from "react";
|
|
4
|
-
import { ChatComposer, type AIChatComposerAction } from "./chat-composer";
|
|
5
|
-
import { ChatHeader } from "./chat-header";
|
|
6
|
-
import { ChatMessages } from "./chat-messages";
|
|
7
|
-
import { ConversationList } from "./conversation-list";
|
|
8
|
-
|
|
9
|
-
export function AIChatWindow({
|
|
10
|
-
className,
|
|
11
|
-
headerActions,
|
|
12
|
-
composerActions,
|
|
13
|
-
showConversationToggle = true,
|
|
14
|
-
showNewConversation = true,
|
|
15
|
-
showEmployeeSelector = true,
|
|
16
|
-
showModelSelector = true,
|
|
17
|
-
showUserPrompt = true,
|
|
18
|
-
enableAttachments = false,
|
|
19
|
-
attachmentActionIndex = 0,
|
|
20
|
-
placeholder,
|
|
21
|
-
disclaimer,
|
|
22
|
-
onToolCallDecision,
|
|
23
|
-
}: AIChatWindowProps) {
|
|
24
|
-
const { conversationListOpen, setConversationListOpen, uploadFiles } =
|
|
25
|
-
useAIChatBase();
|
|
26
|
-
const [draggingFiles, setDraggingFiles] = useState(false);
|
|
27
|
-
const dragDepthRef = useRef(0);
|
|
28
|
-
|
|
29
|
-
const hasFiles = (event: DragEvent) =>
|
|
30
|
-
Array.from(event.dataTransfer.types).includes("Files");
|
|
31
|
-
|
|
32
|
-
return (
|
|
33
|
-
<section
|
|
34
|
-
className={cn(
|
|
35
|
-
"ai-chat-window relative flex h-full min-h-0 w-full overflow-hidden bg-background text-foreground",
|
|
36
|
-
className
|
|
37
|
-
)}
|
|
38
|
-
onDragEnter={(event) => {
|
|
39
|
-
if (!enableAttachments || !hasFiles(event)) return;
|
|
40
|
-
event.preventDefault();
|
|
41
|
-
dragDepthRef.current += 1;
|
|
42
|
-
setDraggingFiles(true);
|
|
43
|
-
}}
|
|
44
|
-
onDragOver={(event) => {
|
|
45
|
-
if (!enableAttachments || !hasFiles(event)) return;
|
|
46
|
-
event.preventDefault();
|
|
47
|
-
event.dataTransfer.dropEffect = "copy";
|
|
48
|
-
}}
|
|
49
|
-
onDragLeave={(event) => {
|
|
50
|
-
if (!enableAttachments || !hasFiles(event)) return;
|
|
51
|
-
event.preventDefault();
|
|
52
|
-
dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);
|
|
53
|
-
if (dragDepthRef.current === 0) setDraggingFiles(false);
|
|
54
|
-
}}
|
|
55
|
-
onDrop={(event) => {
|
|
56
|
-
if (!enableAttachments || !hasFiles(event)) return;
|
|
57
|
-
event.preventDefault();
|
|
58
|
-
dragDepthRef.current = 0;
|
|
59
|
-
setDraggingFiles(false);
|
|
60
|
-
const files = Array.from(event.dataTransfer.files);
|
|
61
|
-
if (files.length) void uploadFiles(files);
|
|
62
|
-
}}
|
|
63
|
-
>
|
|
64
|
-
{draggingFiles ? (
|
|
65
|
-
<div className="pointer-events-none absolute inset-2 z-50 flex items-center justify-center rounded-xl border-2 border-dashed border-foreground/25 bg-background/90 text-sm font-medium shadow-sm backdrop-blur-sm">
|
|
66
|
-
Drop files to upload
|
|
67
|
-
</div>
|
|
68
|
-
) : null}
|
|
69
|
-
{conversationListOpen && showConversationToggle ? (
|
|
70
|
-
<button
|
|
71
|
-
type="button"
|
|
72
|
-
className="ai-chat-conversation-backdrop absolute inset-0 z-20 bg-black/25 backdrop-blur-[1px]"
|
|
73
|
-
aria-label="Close conversation list"
|
|
74
|
-
onClick={() => setConversationListOpen(false)}
|
|
75
|
-
/>
|
|
76
|
-
) : null}
|
|
77
|
-
{conversationListOpen && showConversationToggle ? (
|
|
78
|
-
<aside className="ai-chat-conversation-panel z-30 flex h-full w-[300px] shrink-0 border-r bg-card">
|
|
79
|
-
<ConversationList />
|
|
80
|
-
</aside>
|
|
81
|
-
) : null}
|
|
82
|
-
<div className="flex min-w-0 flex-1 flex-col">
|
|
83
|
-
<ChatHeader
|
|
84
|
-
actions={headerActions}
|
|
85
|
-
showConversationToggle={showConversationToggle}
|
|
86
|
-
showNewConversation={showNewConversation}
|
|
87
|
-
showUserPrompt={showUserPrompt}
|
|
88
|
-
/>
|
|
89
|
-
<ChatMessages onToolCallDecision={onToolCallDecision} />
|
|
90
|
-
<ChatComposer
|
|
91
|
-
actions={composerActions}
|
|
92
|
-
showEmployeeSelector={showEmployeeSelector}
|
|
93
|
-
showModelSelector={showModelSelector}
|
|
94
|
-
enableAttachments={enableAttachments}
|
|
95
|
-
attachmentActionIndex={attachmentActionIndex}
|
|
96
|
-
placeholder={placeholder}
|
|
97
|
-
disclaimer={disclaimer}
|
|
98
|
-
/>
|
|
99
|
-
</div>
|
|
100
|
-
</section>
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export type AIChatWindowProps = {
|
|
105
|
-
className?: string;
|
|
106
|
-
headerActions?: ReactNode;
|
|
107
|
-
composerActions?: AIChatComposerAction[];
|
|
108
|
-
showConversationToggle?: boolean;
|
|
109
|
-
showNewConversation?: boolean;
|
|
110
|
-
showEmployeeSelector?: boolean;
|
|
111
|
-
showModelSelector?: boolean;
|
|
112
|
-
showUserPrompt?: boolean;
|
|
113
|
-
enableAttachments?: boolean;
|
|
114
|
-
attachmentActionIndex?: number;
|
|
115
|
-
placeholder?: string;
|
|
116
|
-
disclaimer?: ReactNode | false;
|
|
117
|
-
onToolCallDecision?: (decision: AIToolCallDecision) => void | Promise<void>;
|
|
118
|
-
};
|