ai-design-system 0.1.53 → 0.1.56
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/components/ai-elements/reasoning.tsx +6 -6
- package/components/blocks/AIConversation/AIConversation.tsx +33 -6
- package/components/composites/FileTreeExplorer/FileTreeExplorer.tsx +72 -2
- package/components/composites/ReasoningDisplay/ReasoningDisplay.stories.tsx +76 -0
- package/components/composites/ReasoningDisplay/ReasoningDisplay.tsx +46 -0
- package/components/composites/ReasoningDisplay/index.ts +2 -0
- package/components/composites/ToolCallDisplay/ToolCallDisplay.tsx +30 -1
- package/components/composites/index.ts +4 -0
- package/components/features/AIDocEditor/AIDocEditor.tsx +56 -44
- package/components/features/AIDocEditor/useAIDocEditor.mock.ts +6 -11
- package/components/features/RefinementPanel/RefinementPanel.tsx +6 -0
- package/components/features/RefinementPanel/useRefinementPanel.d.ts +4 -0
- package/components/features/RefinementPanel/useRefinementPanel.mock.ts +3 -0
- package/dist/index.cjs +352 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +8 -0
- package/dist/index.d.ts +41 -14
- package/dist/index.js +354 -76
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -22,7 +22,7 @@ type ReasoningContextValue = {
|
|
|
22
22
|
|
|
23
23
|
const ReasoningContext = createContext<ReasoningContextValue | null>(null);
|
|
24
24
|
|
|
25
|
-
const useReasoning = () => {
|
|
25
|
+
export const useReasoning = () => {
|
|
26
26
|
const context = useContext(ReasoningContext);
|
|
27
27
|
if (!context) {
|
|
28
28
|
throw new Error("Reasoning components must be used within Reasoning");
|
|
@@ -124,14 +124,14 @@ export const Reasoning = memo(
|
|
|
124
124
|
|
|
125
125
|
export type ReasoningTriggerProps = ComponentProps<typeof CollapsibleTrigger>;
|
|
126
126
|
|
|
127
|
-
const getThinkingMessage = (isStreaming: boolean, duration?: number) => {
|
|
128
|
-
if (isStreaming
|
|
127
|
+
export const getThinkingMessage = (isStreaming: boolean, duration?: number) => {
|
|
128
|
+
if (isStreaming) {
|
|
129
129
|
return <Shimmer duration={1}>Thinking...</Shimmer>;
|
|
130
130
|
}
|
|
131
|
-
if (duration === undefined) {
|
|
132
|
-
return <
|
|
131
|
+
if (duration === undefined || duration === 0) {
|
|
132
|
+
return <span>Thought for a few seconds</span>;
|
|
133
133
|
}
|
|
134
|
-
return <
|
|
134
|
+
return <span>Thought for {duration} seconds</span>;
|
|
135
135
|
};
|
|
136
136
|
|
|
137
137
|
export const ReasoningTrigger = memo(
|
|
@@ -11,6 +11,7 @@ import { UserMessage } from "@/components/composites/UserMessage"
|
|
|
11
11
|
import { SpecialistMessage } from "@/components/composites/SpecialistMessage"
|
|
12
12
|
import { OrchestratorMessage } from "@/components/composites/OrchestratorMessage"
|
|
13
13
|
import { ToolCallDisplay } from "@/components/composites/ToolCallDisplay"
|
|
14
|
+
import { ReasoningDisplay } from "@/components/composites/ReasoningDisplay"
|
|
14
15
|
import { ApprovalCard } from "@/components/composites/ApprovalCard"
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -61,6 +62,10 @@ export interface AIConversationProps
|
|
|
61
62
|
description?: string
|
|
62
63
|
icon?: React.ReactNode
|
|
63
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Callback fired when a tool action (like a link click) is triggered
|
|
67
|
+
*/
|
|
68
|
+
onToolAction?: (toolCall: ToolCall, action: string) => void
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
/**
|
|
@@ -73,6 +78,7 @@ export const AIConversation = React.memo<AIConversationProps>(
|
|
|
73
78
|
onSelectSubAgent,
|
|
74
79
|
selectedSubAgent,
|
|
75
80
|
emptyState,
|
|
81
|
+
onToolAction,
|
|
76
82
|
...conversationProps
|
|
77
83
|
}) => {
|
|
78
84
|
const isEmpty = React.useMemo(
|
|
@@ -122,31 +128,52 @@ export const AIConversation = React.memo<AIConversationProps>(
|
|
|
122
128
|
|
|
123
129
|
// Filter tool calls that aren't "task" type (those become sub-agents)
|
|
124
130
|
// Also completely hide "ask_user" and "ask_question" tools so they are ONLY rendered in the prompt input area
|
|
125
|
-
const
|
|
131
|
+
const allToolCalls =
|
|
126
132
|
message.toolCalls?.filter(
|
|
127
133
|
(tc) => tc.name !== "task" && tc.name !== "ask_user"
|
|
128
134
|
) || []
|
|
129
135
|
|
|
136
|
+
// Split into reasoning tools (shown collapsed) and direct tools (shown normally)
|
|
137
|
+
const reasoningCalls = allToolCalls.filter((tc) => tc.visibility === "reasoning")
|
|
138
|
+
const directToolCalls = allToolCalls.filter((tc) => tc.visibility !== "reasoning")
|
|
139
|
+
|
|
130
140
|
const hasContent = contentStr.trim() !== ""
|
|
131
|
-
|
|
141
|
+
const hasReasoning = reasoningCalls.length > 0
|
|
142
|
+
const reasoningText = hasReasoning ? contentStr : undefined
|
|
143
|
+
const displayContentStr = hasReasoning ? "" : contentStr
|
|
144
|
+
const hasDisplayContent = displayContentStr.trim() !== ""
|
|
145
|
+
|
|
146
|
+
if (!hasDisplayContent && directToolCalls.length === 0 && reasoningCalls.length === 0 && subAgents.length === 0 && !message.isLoading) {
|
|
132
147
|
return null;
|
|
133
148
|
}
|
|
134
149
|
|
|
150
|
+
const isStreaming = reasoningCalls.some((tc) => tc.status === "pending")
|
|
151
|
+
|
|
135
152
|
return (
|
|
136
153
|
<OrchestratorMessage
|
|
137
154
|
key={message.id}
|
|
138
155
|
message={{
|
|
139
156
|
id: message.id,
|
|
140
|
-
content:
|
|
157
|
+
content: displayContentStr,
|
|
141
158
|
avatarSrc: message.avatarSrc,
|
|
142
159
|
avatarName: message.avatarName,
|
|
143
|
-
isLoading: message.isLoading,
|
|
160
|
+
isLoading: message.isLoading && !hasReasoning,
|
|
144
161
|
}}
|
|
145
|
-
showAvatar={showAvatars}
|
|
162
|
+
showAvatar={showAvatars && hasDisplayContent}
|
|
146
163
|
>
|
|
164
|
+
{/* Render reasoning-section for hidden tool results */}
|
|
165
|
+
{hasReasoning && (
|
|
166
|
+
<ReasoningDisplay
|
|
167
|
+
content={reasoningText}
|
|
168
|
+
items={reasoningCalls}
|
|
169
|
+
isStreaming={isStreaming}
|
|
170
|
+
onToolAction={onToolAction}
|
|
171
|
+
/>
|
|
172
|
+
)}
|
|
173
|
+
|
|
147
174
|
{/* Render direct tool calls */}
|
|
148
175
|
{directToolCalls.map((tc) => (
|
|
149
|
-
<ToolCallDisplay key={tc.id} toolCall={tc} />
|
|
176
|
+
<ToolCallDisplay key={tc.id} toolCall={tc} onToolAction={onToolAction} />
|
|
150
177
|
))}
|
|
151
178
|
|
|
152
179
|
{/* Render specialist sub-agents */}
|
|
@@ -41,6 +41,40 @@ function renderTree(nodes: FileTreeNode[]): React.ReactNode {
|
|
|
41
41
|
})
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
function filterTree(nodes: FileTreeNode[], query: string): FileTreeNode[] {
|
|
45
|
+
if (!query) return nodes;
|
|
46
|
+
const lowerQuery = query.toLowerCase();
|
|
47
|
+
|
|
48
|
+
return nodes.reduce<FileTreeNode[]>((acc, node) => {
|
|
49
|
+
if (node.type === 'file') {
|
|
50
|
+
if (node.name.toLowerCase().includes(lowerQuery)) {
|
|
51
|
+
acc.push(node);
|
|
52
|
+
}
|
|
53
|
+
} else if (node.type === 'folder' && node.children) {
|
|
54
|
+
const filteredChildren = filterTree(node.children, query);
|
|
55
|
+
// Keep the folder if it matches the query itself, or if any of its children match
|
|
56
|
+
if (node.name.toLowerCase().includes(lowerQuery) || filteredChildren.length > 0) {
|
|
57
|
+
acc.push({ ...node, children: filteredChildren });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return acc;
|
|
61
|
+
}, []);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getAllFolderPaths(nodes: FileTreeNode[]): Set<string> {
|
|
65
|
+
const paths = new Set<string>();
|
|
66
|
+
function traverse(n: FileTreeNode[]) {
|
|
67
|
+
for (const node of n) {
|
|
68
|
+
if (node.type === 'folder') {
|
|
69
|
+
paths.add(node.path);
|
|
70
|
+
if (node.children) traverse(node.children);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
traverse(nodes);
|
|
75
|
+
return paths;
|
|
76
|
+
}
|
|
77
|
+
|
|
44
78
|
export const FileTreeExplorer = React.memo<FileTreeExplorerProps>(
|
|
45
79
|
({
|
|
46
80
|
tree,
|
|
@@ -53,6 +87,38 @@ export const FileTreeExplorer = React.memo<FileTreeExplorerProps>(
|
|
|
53
87
|
headerClassName,
|
|
54
88
|
className,
|
|
55
89
|
}) => {
|
|
90
|
+
const [searchQuery, setSearchQuery] = React.useState("")
|
|
91
|
+
const [userExpanded, setUserExpanded] = React.useState<Set<string>>(defaultExpanded || new Set())
|
|
92
|
+
|
|
93
|
+
// Auto-expand parents when selectedPath changes
|
|
94
|
+
React.useEffect(() => {
|
|
95
|
+
if (selectedPath) {
|
|
96
|
+
const parts = selectedPath.split('/')
|
|
97
|
+
if (parts.length > 1) {
|
|
98
|
+
setUserExpanded(prev => {
|
|
99
|
+
const next = new Set(prev)
|
|
100
|
+
let currentPath = ''
|
|
101
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
102
|
+
currentPath += (i === 0 ? '' : '/') + parts[i]
|
|
103
|
+
next.add(currentPath)
|
|
104
|
+
}
|
|
105
|
+
return next
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}, [selectedPath])
|
|
110
|
+
|
|
111
|
+
const filteredTree = React.useMemo(() => {
|
|
112
|
+
return filterTree(tree, searchQuery)
|
|
113
|
+
}, [tree, searchQuery])
|
|
114
|
+
|
|
115
|
+
const activeExpanded = React.useMemo(() => {
|
|
116
|
+
if (searchQuery) {
|
|
117
|
+
return getAllFolderPaths(filteredTree)
|
|
118
|
+
}
|
|
119
|
+
return userExpanded
|
|
120
|
+
}, [searchQuery, filteredTree, userExpanded])
|
|
121
|
+
|
|
56
122
|
return (
|
|
57
123
|
<div className={cn("flex flex-col rounded-lg border bg-background", className)}>
|
|
58
124
|
<div className={cn("flex items-center gap-2 border-b px-3 py-2", headerClassName)}>
|
|
@@ -64,6 +130,8 @@ export const FileTreeExplorer = React.memo<FileTreeExplorerProps>(
|
|
|
64
130
|
/>
|
|
65
131
|
<Input
|
|
66
132
|
placeholder={searchPlaceholder}
|
|
133
|
+
value={searchQuery}
|
|
134
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
67
135
|
className="pl-8"
|
|
68
136
|
/>
|
|
69
137
|
</div>
|
|
@@ -80,11 +148,13 @@ export const FileTreeExplorer = React.memo<FileTreeExplorerProps>(
|
|
|
80
148
|
</div>
|
|
81
149
|
<div className="p-2">
|
|
82
150
|
<FileTree
|
|
83
|
-
|
|
151
|
+
className="border-none bg-transparent"
|
|
152
|
+
expanded={activeExpanded}
|
|
153
|
+
onExpandedChange={setUserExpanded}
|
|
84
154
|
selectedPath={selectedPath}
|
|
85
155
|
onSelect={onSelect}
|
|
86
156
|
>
|
|
87
|
-
{renderTree(
|
|
157
|
+
{renderTree(filteredTree)}
|
|
88
158
|
</FileTree>
|
|
89
159
|
</div>
|
|
90
160
|
</div>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react'
|
|
2
|
+
import { ReasoningDisplay } from './ReasoningDisplay'
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof ReasoningDisplay> = {
|
|
5
|
+
title: 'Composites/ReasoningDisplay',
|
|
6
|
+
component: ReasoningDisplay,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: 'padded',
|
|
9
|
+
},
|
|
10
|
+
} satisfies Meta<typeof ReasoningDisplay>
|
|
11
|
+
|
|
12
|
+
export default meta
|
|
13
|
+
type Story = StoryObj<typeof meta>
|
|
14
|
+
|
|
15
|
+
export const Default: Story = {
|
|
16
|
+
args: {
|
|
17
|
+
content: "Let me evaluate the user's request and read the necessary files to propose a solution.",
|
|
18
|
+
isStreaming: false,
|
|
19
|
+
items: [
|
|
20
|
+
{
|
|
21
|
+
id: 'tool_1',
|
|
22
|
+
name: 'read_file',
|
|
23
|
+
args: { file_path: '/workspace/src/main.tsx' },
|
|
24
|
+
result: 'import { App } from "./App"; ...',
|
|
25
|
+
status: 'completed',
|
|
26
|
+
visibility: 'reasoning',
|
|
27
|
+
uiVariant: 'link',
|
|
28
|
+
linkText: 'src/main.tsx',
|
|
29
|
+
linkAction: 'open-file',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'tool_2',
|
|
33
|
+
name: 'read_file',
|
|
34
|
+
args: { file_path: '/workspace/src/App.tsx' },
|
|
35
|
+
result: 'export const App = () => <div>Hello</div>;',
|
|
36
|
+
status: 'completed',
|
|
37
|
+
visibility: 'reasoning',
|
|
38
|
+
uiVariant: 'link',
|
|
39
|
+
linkText: 'src/App.tsx',
|
|
40
|
+
linkAction: 'open-file',
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const Streaming: Story = {
|
|
47
|
+
args: {
|
|
48
|
+
content: "I'll start by listing the directory contents...",
|
|
49
|
+
isStreaming: true,
|
|
50
|
+
items: [
|
|
51
|
+
{
|
|
52
|
+
id: 'tool_1',
|
|
53
|
+
name: 'list_dir',
|
|
54
|
+
args: { path: '/src' },
|
|
55
|
+
status: 'pending',
|
|
56
|
+
visibility: 'reasoning',
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const OnlyTools: Story = {
|
|
63
|
+
args: {
|
|
64
|
+
isStreaming: false,
|
|
65
|
+
items: [
|
|
66
|
+
{
|
|
67
|
+
id: 'tool_1',
|
|
68
|
+
name: 'write_todos',
|
|
69
|
+
args: { todos: ['Fix layout', 'Update colors'] },
|
|
70
|
+
result: 'Todos written successfully.',
|
|
71
|
+
status: 'completed',
|
|
72
|
+
visibility: 'reasoning',
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Reasoning,
|
|
5
|
+
ReasoningTrigger,
|
|
6
|
+
} from "@/components/ai-elements/reasoning";
|
|
7
|
+
import { CollapsibleContent } from "@/components/primitives/Collapsible";
|
|
8
|
+
import { Response } from "@/components/ai-elements/response";
|
|
9
|
+
import { ToolCallDisplay } from "@/components/composites/ToolCallDisplay";
|
|
10
|
+
import type { ToolCall } from "@/components/composites/ToolCallDisplay";
|
|
11
|
+
|
|
12
|
+
export interface ReasoningDisplayProps {
|
|
13
|
+
items: ToolCall[];
|
|
14
|
+
isStreaming?: boolean;
|
|
15
|
+
content?: string;
|
|
16
|
+
onToolAction?: (toolCall: ToolCall, action: string) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const ReasoningDisplay = ({
|
|
20
|
+
items,
|
|
21
|
+
isStreaming = false,
|
|
22
|
+
content,
|
|
23
|
+
onToolAction,
|
|
24
|
+
}: ReasoningDisplayProps) => {
|
|
25
|
+
if (items.length === 0 && !content) return null;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Reasoning isStreaming={isStreaming} defaultOpen={false}>
|
|
29
|
+
<ReasoningTrigger />
|
|
30
|
+
<CollapsibleContent
|
|
31
|
+
className="mt-4 flex flex-col gap-2 data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-muted-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in"
|
|
32
|
+
>
|
|
33
|
+
{content && content.trim() && (
|
|
34
|
+
<div className="mb-4">
|
|
35
|
+
<Response className="grid gap-2">{content}</Response>
|
|
36
|
+
</div>
|
|
37
|
+
)}
|
|
38
|
+
<div className="flex flex-col gap-2">
|
|
39
|
+
{items.map((item) => (
|
|
40
|
+
<ToolCallDisplay key={item.id} toolCall={item} onToolAction={onToolAction} />
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
</CollapsibleContent>
|
|
44
|
+
</Reasoning>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
|
+
import { Icon } from "@/components/primitives/Icon"
|
|
2
3
|
import {
|
|
3
4
|
Tool,
|
|
4
5
|
ToolHeader,
|
|
@@ -20,6 +21,10 @@ export interface ToolCall {
|
|
|
20
21
|
args: Record<string, unknown>
|
|
21
22
|
result?: string
|
|
22
23
|
status: "pending" | "completed" | "error"
|
|
24
|
+
visibility?: "visible" | "reasoning"
|
|
25
|
+
uiVariant?: "default" | "link"
|
|
26
|
+
linkText?: string
|
|
27
|
+
linkAction?: string
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
export interface ToolCallDisplayProps {
|
|
@@ -31,13 +36,17 @@ export interface ToolCallDisplayProps {
|
|
|
31
36
|
* Whether the tool call is initially expanded
|
|
32
37
|
*/
|
|
33
38
|
defaultExpanded?: boolean
|
|
39
|
+
/**
|
|
40
|
+
* Callback fired when a tool action (like a link click) is triggered
|
|
41
|
+
*/
|
|
42
|
+
onToolAction?: (toolCall: ToolCall, action: string) => void
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
/**
|
|
37
46
|
* ToolCallDisplay component - displays tool execution with expandable details
|
|
38
47
|
*/
|
|
39
48
|
export const ToolCallDisplay = React.memo<ToolCallDisplayProps>(
|
|
40
|
-
({ toolCall, defaultExpanded = false }) => {
|
|
49
|
+
({ toolCall, defaultExpanded = false, onToolAction }) => {
|
|
41
50
|
const [isExpanded, setIsExpanded] = React.useState(defaultExpanded)
|
|
42
51
|
|
|
43
52
|
const { name, args, result, status } = React.useMemo(() => {
|
|
@@ -78,6 +87,26 @@ export const ToolCallDisplay = React.memo<ToolCallDisplayProps>(
|
|
|
78
87
|
}
|
|
79
88
|
}, [status])
|
|
80
89
|
|
|
90
|
+
if (toolCall.uiVariant === "link") {
|
|
91
|
+
return (
|
|
92
|
+
<div
|
|
93
|
+
className="flex items-center gap-2 p-3 my-2 rounded-md border bg-muted/30 cursor-pointer hover:bg-muted/50 transition-colors"
|
|
94
|
+
onClick={(e) => {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
e.stopPropagation();
|
|
97
|
+
if (toolCall.linkAction && onToolAction) {
|
|
98
|
+
onToolAction(toolCall, toolCall.linkAction);
|
|
99
|
+
}
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
<Icon name="external-link" className="size-4 text-blue-500" />
|
|
103
|
+
<span className="font-medium text-sm text-blue-500 hover:underline">
|
|
104
|
+
{toolCall.linkText || `${name} Action`}
|
|
105
|
+
</span>
|
|
106
|
+
</div>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
81
110
|
return (
|
|
82
111
|
<Tool open={isExpanded} onOpenChange={setIsExpanded}>
|
|
83
112
|
<ToolHeader
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
export { ToolCallDisplay } from './ToolCallDisplay'
|
|
10
10
|
export type { ToolCallDisplayProps, ToolCall } from './ToolCallDisplay'
|
|
11
11
|
|
|
12
|
+
// ReasoningDisplay Block
|
|
13
|
+
export { ReasoningDisplay } from './ReasoningDisplay'
|
|
14
|
+
export type { ReasoningDisplayProps } from './ReasoningDisplay'
|
|
15
|
+
|
|
12
16
|
// AgentIndicator Block
|
|
13
17
|
export { AgentIndicator } from './AgentIndicator'
|
|
14
18
|
export type { AgentIndicatorProps, SubAgent } from './AgentIndicator'
|
|
@@ -103,6 +103,8 @@ export interface AIDocEditorMultiTabProps {
|
|
|
103
103
|
onTabClose?: (documentId: string) => void
|
|
104
104
|
/** Optional completely separate file tree to show all available files in the explorer (even closed ones). If not provided, it falls back to showing only the currently open documents. */
|
|
105
105
|
fileTree?: FileTreeNode[]
|
|
106
|
+
/** When true, hides the document tab bar — useful for single-file mode where the file tree provides navigation. */
|
|
107
|
+
hideTabBar?: boolean
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
/**
|
|
@@ -273,60 +275,70 @@ export const AIDocEditor = React.memo<AIDocEditorProps>(
|
|
|
273
275
|
/**
|
|
274
276
|
* Multi-tab mode
|
|
275
277
|
*/
|
|
278
|
+
const { hideTabBar } = props as AIDocEditorMultiTabProps
|
|
279
|
+
|
|
280
|
+
let editorPane: React.ReactNode
|
|
281
|
+
|
|
276
282
|
if (!currentDocument) {
|
|
277
|
-
|
|
278
|
-
<div className=
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
283
|
+
editorPane = (
|
|
284
|
+
<div className="ai-doc-editor flex flex-col h-full w-full">
|
|
285
|
+
{!hideTabBar && (
|
|
286
|
+
<DocumentTabBar
|
|
287
|
+
className="w-full"
|
|
288
|
+
tabs={props.documents?.map((doc) => doc.file) || []}
|
|
289
|
+
activeTabId={props.activeDocumentId}
|
|
290
|
+
onTabSelect={props.onTabSelect}
|
|
291
|
+
onTabClose={props.onTabClose}
|
|
292
|
+
/>
|
|
293
|
+
)}
|
|
285
294
|
<div className="flex-1 flex items-center justify-center p-6 text-muted-foreground">
|
|
286
295
|
No documents open
|
|
287
296
|
</div>
|
|
288
297
|
</div>
|
|
289
298
|
)
|
|
290
|
-
}
|
|
299
|
+
} else {
|
|
300
|
+
const isMarkdownMulti = currentDocument.file.format === 'markdown'
|
|
291
301
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
onTabClose={props.onTabClose}
|
|
302
|
-
/>
|
|
303
|
-
<div className="flex-1 overflow-auto">
|
|
304
|
-
{isMarkdownMulti ? (
|
|
305
|
-
<div className="p-6">
|
|
306
|
-
<Streamdown
|
|
307
|
-
mode="streaming"
|
|
308
|
-
isAnimating
|
|
309
|
-
className="[&>*:first-child]:mt-0 [&>*:last-child]:mb-0"
|
|
310
|
-
>
|
|
311
|
-
{currentDocument.content as string}
|
|
312
|
-
</Streamdown>
|
|
313
|
-
</div>
|
|
314
|
-
) : (
|
|
315
|
-
<DocumentEditorWithComments
|
|
316
|
-
content={currentDocument.content}
|
|
317
|
-
format={currentDocument.file.format as 'json' | 'markdown' | undefined}
|
|
318
|
-
annotations={currentDocument.annotations}
|
|
319
|
-
currentUserId={currentUser.id}
|
|
320
|
-
currentUserName={currentUser.name}
|
|
321
|
-
readOnly={mode === 'readonly'}
|
|
322
|
-
onAnnotationAdd={handleAnnotationAdd}
|
|
323
|
-
onAnnotationUpdate={handleAnnotationUpdate}
|
|
324
|
-
className="p-6 h-full flex flex-col"
|
|
302
|
+
editorPane = (
|
|
303
|
+
<div className="ai-doc-editor flex flex-col h-full w-full">
|
|
304
|
+
{!hideTabBar && (
|
|
305
|
+
<DocumentTabBar
|
|
306
|
+
className="w-full"
|
|
307
|
+
tabs={props.documents?.map((doc) => doc.file) || []}
|
|
308
|
+
activeTabId={props.activeDocumentId}
|
|
309
|
+
onTabSelect={props.onTabSelect}
|
|
310
|
+
onTabClose={props.onTabClose}
|
|
325
311
|
/>
|
|
326
312
|
)}
|
|
313
|
+
<div className="flex-1 overflow-auto">
|
|
314
|
+
{isMarkdownMulti ? (
|
|
315
|
+
<div className="p-6">
|
|
316
|
+
<Streamdown
|
|
317
|
+
mode="streaming"
|
|
318
|
+
isAnimating
|
|
319
|
+
className="[&>*:first-child]:mt-0 [&>*:last-child]:mb-0"
|
|
320
|
+
>
|
|
321
|
+
{currentDocument.content as string}
|
|
322
|
+
</Streamdown>
|
|
323
|
+
</div>
|
|
324
|
+
) : (
|
|
325
|
+
<DocumentEditorWithComments
|
|
326
|
+
content={currentDocument.content}
|
|
327
|
+
format={currentDocument.file.format as 'json' | 'markdown' | undefined}
|
|
328
|
+
annotations={currentDocument.annotations}
|
|
329
|
+
selectedAnnotationId={props.selectedAnnotationId}
|
|
330
|
+
currentUserId={currentUser.id}
|
|
331
|
+
currentUserName={currentUser.name}
|
|
332
|
+
readOnly={mode === 'readonly'}
|
|
333
|
+
onAnnotationAdd={handleAnnotationAdd}
|
|
334
|
+
onAnnotationUpdate={handleAnnotationUpdate}
|
|
335
|
+
className="ai-doc-editor p-6 min-h-full"
|
|
336
|
+
/>
|
|
337
|
+
)}
|
|
338
|
+
</div>
|
|
327
339
|
</div>
|
|
328
|
-
|
|
329
|
-
|
|
340
|
+
)
|
|
341
|
+
}
|
|
330
342
|
|
|
331
343
|
return (
|
|
332
344
|
<AdjustableLayout
|
|
@@ -127,17 +127,12 @@ function useMultiTabDocEditorMockState(
|
|
|
127
127
|
|
|
128
128
|
const closeDocument = useCallback((documentId: string) => {
|
|
129
129
|
console.log('[Multi-Tab Mock] Closing document:', documentId)
|
|
130
|
-
setDocuments(prev =>
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
setActiveDocumentId(undefined)
|
|
137
|
-
}
|
|
138
|
-
return filtered
|
|
139
|
-
})
|
|
140
|
-
}, [activeDocumentId])
|
|
130
|
+
setDocuments(prev => prev.filter(doc => doc.file.id !== documentId))
|
|
131
|
+
if (activeDocumentId === documentId) {
|
|
132
|
+
const filtered = documents.filter(doc => doc.file.id !== documentId)
|
|
133
|
+
setActiveDocumentId(filtered.length > 0 ? filtered[0].file.id : undefined)
|
|
134
|
+
}
|
|
135
|
+
}, [activeDocumentId, documents])
|
|
141
136
|
|
|
142
137
|
const switchDocument = useCallback((documentId: string) => {
|
|
143
138
|
console.log('[Multi-Tab Mock] Switching to document:', documentId)
|
|
@@ -101,6 +101,10 @@ export interface RefinementPanelProps {
|
|
|
101
101
|
* Additional CSS classes
|
|
102
102
|
*/
|
|
103
103
|
className?: string;
|
|
104
|
+
/**
|
|
105
|
+
* Action handler for tool interactions
|
|
106
|
+
*/
|
|
107
|
+
onToolAction?: (toolCall: ToolCall, action: string) => void;
|
|
104
108
|
}
|
|
105
109
|
|
|
106
110
|
/**
|
|
@@ -122,6 +126,7 @@ export const RefinementPanel = React.memo<RefinementPanelProps>(
|
|
|
122
126
|
loading = false,
|
|
123
127
|
placeholder = "Ask a question or describe a task...",
|
|
124
128
|
className,
|
|
129
|
+
onToolAction,
|
|
125
130
|
}) => {
|
|
126
131
|
// File change queue state
|
|
127
132
|
const [fileChangeState, setFileChangeState] = React.useState<
|
|
@@ -262,6 +267,7 @@ export const RefinementPanel = React.memo<RefinementPanelProps>(
|
|
|
262
267
|
<AIConversation
|
|
263
268
|
messages={messages}
|
|
264
269
|
showAvatars={true}
|
|
270
|
+
onToolAction={onToolAction}
|
|
265
271
|
className="flex-1 min-h-0"
|
|
266
272
|
/>
|
|
267
273
|
<div className="sticky bottom-0 z-10 p-4 bg-gradient-to-t from-card via-card to-transparent pt-6">
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import type { RefinementMessage } from "./RefinementPanel";
|
|
12
12
|
import type { FileChangeData } from "@/components/composites/FileQueue";
|
|
13
13
|
import type { ActionRequest, ReviewConfig } from "@/components/composites/ApprovalCard";
|
|
14
|
+
import type { ToolCall } from "@/components/composites/ToolCallDisplay";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Return type for the refinement panel hook
|
|
@@ -51,6 +52,9 @@ export interface UseRefinementPanelReturn {
|
|
|
51
52
|
|
|
52
53
|
/** Processing state for HITL approval */
|
|
53
54
|
isApprovalProcessing?: boolean;
|
|
55
|
+
|
|
56
|
+
/** Action handler for tool interactions */
|
|
57
|
+
handleToolAction?: (toolCall: ToolCall, action: string) => void;
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
/**
|
|
@@ -3,6 +3,7 @@ import type { RefinementMessage } from "./RefinementPanel";
|
|
|
3
3
|
import type { FileChangeData } from "@/components/composites/FileQueue";
|
|
4
4
|
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
|
5
5
|
import type { ActionRequest, ReviewConfig } from "@/components/composites/ApprovalCard";
|
|
6
|
+
import type { ToolCall } from "@/components/composites/ToolCallDisplay";
|
|
6
7
|
import type { FormEvent } from "react";
|
|
7
8
|
|
|
8
9
|
export interface UseRefinementPanelReturn {
|
|
@@ -19,6 +20,7 @@ export interface UseRefinementPanelReturn {
|
|
|
19
20
|
handleApprovalReject?: (reason: string) => Promise<void>;
|
|
20
21
|
handleApprovalEdit?: (editedArgs: Record<string, unknown>) => Promise<void>;
|
|
21
22
|
isApprovalProcessing?: boolean;
|
|
23
|
+
handleToolAction?: (toolCall: ToolCall, action: string) => void;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
export interface UseRefinementPanelOptions {
|
|
@@ -173,5 +175,6 @@ export function useRefinementPanelMock(
|
|
|
173
175
|
handleApprovalReject: initialApprovalRequest ? handleApprovalReject : undefined,
|
|
174
176
|
handleApprovalEdit: initialApprovalRequest ? handleApprovalEdit : undefined,
|
|
175
177
|
isApprovalProcessing,
|
|
178
|
+
handleToolAction: (toolCall, action) => console.log(`[Mock Tool Action] ${action} on tool:`, toolCall),
|
|
176
179
|
};
|
|
177
180
|
}
|