@nocobase/portal-template-default 1.0.2 → 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/package.json +1 -1
- package/scripts/registry.mjs +2 -0
- 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
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { Button } from "@/components/ui/button";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
import { ChevronDown, ChevronRight, LoaderCircle } from "lucide-react";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import {
|
|
6
|
+
useAI,
|
|
7
|
+
type AISubAgentConversation as AISubAgentConversationType,
|
|
8
|
+
type AIToolCallDecision,
|
|
9
|
+
} from "../../providers";
|
|
10
|
+
import { AIEmployeeAvatar } from "./ai-employee-avatar";
|
|
11
|
+
import { MarkdownMessage } from "./markdown-message";
|
|
12
|
+
import { ReasoningPanel } from "./reasoning-panel";
|
|
13
|
+
import {
|
|
14
|
+
getToolCallName,
|
|
15
|
+
isToolCallPart,
|
|
16
|
+
ToolCallCard,
|
|
17
|
+
} from "./tool-call-card";
|
|
18
|
+
|
|
19
|
+
export function SubAgentConversation({
|
|
20
|
+
conversation,
|
|
21
|
+
onToolCallDecision,
|
|
22
|
+
status = "ready",
|
|
23
|
+
decideToolCall,
|
|
24
|
+
focusComposer,
|
|
25
|
+
}: {
|
|
26
|
+
conversation: AISubAgentConversationType;
|
|
27
|
+
onToolCallDecision?: (decision: AIToolCallDecision) => void | Promise<void>;
|
|
28
|
+
status?: "submitted" | "streaming" | "ready" | "error";
|
|
29
|
+
decideToolCall?: (decision: AIToolCallDecision) => Promise<void>;
|
|
30
|
+
focusComposer?: () => void;
|
|
31
|
+
}) {
|
|
32
|
+
const ai = useAI();
|
|
33
|
+
const completed = conversation.status === "completed";
|
|
34
|
+
const [expanded, setExpanded] = useState(true);
|
|
35
|
+
const employee = ai.employees.find(
|
|
36
|
+
(item) => item.username === conversation.username
|
|
37
|
+
) ?? {
|
|
38
|
+
username: conversation.username,
|
|
39
|
+
nickname: conversation.username,
|
|
40
|
+
};
|
|
41
|
+
const messages =
|
|
42
|
+
conversation.messages[0]?.role === "user"
|
|
43
|
+
? conversation.messages.slice(1)
|
|
44
|
+
: conversation.messages;
|
|
45
|
+
const interactionPending = status === "streaming" || status === "submitted";
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<section className="rounded-xl border border-dashed bg-muted/15">
|
|
49
|
+
<Button
|
|
50
|
+
type="button"
|
|
51
|
+
variant="ghost"
|
|
52
|
+
className="h-auto w-full justify-start rounded-xl px-3 py-2 text-left"
|
|
53
|
+
onClick={() => setExpanded((current) => !current)}
|
|
54
|
+
>
|
|
55
|
+
{expanded ? (
|
|
56
|
+
<ChevronDown className="size-4 shrink-0" />
|
|
57
|
+
) : (
|
|
58
|
+
<ChevronRight className="size-4 shrink-0" />
|
|
59
|
+
)}
|
|
60
|
+
<AIEmployeeAvatar employee={employee} className="size-6" />
|
|
61
|
+
<span className="min-w-0 flex-1 truncate text-sm font-medium">
|
|
62
|
+
{employee.nickname}
|
|
63
|
+
</span>
|
|
64
|
+
<span className="flex shrink-0 items-center gap-1.5 text-xs font-normal text-muted-foreground">
|
|
65
|
+
{!completed ? <LoaderCircle className="size-3 animate-spin" /> : null}
|
|
66
|
+
{completed ? "Completed" : "Working"}
|
|
67
|
+
</span>
|
|
68
|
+
</Button>
|
|
69
|
+
{expanded ? (
|
|
70
|
+
<div className="space-y-3 border-t border-dashed px-3 py-3">
|
|
71
|
+
{messages.flatMap((message) =>
|
|
72
|
+
message.parts.map((part, index) => {
|
|
73
|
+
if (part.type === "reasoning") {
|
|
74
|
+
return (
|
|
75
|
+
<ReasoningPanel
|
|
76
|
+
key={`${message.id}-reasoning-${index}`}
|
|
77
|
+
streaming={part.state === "streaming"}
|
|
78
|
+
>
|
|
79
|
+
{part.text}
|
|
80
|
+
</ReasoningPanel>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (part.type === "text") {
|
|
84
|
+
return (
|
|
85
|
+
<div
|
|
86
|
+
key={`${message.id}-text-${index}`}
|
|
87
|
+
className="ai-markdown text-sm leading-6 text-foreground"
|
|
88
|
+
>
|
|
89
|
+
<MarkdownMessage>{part.text}</MarkdownMessage>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (part.type === "data-subAgent") {
|
|
94
|
+
return (
|
|
95
|
+
<SubAgentConversation
|
|
96
|
+
key={part.id ?? part.data.sessionId}
|
|
97
|
+
conversation={part.data}
|
|
98
|
+
onToolCallDecision={onToolCallDecision}
|
|
99
|
+
status={status}
|
|
100
|
+
decideToolCall={decideToolCall}
|
|
101
|
+
focusComposer={focusComposer}
|
|
102
|
+
/>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (!isToolCallPart(part)) return [];
|
|
106
|
+
return (
|
|
107
|
+
<ToolCallCard
|
|
108
|
+
key={part.toolCallId}
|
|
109
|
+
part={part}
|
|
110
|
+
approval={message.metadata?.toolApprovals?.[part.toolCallId]}
|
|
111
|
+
disabled={interactionPending}
|
|
112
|
+
onRevise={focusComposer}
|
|
113
|
+
onDecision={async (decision, input) => {
|
|
114
|
+
const toolDecision = {
|
|
115
|
+
messageId: message.id,
|
|
116
|
+
toolCallId: part.toolCallId,
|
|
117
|
+
toolName: getToolCallName(part),
|
|
118
|
+
decision,
|
|
119
|
+
input,
|
|
120
|
+
} satisfies AIToolCallDecision;
|
|
121
|
+
await decideToolCall?.(toolDecision);
|
|
122
|
+
await onToolCallDecision?.(toolDecision);
|
|
123
|
+
}}
|
|
124
|
+
/>
|
|
125
|
+
);
|
|
126
|
+
})
|
|
127
|
+
)}
|
|
128
|
+
{!messages.length ? (
|
|
129
|
+
<div
|
|
130
|
+
className={cn(
|
|
131
|
+
"text-xs text-muted-foreground",
|
|
132
|
+
!completed && "animate-pulse"
|
|
133
|
+
)}
|
|
134
|
+
>
|
|
135
|
+
{completed ? "No visible output." : "Starting delegated work…"}
|
|
136
|
+
</div>
|
|
137
|
+
) : null}
|
|
138
|
+
</div>
|
|
139
|
+
) : null}
|
|
140
|
+
</section>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Collapsible,
|
|
3
|
+
CollapsibleContent,
|
|
4
|
+
CollapsibleTrigger,
|
|
5
|
+
} from "@/components/ui/collapsible";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { cn } from "@/lib/utils";
|
|
8
|
+
import type { AIChatMessage, AIToolCallApproval } from "../../providers";
|
|
9
|
+
import {
|
|
10
|
+
Check,
|
|
11
|
+
ChevronDown,
|
|
12
|
+
CircleAlert,
|
|
13
|
+
LoaderCircle,
|
|
14
|
+
Wrench,
|
|
15
|
+
} from "lucide-react";
|
|
16
|
+
import { useEffect, useState, type ReactNode } from "react";
|
|
17
|
+
import { useAIToolRenderer } from "../tools/tool-renderer-provider";
|
|
18
|
+
|
|
19
|
+
export type ToolCallPart = Extract<
|
|
20
|
+
AIChatMessage["parts"][number],
|
|
21
|
+
{ type: `tool-${string}` | "dynamic-tool" }
|
|
22
|
+
>;
|
|
23
|
+
|
|
24
|
+
export function isToolCallPart(
|
|
25
|
+
part: AIChatMessage["parts"][number]
|
|
26
|
+
): part is ToolCallPart {
|
|
27
|
+
return part.type === "dynamic-tool" || part.type.startsWith("tool-");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const getToolCallName = (part: ToolCallPart) =>
|
|
31
|
+
part.type === "dynamic-tool" ? part.toolName : part.type.slice(5);
|
|
32
|
+
|
|
33
|
+
const toolLabel = (part: ToolCallPart) =>
|
|
34
|
+
getToolCallName(part)
|
|
35
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
36
|
+
.replace(/[-_]+/g, " ")
|
|
37
|
+
.replace(/^./, (character) => character.toUpperCase());
|
|
38
|
+
|
|
39
|
+
const formatValue = (value: unknown) => {
|
|
40
|
+
if (typeof value === "string") return value;
|
|
41
|
+
try {
|
|
42
|
+
return JSON.stringify(value, null, 2) ?? String(value);
|
|
43
|
+
} catch {
|
|
44
|
+
return String(value);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const getStatus = (part: ToolCallPart) => {
|
|
49
|
+
switch (part.state) {
|
|
50
|
+
case "input-streaming":
|
|
51
|
+
return {
|
|
52
|
+
label: "Preparing",
|
|
53
|
+
icon: LoaderCircle,
|
|
54
|
+
tone: "text-muted-foreground",
|
|
55
|
+
};
|
|
56
|
+
case "input-available":
|
|
57
|
+
return {
|
|
58
|
+
label: "Running",
|
|
59
|
+
icon: LoaderCircle,
|
|
60
|
+
tone: "text-muted-foreground",
|
|
61
|
+
};
|
|
62
|
+
case "output-error":
|
|
63
|
+
return {
|
|
64
|
+
label: "Failed",
|
|
65
|
+
icon: CircleAlert,
|
|
66
|
+
tone: "text-destructive",
|
|
67
|
+
};
|
|
68
|
+
case "output-available":
|
|
69
|
+
return {
|
|
70
|
+
label: "Completed",
|
|
71
|
+
icon: Check,
|
|
72
|
+
tone: "text-foreground",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const approvalFromPart = (
|
|
78
|
+
part: ToolCallPart
|
|
79
|
+
): AIToolCallApproval | undefined => {
|
|
80
|
+
if (!("callProviderMetadata" in part)) return undefined;
|
|
81
|
+
const metadata = part.callProviderMetadata?.nocobase;
|
|
82
|
+
if (!metadata || typeof metadata !== "object") return undefined;
|
|
83
|
+
const requiresApproval = (metadata as { requiresApproval?: unknown })
|
|
84
|
+
.requiresApproval;
|
|
85
|
+
return requiresApproval === true
|
|
86
|
+
? { required: true, status: "pending" }
|
|
87
|
+
: undefined;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export type NocoBaseToolCallMetadata = {
|
|
91
|
+
autoApprove?: boolean;
|
|
92
|
+
invokeStatus?: string;
|
|
93
|
+
messageId?: string;
|
|
94
|
+
requiresApproval?: boolean;
|
|
95
|
+
selectedSuggestion?: string;
|
|
96
|
+
status?: string;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const getNocoBaseToolCallMetadata = (part: ToolCallPart) => {
|
|
100
|
+
if (!("callProviderMetadata" in part)) return undefined;
|
|
101
|
+
const metadata = part.callProviderMetadata?.nocobase;
|
|
102
|
+
return metadata && typeof metadata === "object"
|
|
103
|
+
? (metadata as NocoBaseToolCallMetadata)
|
|
104
|
+
: undefined;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
type ToolCallCardProps = {
|
|
108
|
+
part: ToolCallPart;
|
|
109
|
+
approval?: AIToolCallApproval;
|
|
110
|
+
onDecision?: (
|
|
111
|
+
decision: "approve" | "reject" | "edit",
|
|
112
|
+
input?: unknown
|
|
113
|
+
) => void | Promise<void>;
|
|
114
|
+
onRevise?: () => void;
|
|
115
|
+
inlineActions?: ReactNode;
|
|
116
|
+
disabled?: boolean;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export function ToolCallCard({
|
|
120
|
+
part,
|
|
121
|
+
approval,
|
|
122
|
+
onDecision,
|
|
123
|
+
onRevise,
|
|
124
|
+
inlineActions,
|
|
125
|
+
disabled = false,
|
|
126
|
+
}: ToolCallCardProps) {
|
|
127
|
+
const resolvedApproval = approval ?? approvalFromPart(part);
|
|
128
|
+
const [approvalStatus, setApprovalStatus] = useState(
|
|
129
|
+
resolvedApproval?.status ?? "pending"
|
|
130
|
+
);
|
|
131
|
+
const [deciding, setDeciding] = useState(false);
|
|
132
|
+
const renderer = useAIToolRenderer(getToolCallName(part));
|
|
133
|
+
const Renderer = renderer?.component;
|
|
134
|
+
const [open, setOpen] = useState(
|
|
135
|
+
part.state === "output-error" || Boolean(Renderer)
|
|
136
|
+
);
|
|
137
|
+
const toolStatus = getStatus(part);
|
|
138
|
+
const approvalRequired =
|
|
139
|
+
resolvedApproval?.required === true && approvalStatus === "pending";
|
|
140
|
+
const status = approvalRequired
|
|
141
|
+
? { label: "Approval required", icon: CircleAlert, tone: "text-foreground" }
|
|
142
|
+
: approvalStatus === "rejected"
|
|
143
|
+
? { label: "Denied", icon: CircleAlert, tone: "text-muted-foreground" }
|
|
144
|
+
: approvalStatus === "approved" && part.state === "input-available"
|
|
145
|
+
? { label: "Allowed", icon: Check, tone: "text-foreground" }
|
|
146
|
+
: toolStatus;
|
|
147
|
+
const StatusIcon = status.icon;
|
|
148
|
+
const hasInput = part.input !== undefined;
|
|
149
|
+
const hasError = part.state === "output-error";
|
|
150
|
+
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
setApprovalStatus(resolvedApproval?.status ?? "pending");
|
|
153
|
+
}, [resolvedApproval?.status]);
|
|
154
|
+
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
if (part.state === "output-error") {
|
|
157
|
+
setOpen(true);
|
|
158
|
+
}
|
|
159
|
+
}, [part.state]);
|
|
160
|
+
|
|
161
|
+
const decide = async (
|
|
162
|
+
decision: "approve" | "reject" | "edit",
|
|
163
|
+
input?: unknown
|
|
164
|
+
) => {
|
|
165
|
+
if (disabled) return;
|
|
166
|
+
setDeciding(true);
|
|
167
|
+
try {
|
|
168
|
+
await onDecision?.(decision, input);
|
|
169
|
+
setApprovalStatus(decision === "reject" ? "rejected" : "approved");
|
|
170
|
+
} finally {
|
|
171
|
+
setDeciding(false);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
if (Renderer && renderer?.standalone) {
|
|
176
|
+
const specializedDisabled =
|
|
177
|
+
disabled ||
|
|
178
|
+
approvalStatus === "rejected" ||
|
|
179
|
+
(resolvedApproval?.required === true && approvalStatus !== "pending");
|
|
180
|
+
return (
|
|
181
|
+
<div>
|
|
182
|
+
<Renderer
|
|
183
|
+
part={part}
|
|
184
|
+
disabled={specializedDisabled}
|
|
185
|
+
onEdit={(input) => onDecision?.("edit", input)}
|
|
186
|
+
onApprove={() => onDecision?.("approve")}
|
|
187
|
+
onReject={(message) => onDecision?.("reject", message)}
|
|
188
|
+
onRevise={() => onRevise?.()}
|
|
189
|
+
/>
|
|
190
|
+
{part.state === "output-error" ? (
|
|
191
|
+
<div className="mt-2 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-destructive">
|
|
192
|
+
{part.errorText}
|
|
193
|
+
</div>
|
|
194
|
+
) : null}
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<Collapsible
|
|
201
|
+
open={open}
|
|
202
|
+
onOpenChange={setOpen}
|
|
203
|
+
className="overflow-hidden rounded-lg border border-border/70 bg-muted/30"
|
|
204
|
+
>
|
|
205
|
+
<div className="flex items-center">
|
|
206
|
+
<CollapsibleTrigger className="group/tool-call flex min-w-0 flex-1 items-center gap-2 px-3 py-2 text-left text-xs font-medium text-muted-foreground outline-none hover:bg-muted/40 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset">
|
|
207
|
+
<Wrench className="size-3.5 shrink-0" />
|
|
208
|
+
<span className="min-w-0 flex-1 truncate">{toolLabel(part)}</span>
|
|
209
|
+
<span
|
|
210
|
+
className={cn(
|
|
211
|
+
"flex shrink-0 items-center gap-1.5 font-normal",
|
|
212
|
+
status.tone
|
|
213
|
+
)}
|
|
214
|
+
>
|
|
215
|
+
<StatusIcon
|
|
216
|
+
className={cn(
|
|
217
|
+
"size-3.5",
|
|
218
|
+
(deciding ||
|
|
219
|
+
(!approvalRequired &&
|
|
220
|
+
approvalStatus === "pending" &&
|
|
221
|
+
(part.state === "input-streaming" ||
|
|
222
|
+
part.state === "input-available"))) &&
|
|
223
|
+
"animate-spin"
|
|
224
|
+
)}
|
|
225
|
+
/>
|
|
226
|
+
{deciding ? "Applying…" : status.label}
|
|
227
|
+
</span>
|
|
228
|
+
<ChevronDown className="size-3.5 shrink-0 transition-transform group-data-panel-open/tool-call:rotate-180" />
|
|
229
|
+
</CollapsibleTrigger>
|
|
230
|
+
{inlineActions ? (
|
|
231
|
+
<div className="flex shrink-0 items-center pr-2">{inlineActions}</div>
|
|
232
|
+
) : null}
|
|
233
|
+
</div>
|
|
234
|
+
{approvalRequired && !renderer?.handlesApproval ? (
|
|
235
|
+
<div className="flex items-center justify-between gap-3 border-t px-3 py-2.5">
|
|
236
|
+
<p className="text-xs text-muted-foreground">
|
|
237
|
+
This tool needs permission before it can run.
|
|
238
|
+
</p>
|
|
239
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
240
|
+
<Button
|
|
241
|
+
variant="ghost"
|
|
242
|
+
size="sm"
|
|
243
|
+
disabled={disabled || deciding}
|
|
244
|
+
onClick={() => void decide("reject")}
|
|
245
|
+
>
|
|
246
|
+
Deny
|
|
247
|
+
</Button>
|
|
248
|
+
<Button
|
|
249
|
+
size="sm"
|
|
250
|
+
disabled={disabled || deciding}
|
|
251
|
+
onClick={() => void decide("approve")}
|
|
252
|
+
>
|
|
253
|
+
Allow
|
|
254
|
+
</Button>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
) : null}
|
|
258
|
+
<CollapsibleContent className="border-t">
|
|
259
|
+
{Renderer ? (
|
|
260
|
+
<div className="p-3">
|
|
261
|
+
<Renderer
|
|
262
|
+
part={part}
|
|
263
|
+
disabled={
|
|
264
|
+
disabled ||
|
|
265
|
+
deciding ||
|
|
266
|
+
approvalStatus === "rejected" ||
|
|
267
|
+
(resolvedApproval?.required === true &&
|
|
268
|
+
approvalStatus !== "pending")
|
|
269
|
+
}
|
|
270
|
+
onEdit={(input) => decide("edit", input)}
|
|
271
|
+
onApprove={() => decide("approve")}
|
|
272
|
+
onReject={() => decide("reject")}
|
|
273
|
+
onRevise={() => onRevise?.()}
|
|
274
|
+
/>
|
|
275
|
+
</div>
|
|
276
|
+
) : hasInput ? (
|
|
277
|
+
<ToolCallValue label="Input" value={part.input} />
|
|
278
|
+
) : null}
|
|
279
|
+
{part.state === "output-error" ? (
|
|
280
|
+
<ToolCallValue label="Error" value={part.errorText} error />
|
|
281
|
+
) : null}
|
|
282
|
+
{!Renderer && !hasInput && !hasError ? (
|
|
283
|
+
<p className="px-3 py-3 text-xs text-muted-foreground">
|
|
284
|
+
Waiting for tool input…
|
|
285
|
+
</p>
|
|
286
|
+
) : null}
|
|
287
|
+
</CollapsibleContent>
|
|
288
|
+
</Collapsible>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function ToolCallValue({
|
|
293
|
+
label,
|
|
294
|
+
value,
|
|
295
|
+
error = false,
|
|
296
|
+
}: {
|
|
297
|
+
label: string;
|
|
298
|
+
value: unknown;
|
|
299
|
+
error?: boolean;
|
|
300
|
+
}) {
|
|
301
|
+
return (
|
|
302
|
+
<div className="border-b px-3 py-3 last:border-b-0">
|
|
303
|
+
<div className="mb-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
304
|
+
{label}
|
|
305
|
+
</div>
|
|
306
|
+
<pre
|
|
307
|
+
className={cn(
|
|
308
|
+
"max-h-48 overflow-auto whitespace-pre-wrap break-words rounded-lg bg-background p-2.5 font-mono text-xs leading-5",
|
|
309
|
+
error && "text-destructive"
|
|
310
|
+
)}
|
|
311
|
+
>
|
|
312
|
+
{formatValue(value)}
|
|
313
|
+
</pre>
|
|
314
|
+
</div>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Button } from "@/components/ui/button";
|
|
2
|
+
import {
|
|
3
|
+
Popover,
|
|
4
|
+
PopoverContent,
|
|
5
|
+
PopoverDescription,
|
|
6
|
+
PopoverHeader,
|
|
7
|
+
PopoverTitle,
|
|
8
|
+
PopoverTrigger,
|
|
9
|
+
} from "@/components/ui/popover";
|
|
10
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
11
|
+
import {
|
|
12
|
+
Tooltip,
|
|
13
|
+
TooltipContent,
|
|
14
|
+
TooltipTrigger,
|
|
15
|
+
} from "@/components/ui/tooltip";
|
|
16
|
+
import { MessageSquareText } from "lucide-react";
|
|
17
|
+
import { useEffect, useState } from "react";
|
|
18
|
+
import { useAIChatBase } from "../../providers";
|
|
19
|
+
|
|
20
|
+
export function UserPromptEditor() {
|
|
21
|
+
const { currentEmployee, saveUserPrompt } = useAIChatBase();
|
|
22
|
+
const [open, setOpen] = useState(false);
|
|
23
|
+
const [prompt, setPrompt] = useState("");
|
|
24
|
+
const [saving, setSaving] = useState(false);
|
|
25
|
+
const [error, setError] = useState<string>();
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!open) return;
|
|
29
|
+
setPrompt(currentEmployee.userConfig?.prompt ?? "");
|
|
30
|
+
setError(undefined);
|
|
31
|
+
}, [currentEmployee.userConfig?.prompt, currentEmployee.username, open]);
|
|
32
|
+
|
|
33
|
+
const save = async () => {
|
|
34
|
+
setSaving(true);
|
|
35
|
+
setError(undefined);
|
|
36
|
+
try {
|
|
37
|
+
await saveUserPrompt(prompt);
|
|
38
|
+
setOpen(false);
|
|
39
|
+
} catch (saveError) {
|
|
40
|
+
setError(
|
|
41
|
+
saveError instanceof Error ? saveError.message : "Unable to save prompt"
|
|
42
|
+
);
|
|
43
|
+
} finally {
|
|
44
|
+
setSaving(false);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
50
|
+
<Tooltip>
|
|
51
|
+
<TooltipTrigger
|
|
52
|
+
render={
|
|
53
|
+
<PopoverTrigger
|
|
54
|
+
render={
|
|
55
|
+
<Button
|
|
56
|
+
variant="ghost"
|
|
57
|
+
size="icon-sm"
|
|
58
|
+
aria-label="Personalized prompt"
|
|
59
|
+
/>
|
|
60
|
+
}
|
|
61
|
+
/>
|
|
62
|
+
}
|
|
63
|
+
>
|
|
64
|
+
<MessageSquareText />
|
|
65
|
+
</TooltipTrigger>
|
|
66
|
+
<TooltipContent>Personalized prompt</TooltipContent>
|
|
67
|
+
</Tooltip>
|
|
68
|
+
<PopoverContent
|
|
69
|
+
align="end"
|
|
70
|
+
sideOffset={8}
|
|
71
|
+
className="w-[380px] gap-3 p-4"
|
|
72
|
+
>
|
|
73
|
+
<PopoverHeader>
|
|
74
|
+
<PopoverTitle>Personalized prompt</PopoverTitle>
|
|
75
|
+
<PopoverDescription>
|
|
76
|
+
Add instructions that this AI employee should follow when working
|
|
77
|
+
with you.
|
|
78
|
+
</PopoverDescription>
|
|
79
|
+
</PopoverHeader>
|
|
80
|
+
<Textarea
|
|
81
|
+
value={prompt}
|
|
82
|
+
onChange={(event) => setPrompt(event.target.value)}
|
|
83
|
+
rows={5}
|
|
84
|
+
className="max-h-52 min-h-28 resize-y"
|
|
85
|
+
placeholder="For example: Keep answers concise and ask before changing data."
|
|
86
|
+
/>
|
|
87
|
+
{error ? <p className="text-xs text-destructive">{error}</p> : null}
|
|
88
|
+
<div className="flex justify-end gap-2">
|
|
89
|
+
<Button variant="outline" size="sm" onClick={() => setOpen(false)}>
|
|
90
|
+
Cancel
|
|
91
|
+
</Button>
|
|
92
|
+
<Button size="sm" disabled={saving} onClick={() => void save()}>
|
|
93
|
+
{saving ? "Saving…" : "Save"}
|
|
94
|
+
</Button>
|
|
95
|
+
</div>
|
|
96
|
+
</PopoverContent>
|
|
97
|
+
</Popover>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Button } from "@/components/ui/button";
|
|
2
|
+
import {
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogContent,
|
|
5
|
+
DialogDescription,
|
|
6
|
+
DialogHeader,
|
|
7
|
+
DialogTitle,
|
|
8
|
+
} from "@/components/ui/dialog";
|
|
9
|
+
import { cn } from "@/lib/utils";
|
|
10
|
+
import { MousePointer2, X } from "lucide-react";
|
|
11
|
+
import { useMemo, useState } from "react";
|
|
12
|
+
import type { AIWorkContextItem } from "../../providers";
|
|
13
|
+
|
|
14
|
+
const formatContextContent = (content: unknown) => {
|
|
15
|
+
if (content === undefined) return "No context payload";
|
|
16
|
+
if (typeof content === "string") return content;
|
|
17
|
+
try {
|
|
18
|
+
return JSON.stringify(content, null, 2);
|
|
19
|
+
} catch {
|
|
20
|
+
return String(content);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function WorkContextChip({
|
|
25
|
+
item,
|
|
26
|
+
onRemove,
|
|
27
|
+
className,
|
|
28
|
+
}: {
|
|
29
|
+
item: AIWorkContextItem;
|
|
30
|
+
onRemove?: () => void;
|
|
31
|
+
className?: string;
|
|
32
|
+
}) {
|
|
33
|
+
const [open, setOpen] = useState(false);
|
|
34
|
+
const title = item.title ?? "Page element";
|
|
35
|
+
const kind = typeof item.kind === "string" ? item.kind : undefined;
|
|
36
|
+
const content = useMemo(
|
|
37
|
+
() => formatContextContent(item.content),
|
|
38
|
+
[item.content]
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
<span
|
|
44
|
+
className={cn(
|
|
45
|
+
"flex min-w-0 max-w-full items-center rounded-md border bg-muted/40 text-xs text-muted-foreground",
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
>
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
className="flex min-w-0 items-center gap-1.5 rounded-l-md px-2 py-1 text-left hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
52
|
+
aria-label={`View context: ${title}`}
|
|
53
|
+
onClick={() => setOpen(true)}
|
|
54
|
+
>
|
|
55
|
+
<MousePointer2 className="size-3.5 shrink-0" />
|
|
56
|
+
<span className="truncate">{title}</span>
|
|
57
|
+
</button>
|
|
58
|
+
{onRemove ? (
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
className="mr-1 rounded-sm p-0.5 hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
62
|
+
aria-label={`Remove ${title}`}
|
|
63
|
+
onClick={onRemove}
|
|
64
|
+
>
|
|
65
|
+
<X className="size-3" />
|
|
66
|
+
</button>
|
|
67
|
+
) : null}
|
|
68
|
+
</span>
|
|
69
|
+
|
|
70
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
71
|
+
<DialogContent className="sm:max-w-xl">
|
|
72
|
+
<DialogHeader className="pr-8">
|
|
73
|
+
<DialogTitle>{title}</DialogTitle>
|
|
74
|
+
<DialogDescription>
|
|
75
|
+
This is the page context included with the message sent to the AI
|
|
76
|
+
employee.
|
|
77
|
+
</DialogDescription>
|
|
78
|
+
</DialogHeader>
|
|
79
|
+
|
|
80
|
+
<div className="flex flex-wrap gap-2 text-xs">
|
|
81
|
+
<span className="rounded-md border bg-muted/40 px-2 py-1">
|
|
82
|
+
Type: {item.type}
|
|
83
|
+
</span>
|
|
84
|
+
{kind ? (
|
|
85
|
+
<span className="rounded-md border bg-muted/40 px-2 py-1">
|
|
86
|
+
Kind: {kind}
|
|
87
|
+
</span>
|
|
88
|
+
) : null}
|
|
89
|
+
{item.id ? (
|
|
90
|
+
<span className="max-w-full truncate rounded-md border bg-muted/40 px-2 py-1">
|
|
91
|
+
ID: {item.id}
|
|
92
|
+
</span>
|
|
93
|
+
) : null}
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div className="space-y-2">
|
|
97
|
+
<div className="text-xs font-medium text-muted-foreground">
|
|
98
|
+
Context payload
|
|
99
|
+
</div>
|
|
100
|
+
<pre className="max-h-[55vh] overflow-auto rounded-lg border bg-muted/30 p-3 text-xs leading-5 whitespace-pre-wrap break-words">
|
|
101
|
+
{content}
|
|
102
|
+
</pre>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div className="flex justify-end">
|
|
106
|
+
<Button variant="outline" onClick={() => setOpen(false)}>
|
|
107
|
+
Close
|
|
108
|
+
</Button>
|
|
109
|
+
</div>
|
|
110
|
+
</DialogContent>
|
|
111
|
+
</Dialog>
|
|
112
|
+
</>
|
|
113
|
+
);
|
|
114
|
+
}
|