@nocobase/portal-template-default 1.0.1 → 1.0.3
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 +22 -3
- package/package.json +4 -2
- package/registry/nocobase-acl/components/role-options.ts +7 -0
- package/registry/nocobase-acl/components/role-switcher.tsx +9 -4
- package/registry/nocobase-acl/demo/boundary-api.tsx +87 -16
- package/registry/nocobase-acl/demo/components.tsx +32 -11
- package/registry/nocobase-acl/demo/index.tsx +45 -14
- package/registry/nocobase-acl/demo/lazy-route.tsx +2 -7
- package/registry/nocobase-acl/demo/prompt-generator.tsx +52 -33
- package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +28 -9
- package/registry/nocobase-acl/extension.tsx +1 -1
- package/registry/nocobase-acl/tests/acl-regression.mjs +7 -6
- package/registry/nocobase-ai/components/chat/chat-messages.tsx +3 -4
- package/registry/nocobase-ai/components/chat/conversation-list.tsx +2 -4
- package/registry/nocobase-ai/components/tools/business-report-dialog.tsx +16 -19
- package/registry/nocobase-ai/components/tools/chart-renderer.tsx +3 -14
- package/registry/nocobase-ai/components/tools/echarts-preview.tsx +2 -5
- package/registry/nocobase-ai/demo/configuration-gate.tsx +9 -12
- package/registry/nocobase-ai/extension.tsx +2 -7
- package/registry/nocobase-i18n/README.md +53 -0
- package/registry/nocobase-i18n/components/index.ts +1 -0
- package/registry/nocobase-i18n/components/language-switcher.tsx +138 -0
- package/registry/nocobase-i18n/demo/index.tsx +209 -0
- package/registry/nocobase-i18n/demo/prompt-generator.tsx +155 -0
- package/registry/nocobase-i18n/extension.tsx +46 -0
- package/registry/nocobase-i18n/i18n-provider.ts +9 -0
- package/registry/nocobase-i18n/index.ts +6 -0
- package/registry/nocobase-i18n/locale-store.ts +134 -0
- package/registry/nocobase-i18n/locales/en-US.ts +89 -0
- package/registry/nocobase-i18n/locales/zh-CN.ts +85 -0
- package/registry/nocobase-i18n/provider.tsx +32 -0
- package/registry/nocobase-i18n/runtime.ts +118 -0
- package/registry/nocobase-i18n/server-resources.ts +184 -0
- package/registry/nocobase-i18n/tests/i18n-regression.mjs +42 -0
- package/registry.config.json +34 -14
- package/scripts/registry.mjs +5 -0
- package/src/App.tsx +27 -86
- package/src/app/extension.ts +3 -1
- package/src/app/extensions.tsx +21 -0
- package/src/components/access-control/acl-gate.tsx +4 -10
- package/src/components/app-shell/breadcrumb.tsx +39 -3
- package/src/components/app-shell/document-title-handler.tsx +11 -20
- package/src/components/app-shell/header.tsx +18 -3
- package/src/components/app-shell/loading-overlay.tsx +2 -4
- package/src/components/app-shell/loading-state.tsx +15 -0
- package/src/components/app-shell/sidebar.tsx +75 -47
- package/src/components/data-table/data-table-pagination.tsx +27 -7
- package/src/components/data-table/data-table-styles.ts +57 -0
- package/src/components/data-table/data-table.tsx +14 -59
- package/src/components/resources/resource-label.ts +101 -0
- package/src/components/resources/views/create-view.tsx +17 -14
- package/src/components/resources/views/edit-view.tsx +17 -15
- package/src/components/resources/views/list-view.tsx +20 -10
- package/src/components/resources/views/show-view.tsx +20 -18
- package/src/extensions/nocobase-acl/README.md +19 -0
- package/src/extensions/nocobase-acl/components/acl-boundary.tsx +106 -0
- package/src/extensions/nocobase-acl/components/index.ts +3 -0
- package/src/extensions/nocobase-acl/components/role-options.ts +36 -0
- package/src/extensions/nocobase-acl/components/role-switcher.tsx +135 -0
- package/src/extensions/nocobase-acl/demo/boundary-api.tsx +186 -0
- package/src/extensions/nocobase-acl/demo/components.tsx +231 -0
- package/src/extensions/nocobase-acl/demo/index.tsx +505 -0
- package/src/extensions/nocobase-acl/demo/lazy-route.tsx +10 -0
- package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +134 -0
- package/src/extensions/nocobase-acl/demo/role-switcher-prompt-generator.tsx +198 -0
- package/src/extensions/nocobase-acl/demo/scenario-section.tsx +37 -0
- package/src/extensions/nocobase-acl/extension.tsx +71 -0
- package/src/extensions/nocobase-acl/index.ts +1 -0
- package/src/extensions/nocobase-ai/README.md +70 -0
- package/src/extensions/nocobase-ai/adapters/react-hook-form.ts +20 -0
- package/src/extensions/nocobase-ai/assets/nocobase-ai-chat.svg +50 -0
- package/src/extensions/nocobase-ai/components/ai-root-provider.tsx +32 -0
- package/src/extensions/nocobase-ai/components/chat/ai-employee-avatar.tsx +31 -0
- package/src/extensions/nocobase-ai/components/chat/chat-attachment.tsx +92 -0
- package/src/extensions/nocobase-ai/components/chat/chat-compact.tsx +90 -0
- package/src/extensions/nocobase-ai/components/chat/chat-composer.tsx +429 -0
- package/src/extensions/nocobase-ai/components/chat/chat-empty-state.tsx +51 -0
- package/src/extensions/nocobase-ai/components/chat/chat-header.tsx +88 -0
- package/src/extensions/nocobase-ai/components/chat/chat-history-dialog.tsx +77 -0
- package/src/extensions/nocobase-ai/components/chat/chat-message.tsx +252 -0
- package/src/extensions/nocobase-ai/components/chat/chat-messages.tsx +173 -0
- package/src/extensions/nocobase-ai/components/chat/chat-window.tsx +118 -0
- package/src/extensions/nocobase-ai/components/chat/conversation-list.tsx +343 -0
- package/src/extensions/nocobase-ai/components/chat/markdown-message.tsx +165 -0
- package/src/extensions/nocobase-ai/components/chat/model-select-options.tsx +30 -0
- package/src/extensions/nocobase-ai/components/chat/reasoning-panel.tsx +29 -0
- package/src/extensions/nocobase-ai/components/chat/sub-agent-conversation.tsx +142 -0
- package/src/extensions/nocobase-ai/components/chat/tool-call-card.tsx +316 -0
- package/src/extensions/nocobase-ai/components/chat/user-prompt-editor.tsx +99 -0
- package/src/extensions/nocobase-ai/components/chat/work-context-chip.tsx +114 -0
- package/src/extensions/nocobase-ai/components/index.ts +59 -0
- package/src/extensions/nocobase-ai/components/page-elements/ai-form.tsx +44 -0
- package/src/extensions/nocobase-ai/components/page-elements/page-element-provider.tsx +469 -0
- package/src/extensions/nocobase-ai/components/surfaces/chat-dialog.tsx +31 -0
- package/src/extensions/nocobase-ai/components/surfaces/chat-inline.tsx +18 -0
- package/src/extensions/nocobase-ai/components/surfaces/chat-page.tsx +18 -0
- package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +54 -0
- package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel.tsx +74 -0
- package/src/extensions/nocobase-ai/components/surfaces/chat-surface-actions.tsx +54 -0
- package/src/extensions/nocobase-ai/components/tools/builtin-tool-renderers.tsx +31 -0
- package/src/extensions/nocobase-ai/components/tools/business-report-dialog.tsx +364 -0
- package/src/extensions/nocobase-ai/components/tools/business-report-renderer.tsx +122 -0
- package/src/extensions/nocobase-ai/components/tools/business-report-utils.tsx +282 -0
- package/src/extensions/nocobase-ai/components/tools/chart-renderer.tsx +99 -0
- package/src/extensions/nocobase-ai/components/tools/echarts-preview.tsx +59 -0
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-advanced.ts +18 -0
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-common.ts +20 -0
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-components.ts +34 -0
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +24 -0
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime.ts +138 -0
- package/src/extensions/nocobase-ai/components/tools/sub-agent-renderer.tsx +67 -0
- package/src/extensions/nocobase-ai/components/tools/suggestions-renderer.tsx +59 -0
- package/src/extensions/nocobase-ai/components/tools/tool-renderer-provider.tsx +55 -0
- package/src/extensions/nocobase-ai/components/tools/tool-renderer-utils.ts +18 -0
- package/src/extensions/nocobase-ai/components/tools/workflow-renderer.tsx +108 -0
- package/src/extensions/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +73 -0
- package/src/extensions/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +149 -0
- package/src/extensions/nocobase-ai/demo/configuration-gate.tsx +49 -0
- package/src/extensions/nocobase-ai/demo/container-showcase.tsx +210 -0
- package/src/extensions/nocobase-ai/demo/floating.tsx +134 -0
- package/src/extensions/nocobase-ai/demo/index.tsx +346 -0
- package/src/extensions/nocobase-ai/demo/interaction-showcase.tsx +151 -0
- package/src/extensions/nocobase-ai/demo/page-context-prompt-generator.tsx +355 -0
- package/src/extensions/nocobase-ai/demo/page-context.tsx +884 -0
- package/src/extensions/nocobase-ai/demo/page-element-showcase.tsx +197 -0
- package/src/extensions/nocobase-ai/demo/prompt-card.tsx +41 -0
- package/src/extensions/nocobase-ai/demo/prompt-generator.tsx +452 -0
- package/src/extensions/nocobase-ai/demo/shortcut.tsx +1207 -0
- package/src/extensions/nocobase-ai/demo/tool-cards.tsx +530 -0
- package/src/extensions/nocobase-ai/extension.tsx +158 -0
- package/src/extensions/nocobase-ai/global-ai-chat.tsx +213 -0
- package/src/extensions/nocobase-ai/index.ts +3 -0
- package/src/extensions/nocobase-ai/providers/ai-provider.tsx +323 -0
- package/src/extensions/nocobase-ai/providers/avatars.ts +817 -0
- package/src/extensions/nocobase-ai/providers/chat-context.tsx +109 -0
- package/src/extensions/nocobase-ai/providers/chat-controller.ts +84 -0
- package/src/extensions/nocobase-ai/providers/chat-message-utils.ts +198 -0
- package/src/extensions/nocobase-ai/providers/chat-provider.tsx +871 -0
- package/src/extensions/nocobase-ai/providers/chat-reducer.ts +147 -0
- package/src/extensions/nocobase-ai/providers/chat-task-utils.ts +105 -0
- package/src/extensions/nocobase-ai/providers/chat-transport.ts +184 -0
- package/src/extensions/nocobase-ai/providers/form-registry.tsx +271 -0
- package/src/extensions/nocobase-ai/providers/frontend-tool-registry.tsx +231 -0
- package/src/extensions/nocobase-ai/providers/index.ts +61 -0
- package/src/extensions/nocobase-ai/providers/model.ts +32 -0
- package/src/extensions/nocobase-ai/providers/page-context.tsx +136 -0
- package/src/extensions/nocobase-ai/providers/stream-coalescer.ts +60 -0
- package/src/extensions/nocobase-ai/providers/stream-event-utils.ts +82 -0
- package/src/extensions/nocobase-ai/providers/stream-parser.ts +61 -0
- package/src/extensions/nocobase-ai/providers/sub-agent-stream.ts +316 -0
- package/src/extensions/nocobase-ai/providers/types.ts +183 -0
- package/src/extensions/nocobase-ai/providers/ui-message-stream.ts +463 -0
- package/src/extensions/nocobase-ai/providers/use-automatic-tool-approval.ts +94 -0
- package/src/extensions/nocobase-ai/providers/use-chat-attachments.ts +206 -0
- package/src/extensions/nocobase-ai/providers/use-chat-message-actions.ts +554 -0
- package/src/extensions/nocobase-ai/providers/use-chat-runtime.ts +210 -0
- package/src/extensions/nocobase-ai/providers/use-chat-work-context.ts +93 -0
- package/src/extensions/nocobase-ai/providers/use-conversation-catalog.ts +108 -0
- package/src/extensions/nocobase-ai/providers/use-conversation-history.ts +178 -0
- package/src/extensions/nocobase-ai/services/index.ts +7 -0
- package/src/extensions/nocobase-ai/services/nocobase-ai-service.ts +454 -0
- package/src/extensions/nocobase-ai/services/types.ts +82 -0
- package/src/index.tsx +2 -3
- package/src/lib/i18n.ts +161 -0
- package/src/lib/nocobase/client.ts +19 -2
- package/src/locales/en-US.ts +82 -0
- package/src/locales/index.ts +13 -0
- package/src/locales/zh-CN.ts +79 -0
- package/src/pages/users/create.tsx +98 -0
- package/src/pages/users/edit.tsx +92 -0
- package/src/pages/users/form-context.ts +57 -0
- package/src/pages/users/form-fields.tsx +211 -0
- package/src/pages/users/list.tsx +205 -0
- package/src/pages/users/show.tsx +187 -0
- package/src/pages/users/types.ts +20 -0
- package/src/providers/constants.ts +1 -0
- package/src/pages/blog-posts/create.tsx +0 -177
- package/src/pages/blog-posts/edit.tsx +0 -197
- package/src/pages/blog-posts/list.tsx +0 -131
- package/src/pages/blog-posts/show.tsx +0 -81
- package/src/pages/categories/create.tsx +0 -73
- package/src/pages/categories/edit.tsx +0 -73
- package/src/pages/categories/index.ts +0 -4
- package/src/pages/categories/list.tsx +0 -86
- package/src/pages/categories/show.tsx +0 -37
- /package/src/pages/{blog-posts → users}/index.ts +0 -0
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
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
|
+
);
|
|
@@ -0,0 +1,173 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
};
|