@codrstudio/openclaude-chat 0.1.0 → 0.2.0
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/dist/components/StreamingIndicator.js +5 -5
- package/dist/display/DisplayReactRenderer.js +12 -12
- package/dist/display/react-sandbox/bootstrap.js +150 -150
- package/dist/styles.css +1 -2
- package/package.json +64 -61
- package/src/components/Chat.tsx +107 -107
- package/src/components/ErrorNote.tsx +35 -35
- package/src/components/LazyRender.tsx +42 -42
- package/src/components/Markdown.tsx +114 -114
- package/src/components/MessageBubble.tsx +107 -107
- package/src/components/MessageInput.tsx +421 -421
- package/src/components/MessageList.tsx +153 -153
- package/src/components/StreamingIndicator.tsx +19 -19
- package/src/display/AlertRenderer.tsx +23 -23
- package/src/display/CarouselRenderer.tsx +141 -141
- package/src/display/ChartRenderer.tsx +195 -195
- package/src/display/ChoiceButtonsRenderer.tsx +114 -114
- package/src/display/CodeBlockRenderer.tsx +49 -49
- package/src/display/ComparisonTableRenderer.tsx +132 -132
- package/src/display/DataTableRenderer.tsx +144 -144
- package/src/display/DisplayReactRenderer.tsx +269 -269
- package/src/display/FileCardRenderer.tsx +55 -55
- package/src/display/GalleryRenderer.tsx +65 -65
- package/src/display/ImageViewerRenderer.tsx +114 -114
- package/src/display/LinkPreviewRenderer.tsx +74 -74
- package/src/display/MapViewRenderer.tsx +75 -75
- package/src/display/MetricCardRenderer.tsx +29 -29
- package/src/display/PriceHighlightRenderer.tsx +62 -62
- package/src/display/ProductCardRenderer.tsx +112 -112
- package/src/display/ProgressStepsRenderer.tsx +59 -59
- package/src/display/SourcesListRenderer.tsx +47 -47
- package/src/display/SpreadsheetRenderer.tsx +86 -86
- package/src/display/StepTimelineRenderer.tsx +75 -75
- package/src/display/index.ts +21 -21
- package/src/display/react-sandbox/bootstrap.ts +155 -155
- package/src/display/registry.ts +84 -84
- package/src/display/sdk-types.ts +217 -217
- package/src/hooks/ChatProvider.tsx +21 -21
- package/src/hooks/useIsMobile.ts +15 -15
- package/src/hooks/useOpenClaudeChat.ts +476 -476
- package/src/index.ts +76 -76
- package/src/lib/utils.ts +6 -6
- package/src/parts/PartErrorBoundary.tsx +51 -51
- package/src/parts/PartRenderer.tsx +145 -145
- package/src/parts/ReasoningBlock.tsx +41 -41
- package/src/parts/ToolActivity.tsx +78 -78
- package/src/parts/ToolResult.tsx +79 -79
- package/src/styles.css +2 -2
- package/src/types.ts +41 -41
- package/src/ui/alert.tsx +77 -77
- package/src/ui/badge.tsx +36 -36
- package/src/ui/button.tsx +54 -54
- package/src/ui/card.tsx +68 -68
- package/src/ui/collapsible.tsx +7 -7
- package/src/ui/dialog.tsx +122 -122
- package/src/ui/dropdown-menu.tsx +76 -76
- package/src/ui/input.tsx +24 -24
- package/src/ui/progress.tsx +36 -36
- package/src/ui/scroll-area.tsx +48 -48
- package/src/ui/separator.tsx +31 -31
- package/src/ui/skeleton.tsx +9 -9
- package/src/ui/table.tsx +114 -114
package/src/index.ts
CHANGED
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
// @codrstudio/openclaude-chat — barrel
|
|
2
|
-
|
|
3
|
-
// Tipos publicos
|
|
4
|
-
export type {
|
|
5
|
-
Message,
|
|
6
|
-
MessagePart,
|
|
7
|
-
MessageRole,
|
|
8
|
-
TextPart,
|
|
9
|
-
ReasoningPart,
|
|
10
|
-
ToolInvocationPart,
|
|
11
|
-
ToolInvocationState,
|
|
12
|
-
} from "./types.js";
|
|
13
|
-
|
|
14
|
-
// Componente principal
|
|
15
|
-
export { Chat } from "./components/Chat.js";
|
|
16
|
-
export type { ChatProps } from "./components/Chat.js";
|
|
17
|
-
|
|
18
|
-
// Hook + Provider
|
|
19
|
-
export { useOpenClaudeChat } from "./hooks/useOpenClaudeChat.js";
|
|
20
|
-
export type {
|
|
21
|
-
UseOpenClaudeChatOptions,
|
|
22
|
-
UseOpenClaudeChatReturn,
|
|
23
|
-
} from "./hooks/useOpenClaudeChat.js";
|
|
24
|
-
|
|
25
|
-
export { ChatProvider, useChatContext } from "./hooks/ChatProvider.js";
|
|
26
|
-
export type { ChatProviderProps } from "./hooks/ChatProvider.js";
|
|
27
|
-
|
|
28
|
-
// Subcomponentes
|
|
29
|
-
export { Markdown } from "./components/Markdown.js";
|
|
30
|
-
export { StreamingIndicator } from "./components/StreamingIndicator.js";
|
|
31
|
-
export { ErrorNote } from "./components/ErrorNote.js";
|
|
32
|
-
export type { ErrorNoteProps } from "./components/ErrorNote.js";
|
|
33
|
-
export { MessageBubble } from "./components/MessageBubble.js";
|
|
34
|
-
export type { MessageBubbleProps } from "./components/MessageBubble.js";
|
|
35
|
-
export { MessageList } from "./components/MessageList.js";
|
|
36
|
-
export type { MessageListProps } from "./components/MessageList.js";
|
|
37
|
-
export { MessageInput } from "./components/MessageInput.js";
|
|
38
|
-
export type { MessageInputProps, Attachment } from "./components/MessageInput.js";
|
|
39
|
-
|
|
40
|
-
// Parts
|
|
41
|
-
export { PartRenderer } from "./parts/PartRenderer.js";
|
|
42
|
-
export type { PartRendererProps } from "./parts/PartRenderer.js";
|
|
43
|
-
export { ReasoningBlock } from "./parts/ReasoningBlock.js";
|
|
44
|
-
export type { ReasoningBlockProps } from "./parts/ReasoningBlock.js";
|
|
45
|
-
export { ToolActivity, defaultToolIconMap } from "./parts/ToolActivity.js";
|
|
46
|
-
export type { ToolActivityProps, ToolActivityState } from "./parts/ToolActivity.js";
|
|
47
|
-
export { ToolResult } from "./parts/ToolResult.js";
|
|
48
|
-
export type { ToolResultProps } from "./parts/ToolResult.js";
|
|
49
|
-
|
|
50
|
-
// Display renderers
|
|
51
|
-
export { AlertRenderer } from "./display/AlertRenderer.js";
|
|
52
|
-
export { MetricCardRenderer } from "./display/MetricCardRenderer.js";
|
|
53
|
-
export { PriceHighlightRenderer } from "./display/PriceHighlightRenderer.js";
|
|
54
|
-
export { FileCardRenderer } from "./display/FileCardRenderer.js";
|
|
55
|
-
export { CodeBlockRenderer } from "./display/CodeBlockRenderer.js";
|
|
56
|
-
export { SourcesListRenderer } from "./display/SourcesListRenderer.js";
|
|
57
|
-
export { StepTimelineRenderer } from "./display/StepTimelineRenderer.js";
|
|
58
|
-
export { ProgressStepsRenderer } from "./display/ProgressStepsRenderer.js";
|
|
59
|
-
export { ChartRenderer } from "./display/ChartRenderer.js";
|
|
60
|
-
export { CarouselRenderer } from "./display/CarouselRenderer.js";
|
|
61
|
-
export { ProductCardRenderer } from "./display/ProductCardRenderer.js";
|
|
62
|
-
export { ComparisonTableRenderer } from "./display/ComparisonTableRenderer.js";
|
|
63
|
-
export { DataTableRenderer } from "./display/DataTableRenderer.js";
|
|
64
|
-
export { SpreadsheetRenderer } from "./display/SpreadsheetRenderer.js";
|
|
65
|
-
export { GalleryRenderer } from "./display/GalleryRenderer.js";
|
|
66
|
-
export { ImageViewerRenderer } from "./display/ImageViewerRenderer.js";
|
|
67
|
-
export { LinkPreviewRenderer } from "./display/LinkPreviewRenderer.js";
|
|
68
|
-
export { MapViewRenderer } from "./display/MapViewRenderer.js";
|
|
69
|
-
export { ChoiceButtonsRenderer } from "./display/ChoiceButtonsRenderer.js";
|
|
70
|
-
|
|
71
|
-
// Registry
|
|
72
|
-
export { defaultDisplayRenderers, resolveDisplayRenderer } from "./display/registry.js";
|
|
73
|
-
export type { DisplayRendererMap, DisplayActionName } from "./display/registry.js";
|
|
74
|
-
|
|
75
|
-
// useIsMobile helper reuse
|
|
76
|
-
export { useIsMobile } from "./hooks/useIsMobile.js";
|
|
1
|
+
// @codrstudio/openclaude-chat — barrel
|
|
2
|
+
|
|
3
|
+
// Tipos publicos
|
|
4
|
+
export type {
|
|
5
|
+
Message,
|
|
6
|
+
MessagePart,
|
|
7
|
+
MessageRole,
|
|
8
|
+
TextPart,
|
|
9
|
+
ReasoningPart,
|
|
10
|
+
ToolInvocationPart,
|
|
11
|
+
ToolInvocationState,
|
|
12
|
+
} from "./types.js";
|
|
13
|
+
|
|
14
|
+
// Componente principal
|
|
15
|
+
export { Chat } from "./components/Chat.js";
|
|
16
|
+
export type { ChatProps } from "./components/Chat.js";
|
|
17
|
+
|
|
18
|
+
// Hook + Provider
|
|
19
|
+
export { useOpenClaudeChat } from "./hooks/useOpenClaudeChat.js";
|
|
20
|
+
export type {
|
|
21
|
+
UseOpenClaudeChatOptions,
|
|
22
|
+
UseOpenClaudeChatReturn,
|
|
23
|
+
} from "./hooks/useOpenClaudeChat.js";
|
|
24
|
+
|
|
25
|
+
export { ChatProvider, useChatContext } from "./hooks/ChatProvider.js";
|
|
26
|
+
export type { ChatProviderProps } from "./hooks/ChatProvider.js";
|
|
27
|
+
|
|
28
|
+
// Subcomponentes
|
|
29
|
+
export { Markdown } from "./components/Markdown.js";
|
|
30
|
+
export { StreamingIndicator } from "./components/StreamingIndicator.js";
|
|
31
|
+
export { ErrorNote } from "./components/ErrorNote.js";
|
|
32
|
+
export type { ErrorNoteProps } from "./components/ErrorNote.js";
|
|
33
|
+
export { MessageBubble } from "./components/MessageBubble.js";
|
|
34
|
+
export type { MessageBubbleProps } from "./components/MessageBubble.js";
|
|
35
|
+
export { MessageList } from "./components/MessageList.js";
|
|
36
|
+
export type { MessageListProps } from "./components/MessageList.js";
|
|
37
|
+
export { MessageInput } from "./components/MessageInput.js";
|
|
38
|
+
export type { MessageInputProps, Attachment } from "./components/MessageInput.js";
|
|
39
|
+
|
|
40
|
+
// Parts
|
|
41
|
+
export { PartRenderer } from "./parts/PartRenderer.js";
|
|
42
|
+
export type { PartRendererProps } from "./parts/PartRenderer.js";
|
|
43
|
+
export { ReasoningBlock } from "./parts/ReasoningBlock.js";
|
|
44
|
+
export type { ReasoningBlockProps } from "./parts/ReasoningBlock.js";
|
|
45
|
+
export { ToolActivity, defaultToolIconMap } from "./parts/ToolActivity.js";
|
|
46
|
+
export type { ToolActivityProps, ToolActivityState } from "./parts/ToolActivity.js";
|
|
47
|
+
export { ToolResult } from "./parts/ToolResult.js";
|
|
48
|
+
export type { ToolResultProps } from "./parts/ToolResult.js";
|
|
49
|
+
|
|
50
|
+
// Display renderers
|
|
51
|
+
export { AlertRenderer } from "./display/AlertRenderer.js";
|
|
52
|
+
export { MetricCardRenderer } from "./display/MetricCardRenderer.js";
|
|
53
|
+
export { PriceHighlightRenderer } from "./display/PriceHighlightRenderer.js";
|
|
54
|
+
export { FileCardRenderer } from "./display/FileCardRenderer.js";
|
|
55
|
+
export { CodeBlockRenderer } from "./display/CodeBlockRenderer.js";
|
|
56
|
+
export { SourcesListRenderer } from "./display/SourcesListRenderer.js";
|
|
57
|
+
export { StepTimelineRenderer } from "./display/StepTimelineRenderer.js";
|
|
58
|
+
export { ProgressStepsRenderer } from "./display/ProgressStepsRenderer.js";
|
|
59
|
+
export { ChartRenderer } from "./display/ChartRenderer.js";
|
|
60
|
+
export { CarouselRenderer } from "./display/CarouselRenderer.js";
|
|
61
|
+
export { ProductCardRenderer } from "./display/ProductCardRenderer.js";
|
|
62
|
+
export { ComparisonTableRenderer } from "./display/ComparisonTableRenderer.js";
|
|
63
|
+
export { DataTableRenderer } from "./display/DataTableRenderer.js";
|
|
64
|
+
export { SpreadsheetRenderer } from "./display/SpreadsheetRenderer.js";
|
|
65
|
+
export { GalleryRenderer } from "./display/GalleryRenderer.js";
|
|
66
|
+
export { ImageViewerRenderer } from "./display/ImageViewerRenderer.js";
|
|
67
|
+
export { LinkPreviewRenderer } from "./display/LinkPreviewRenderer.js";
|
|
68
|
+
export { MapViewRenderer } from "./display/MapViewRenderer.js";
|
|
69
|
+
export { ChoiceButtonsRenderer } from "./display/ChoiceButtonsRenderer.js";
|
|
70
|
+
|
|
71
|
+
// Registry
|
|
72
|
+
export { defaultDisplayRenderers, resolveDisplayRenderer } from "./display/registry.js";
|
|
73
|
+
export type { DisplayRendererMap, DisplayActionName } from "./display/registry.js";
|
|
74
|
+
|
|
75
|
+
// useIsMobile helper reuse
|
|
76
|
+
export { useIsMobile } from "./hooks/useIsMobile.js";
|
package/src/lib/utils.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { clsx, type ClassValue } from "clsx";
|
|
2
|
-
import { twMerge } from "tailwind-merge";
|
|
3
|
-
|
|
4
|
-
export function cn(...inputs: ClassValue[]) {
|
|
5
|
-
return twMerge(clsx(inputs));
|
|
6
|
-
}
|
|
1
|
+
import { clsx, type ClassValue } from "clsx";
|
|
2
|
+
import { twMerge } from "tailwind-merge";
|
|
3
|
+
|
|
4
|
+
export function cn(...inputs: ClassValue[]) {
|
|
5
|
+
return twMerge(clsx(inputs));
|
|
6
|
+
}
|
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { AlertTriangle } from "lucide-react";
|
|
3
|
-
|
|
4
|
-
interface State {
|
|
5
|
-
hasError: boolean;
|
|
6
|
-
message?: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
interface Props {
|
|
10
|
-
children: React.ReactNode;
|
|
11
|
-
label?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Isola o crash de um renderer (ex: display widget com input invalido) pra
|
|
16
|
-
* evitar que um unico bloco derrube a arvore React inteira. O chat continua
|
|
17
|
-
* renderizando os outros parts, e no lugar do part quebrado mostra um aviso.
|
|
18
|
-
*/
|
|
19
|
-
export class PartErrorBoundary extends React.Component<Props, State> {
|
|
20
|
-
state: State = { hasError: false };
|
|
21
|
-
|
|
22
|
-
static getDerivedStateFromError(error: unknown): State {
|
|
23
|
-
return {
|
|
24
|
-
hasError: true,
|
|
25
|
-
message: error instanceof Error ? error.message : String(error),
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
componentDidCatch(error: unknown, info: unknown) {
|
|
30
|
-
// eslint-disable-next-line no-console
|
|
31
|
-
console.warn("[openclaude-chat] part renderer failed:", error, info);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
render() {
|
|
35
|
-
if (this.state.hasError) {
|
|
36
|
-
return (
|
|
37
|
-
<div
|
|
38
|
-
role="alert"
|
|
39
|
-
className="flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-destructive"
|
|
40
|
-
>
|
|
41
|
-
<AlertTriangle className="size-3.5 shrink-0 mt-0.5" />
|
|
42
|
-
<span className="flex-1 min-w-0 break-words font-mono">
|
|
43
|
-
{this.props.label ? `[${this.props.label}] ` : ""}
|
|
44
|
-
{this.state.message ?? "Falha ao renderizar bloco"}
|
|
45
|
-
</span>
|
|
46
|
-
</div>
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
return this.props.children;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { AlertTriangle } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
interface State {
|
|
5
|
+
hasError: boolean;
|
|
6
|
+
message?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
label?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Isola o crash de um renderer (ex: display widget com input invalido) pra
|
|
16
|
+
* evitar que um unico bloco derrube a arvore React inteira. O chat continua
|
|
17
|
+
* renderizando os outros parts, e no lugar do part quebrado mostra um aviso.
|
|
18
|
+
*/
|
|
19
|
+
export class PartErrorBoundary extends React.Component<Props, State> {
|
|
20
|
+
state: State = { hasError: false };
|
|
21
|
+
|
|
22
|
+
static getDerivedStateFromError(error: unknown): State {
|
|
23
|
+
return {
|
|
24
|
+
hasError: true,
|
|
25
|
+
message: error instanceof Error ? error.message : String(error),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
componentDidCatch(error: unknown, info: unknown) {
|
|
30
|
+
// eslint-disable-next-line no-console
|
|
31
|
+
console.warn("[openclaude-chat] part renderer failed:", error, info);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
render() {
|
|
35
|
+
if (this.state.hasError) {
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
role="alert"
|
|
39
|
+
className="flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-destructive"
|
|
40
|
+
>
|
|
41
|
+
<AlertTriangle className="size-3.5 shrink-0 mt-0.5" />
|
|
42
|
+
<span className="flex-1 min-w-0 break-words font-mono">
|
|
43
|
+
{this.props.label ? `[${this.props.label}] ` : ""}
|
|
44
|
+
{this.state.message ?? "Falha ao renderizar bloco"}
|
|
45
|
+
</span>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return this.props.children;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -1,145 +1,145 @@
|
|
|
1
|
-
import { memo, useState } from "react";
|
|
2
|
-
import { Paperclip, ChevronDown } from "lucide-react";
|
|
3
|
-
import { Markdown } from "../components/Markdown.js";
|
|
4
|
-
import { LazyRender } from "../components/LazyRender.js";
|
|
5
|
-
import { ReasoningBlock } from "./ReasoningBlock.js";
|
|
6
|
-
import { ToolActivity } from "./ToolActivity.js";
|
|
7
|
-
import { ToolResult } from "./ToolResult.js";
|
|
8
|
-
import { resolveDisplayRenderer } from "../display/registry.js";
|
|
9
|
-
import type { DisplayRendererMap } from "../display/registry.js";
|
|
10
|
-
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "../ui/collapsible.js";
|
|
11
|
-
import { cn } from "../lib/utils.js";
|
|
12
|
-
import type { MessagePart, TextPart, ReasoningPart, ToolInvocationPart } from "../types.js";
|
|
13
|
-
|
|
14
|
-
const HEAVY_RENDERERS = new Set([
|
|
15
|
-
"chart", "map", "table",
|
|
16
|
-
"spreadsheet", "gallery", "image",
|
|
17
|
-
]);
|
|
18
|
-
|
|
19
|
-
export interface PartRendererProps {
|
|
20
|
-
part: MessagePart;
|
|
21
|
-
isStreaming?: boolean;
|
|
22
|
-
displayRenderers?: DisplayRendererMap;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// ─── Attachment sub-components ────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
function AttachmentTextBlock({ filename, content }: { filename: string; content: string }) {
|
|
28
|
-
const [open, setOpen] = useState(false);
|
|
29
|
-
const lines = content.split("\n");
|
|
30
|
-
const preview = lines.slice(0, 3).join("\n") + (lines.length > 3 && !open ? "\n…" : "");
|
|
31
|
-
|
|
32
|
-
return (
|
|
33
|
-
<Collapsible open={open} onOpenChange={setOpen} className="rounded-lg border text-xs overflow-hidden">
|
|
34
|
-
<div className="flex items-center gap-2 px-3 py-2 bg-muted/50 border-b">
|
|
35
|
-
<Paperclip className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
36
|
-
<span className="flex-1 truncate font-medium">{filename}</span>
|
|
37
|
-
<CollapsibleTrigger asChild>
|
|
38
|
-
<button type="button" className="text-muted-foreground hover:text-foreground transition-colors" aria-label={open ? "Recolher" : "Expandir"}>
|
|
39
|
-
<ChevronDown className={cn("h-3.5 w-3.5 transition-transform", open && "rotate-180")} />
|
|
40
|
-
</button>
|
|
41
|
-
</CollapsibleTrigger>
|
|
42
|
-
</div>
|
|
43
|
-
<pre className="px-3 py-2 text-muted-foreground whitespace-pre-wrap break-words">{preview}</pre>
|
|
44
|
-
<CollapsibleContent>
|
|
45
|
-
<pre className="px-3 py-2 max-h-40 overflow-auto whitespace-pre-wrap break-words border-t">{content}</pre>
|
|
46
|
-
</CollapsibleContent>
|
|
47
|
-
</Collapsible>
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// ─── Main renderer ────────────────────────────────────────────────────────────
|
|
52
|
-
|
|
53
|
-
export const PartRenderer = memo(function PartRenderer({ part, isStreaming, displayRenderers }: PartRendererProps) {
|
|
54
|
-
switch (part.type) {
|
|
55
|
-
case "text": {
|
|
56
|
-
const p = part as TextPart;
|
|
57
|
-
if (p.text.startsWith("[📎")) {
|
|
58
|
-
const firstNewline = p.text.indexOf("\n");
|
|
59
|
-
const header = firstNewline >= 0 ? p.text.slice(0, firstNewline) : p.text;
|
|
60
|
-
const body = firstNewline >= 0 ? p.text.slice(firstNewline + 1) : "";
|
|
61
|
-
const match = header.match(/^\[📎\s+(.+?)\]?$/);
|
|
62
|
-
const filename = match?.[1] ?? header;
|
|
63
|
-
return <AttachmentTextBlock filename={filename} content={body} />;
|
|
64
|
-
}
|
|
65
|
-
return <Markdown>{p.text}</Markdown>;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
case "reasoning": {
|
|
69
|
-
const p = part as ReasoningPart;
|
|
70
|
-
return <ReasoningBlock content={p.reasoning} isStreaming={isStreaming} />;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
case "tool-invocation": {
|
|
74
|
-
const { toolInvocation } = part as ToolInvocationPart;
|
|
75
|
-
// O openclaude-sdk entrega os display tools como meta-tools MCP com
|
|
76
|
-
// prefixo `mcp__display__display_*` (ex: mcp__display__display_highlight).
|
|
77
|
-
// O tipo especifico do widget vem no campo `action` do input/args
|
|
78
|
-
// (ex: {action: "metric", label: "...", value: "..."}).
|
|
79
|
-
//
|
|
80
|
-
// Alem disso aceitamos a forma "legada" direta `display_*` (compat).
|
|
81
|
-
const name = toolInvocation.toolName;
|
|
82
|
-
const isMcpDisplay = /^mcp__display__display_(highlight|collection|card|visual)$/.test(name);
|
|
83
|
-
const isLegacyDisplay = name.startsWith("display_");
|
|
84
|
-
const isDisplay = isMcpDisplay || isLegacyDisplay;
|
|
85
|
-
|
|
86
|
-
if (isDisplay) {
|
|
87
|
-
// IMPORTANTE: para display tools, o `args` (input) contem a definicao
|
|
88
|
-
// COMPLETA do widget. O `result` (tool_result) e apenas uma confirmacao
|
|
89
|
-
// minima (`{action: "metric"}`). Sempre preferimos args aqui.
|
|
90
|
-
const payload = toolInvocation.args as Record<string, unknown> | undefined;
|
|
91
|
-
// Para meta-tools MCP, a action vem no payload.
|
|
92
|
-
// Para display_* legado, a action e o sufixo do toolName.
|
|
93
|
-
const action = isMcpDisplay
|
|
94
|
-
? (payload?.action as string | undefined)
|
|
95
|
-
: name.replace(/^display_/, "");
|
|
96
|
-
const Renderer = action ? resolveDisplayRenderer(action, displayRenderers) : null;
|
|
97
|
-
if (payload && Renderer && action) {
|
|
98
|
-
// Alguns campos complexos (ex: trend, data) podem chegar serializados
|
|
99
|
-
// como JSON string se o modelo nao souber passar objetos. Tentamos
|
|
100
|
-
// parsear string → objeto antes de spreadar no renderer.
|
|
101
|
-
const normalized: Record<string, unknown> = {};
|
|
102
|
-
for (const [k, v] of Object.entries(payload)) {
|
|
103
|
-
if (typeof v === "string" && (v.startsWith("{") || v.startsWith("["))) {
|
|
104
|
-
try {
|
|
105
|
-
normalized[k] = JSON.parse(v);
|
|
106
|
-
} catch {
|
|
107
|
-
normalized[k] = v;
|
|
108
|
-
}
|
|
109
|
-
} else {
|
|
110
|
-
normalized[k] = v;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
// key by toolCallId so iframe-based renderers (DisplayReactRenderer)
|
|
114
|
-
// unmount/remount cleanly when a new tool_use block arrives.
|
|
115
|
-
const rendered = <Renderer key={toolInvocation.toolCallId} {...normalized} />;
|
|
116
|
-
// NOTE: LazyRender via IntersectionObserver nao funciona bem dentro
|
|
117
|
-
// do virtualized MessageList (itens ficam position:absolute e o
|
|
118
|
-
// observer nao dispara consistente). Renderizamos direto.
|
|
119
|
-
return rendered;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (toolInvocation.state === "result") {
|
|
124
|
-
return (
|
|
125
|
-
<ToolResult
|
|
126
|
-
toolName={toolInvocation.toolName}
|
|
127
|
-
result={toolInvocation.result}
|
|
128
|
-
isError={toolInvocation.isError}
|
|
129
|
-
/>
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return (
|
|
134
|
-
<ToolActivity
|
|
135
|
-
toolName={toolInvocation.toolName}
|
|
136
|
-
state={toolInvocation.state}
|
|
137
|
-
args={toolInvocation.args}
|
|
138
|
-
/>
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
default:
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
});
|
|
1
|
+
import { memo, useState } from "react";
|
|
2
|
+
import { Paperclip, ChevronDown } from "lucide-react";
|
|
3
|
+
import { Markdown } from "../components/Markdown.js";
|
|
4
|
+
import { LazyRender } from "../components/LazyRender.js";
|
|
5
|
+
import { ReasoningBlock } from "./ReasoningBlock.js";
|
|
6
|
+
import { ToolActivity } from "./ToolActivity.js";
|
|
7
|
+
import { ToolResult } from "./ToolResult.js";
|
|
8
|
+
import { resolveDisplayRenderer } from "../display/registry.js";
|
|
9
|
+
import type { DisplayRendererMap } from "../display/registry.js";
|
|
10
|
+
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "../ui/collapsible.js";
|
|
11
|
+
import { cn } from "../lib/utils.js";
|
|
12
|
+
import type { MessagePart, TextPart, ReasoningPart, ToolInvocationPart } from "../types.js";
|
|
13
|
+
|
|
14
|
+
const HEAVY_RENDERERS = new Set([
|
|
15
|
+
"chart", "map", "table",
|
|
16
|
+
"spreadsheet", "gallery", "image",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export interface PartRendererProps {
|
|
20
|
+
part: MessagePart;
|
|
21
|
+
isStreaming?: boolean;
|
|
22
|
+
displayRenderers?: DisplayRendererMap;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ─── Attachment sub-components ────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
function AttachmentTextBlock({ filename, content }: { filename: string; content: string }) {
|
|
28
|
+
const [open, setOpen] = useState(false);
|
|
29
|
+
const lines = content.split("\n");
|
|
30
|
+
const preview = lines.slice(0, 3).join("\n") + (lines.length > 3 && !open ? "\n…" : "");
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Collapsible open={open} onOpenChange={setOpen} className="rounded-lg border text-xs overflow-hidden">
|
|
34
|
+
<div className="flex items-center gap-2 px-3 py-2 bg-muted/50 border-b">
|
|
35
|
+
<Paperclip className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
36
|
+
<span className="flex-1 truncate font-medium">{filename}</span>
|
|
37
|
+
<CollapsibleTrigger asChild>
|
|
38
|
+
<button type="button" className="text-muted-foreground hover:text-foreground transition-colors" aria-label={open ? "Recolher" : "Expandir"}>
|
|
39
|
+
<ChevronDown className={cn("h-3.5 w-3.5 transition-transform", open && "rotate-180")} />
|
|
40
|
+
</button>
|
|
41
|
+
</CollapsibleTrigger>
|
|
42
|
+
</div>
|
|
43
|
+
<pre className="px-3 py-2 text-muted-foreground whitespace-pre-wrap break-words">{preview}</pre>
|
|
44
|
+
<CollapsibleContent>
|
|
45
|
+
<pre className="px-3 py-2 max-h-40 overflow-auto whitespace-pre-wrap break-words border-t">{content}</pre>
|
|
46
|
+
</CollapsibleContent>
|
|
47
|
+
</Collapsible>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Main renderer ────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
export const PartRenderer = memo(function PartRenderer({ part, isStreaming, displayRenderers }: PartRendererProps) {
|
|
54
|
+
switch (part.type) {
|
|
55
|
+
case "text": {
|
|
56
|
+
const p = part as TextPart;
|
|
57
|
+
if (p.text.startsWith("[📎")) {
|
|
58
|
+
const firstNewline = p.text.indexOf("\n");
|
|
59
|
+
const header = firstNewline >= 0 ? p.text.slice(0, firstNewline) : p.text;
|
|
60
|
+
const body = firstNewline >= 0 ? p.text.slice(firstNewline + 1) : "";
|
|
61
|
+
const match = header.match(/^\[📎\s+(.+?)\]?$/);
|
|
62
|
+
const filename = match?.[1] ?? header;
|
|
63
|
+
return <AttachmentTextBlock filename={filename} content={body} />;
|
|
64
|
+
}
|
|
65
|
+
return <Markdown>{p.text}</Markdown>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
case "reasoning": {
|
|
69
|
+
const p = part as ReasoningPart;
|
|
70
|
+
return <ReasoningBlock content={p.reasoning} isStreaming={isStreaming} />;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
case "tool-invocation": {
|
|
74
|
+
const { toolInvocation } = part as ToolInvocationPart;
|
|
75
|
+
// O openclaude-sdk entrega os display tools como meta-tools MCP com
|
|
76
|
+
// prefixo `mcp__display__display_*` (ex: mcp__display__display_highlight).
|
|
77
|
+
// O tipo especifico do widget vem no campo `action` do input/args
|
|
78
|
+
// (ex: {action: "metric", label: "...", value: "..."}).
|
|
79
|
+
//
|
|
80
|
+
// Alem disso aceitamos a forma "legada" direta `display_*` (compat).
|
|
81
|
+
const name = toolInvocation.toolName;
|
|
82
|
+
const isMcpDisplay = /^mcp__display__display_(highlight|collection|card|visual)$/.test(name);
|
|
83
|
+
const isLegacyDisplay = name.startsWith("display_");
|
|
84
|
+
const isDisplay = isMcpDisplay || isLegacyDisplay;
|
|
85
|
+
|
|
86
|
+
if (isDisplay) {
|
|
87
|
+
// IMPORTANTE: para display tools, o `args` (input) contem a definicao
|
|
88
|
+
// COMPLETA do widget. O `result` (tool_result) e apenas uma confirmacao
|
|
89
|
+
// minima (`{action: "metric"}`). Sempre preferimos args aqui.
|
|
90
|
+
const payload = toolInvocation.args as Record<string, unknown> | undefined;
|
|
91
|
+
// Para meta-tools MCP, a action vem no payload.
|
|
92
|
+
// Para display_* legado, a action e o sufixo do toolName.
|
|
93
|
+
const action = isMcpDisplay
|
|
94
|
+
? (payload?.action as string | undefined)
|
|
95
|
+
: name.replace(/^display_/, "");
|
|
96
|
+
const Renderer = action ? resolveDisplayRenderer(action, displayRenderers) : null;
|
|
97
|
+
if (payload && Renderer && action) {
|
|
98
|
+
// Alguns campos complexos (ex: trend, data) podem chegar serializados
|
|
99
|
+
// como JSON string se o modelo nao souber passar objetos. Tentamos
|
|
100
|
+
// parsear string → objeto antes de spreadar no renderer.
|
|
101
|
+
const normalized: Record<string, unknown> = {};
|
|
102
|
+
for (const [k, v] of Object.entries(payload)) {
|
|
103
|
+
if (typeof v === "string" && (v.startsWith("{") || v.startsWith("["))) {
|
|
104
|
+
try {
|
|
105
|
+
normalized[k] = JSON.parse(v);
|
|
106
|
+
} catch {
|
|
107
|
+
normalized[k] = v;
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
normalized[k] = v;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// key by toolCallId so iframe-based renderers (DisplayReactRenderer)
|
|
114
|
+
// unmount/remount cleanly when a new tool_use block arrives.
|
|
115
|
+
const rendered = <Renderer key={toolInvocation.toolCallId} {...normalized} />;
|
|
116
|
+
// NOTE: LazyRender via IntersectionObserver nao funciona bem dentro
|
|
117
|
+
// do virtualized MessageList (itens ficam position:absolute e o
|
|
118
|
+
// observer nao dispara consistente). Renderizamos direto.
|
|
119
|
+
return rendered;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (toolInvocation.state === "result") {
|
|
124
|
+
return (
|
|
125
|
+
<ToolResult
|
|
126
|
+
toolName={toolInvocation.toolName}
|
|
127
|
+
result={toolInvocation.result}
|
|
128
|
+
isError={toolInvocation.isError}
|
|
129
|
+
/>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<ToolActivity
|
|
135
|
+
toolName={toolInvocation.toolName}
|
|
136
|
+
state={toolInvocation.state}
|
|
137
|
+
args={toolInvocation.args}
|
|
138
|
+
/>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
default:
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
2
|
-
import { Brain, ChevronDown, ChevronRight } from "lucide-react";
|
|
3
|
-
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
|
|
4
|
-
|
|
5
|
-
export interface ReasoningBlockProps {
|
|
6
|
-
content: string;
|
|
7
|
-
isStreaming?: boolean;
|
|
8
|
-
className?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function ReasoningBlock({ content, isStreaming = false, className }: ReasoningBlockProps) {
|
|
12
|
-
const [expanded, setExpanded] = useState(isStreaming);
|
|
13
|
-
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
if (isStreaming) {
|
|
16
|
-
setExpanded(true);
|
|
17
|
-
} else {
|
|
18
|
-
setExpanded(false);
|
|
19
|
-
}
|
|
20
|
-
}, [isStreaming]);
|
|
21
|
-
|
|
22
|
-
return (
|
|
23
|
-
<Collapsible open={expanded} onOpenChange={setExpanded} className={className}>
|
|
24
|
-
<div className="border border-border bg-muted/30 rounded-md text-sm overflow-hidden">
|
|
25
|
-
<CollapsibleTrigger className="flex items-center gap-2 px-3 py-2 w-full text-left font-medium text-muted-foreground hover:bg-muted/60 cursor-pointer">
|
|
26
|
-
<Brain className="h-3.5 w-3.5" aria-hidden="true" />
|
|
27
|
-
<span className="font-mono text-xs">reasoning</span>
|
|
28
|
-
{expanded
|
|
29
|
-
? <ChevronDown className="h-3.5 w-3.5 ml-auto" aria-hidden="true" />
|
|
30
|
-
: <ChevronRight className="h-3.5 w-3.5 ml-auto" aria-hidden="true" />
|
|
31
|
-
}
|
|
32
|
-
</CollapsibleTrigger>
|
|
33
|
-
<CollapsibleContent>
|
|
34
|
-
<div className="max-h-96 overflow-y-auto px-3 py-3 text-muted-foreground whitespace-pre-wrap break-words border-t border-border text-xs">
|
|
35
|
-
{content}
|
|
36
|
-
</div>
|
|
37
|
-
</CollapsibleContent>
|
|
38
|
-
</div>
|
|
39
|
-
</Collapsible>
|
|
40
|
-
);
|
|
41
|
-
}
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Brain, ChevronDown, ChevronRight } from "lucide-react";
|
|
3
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
|
|
4
|
+
|
|
5
|
+
export interface ReasoningBlockProps {
|
|
6
|
+
content: string;
|
|
7
|
+
isStreaming?: boolean;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ReasoningBlock({ content, isStreaming = false, className }: ReasoningBlockProps) {
|
|
12
|
+
const [expanded, setExpanded] = useState(isStreaming);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (isStreaming) {
|
|
16
|
+
setExpanded(true);
|
|
17
|
+
} else {
|
|
18
|
+
setExpanded(false);
|
|
19
|
+
}
|
|
20
|
+
}, [isStreaming]);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Collapsible open={expanded} onOpenChange={setExpanded} className={className}>
|
|
24
|
+
<div className="border border-border bg-muted/30 rounded-md text-sm overflow-hidden">
|
|
25
|
+
<CollapsibleTrigger className="flex items-center gap-2 px-3 py-2 w-full text-left font-medium text-muted-foreground hover:bg-muted/60 cursor-pointer">
|
|
26
|
+
<Brain className="h-3.5 w-3.5" aria-hidden="true" />
|
|
27
|
+
<span className="font-mono text-xs">reasoning</span>
|
|
28
|
+
{expanded
|
|
29
|
+
? <ChevronDown className="h-3.5 w-3.5 ml-auto" aria-hidden="true" />
|
|
30
|
+
: <ChevronRight className="h-3.5 w-3.5 ml-auto" aria-hidden="true" />
|
|
31
|
+
}
|
|
32
|
+
</CollapsibleTrigger>
|
|
33
|
+
<CollapsibleContent>
|
|
34
|
+
<div className="max-h-96 overflow-y-auto px-3 py-3 text-muted-foreground whitespace-pre-wrap break-words border-t border-border text-xs">
|
|
35
|
+
{content}
|
|
36
|
+
</div>
|
|
37
|
+
</CollapsibleContent>
|
|
38
|
+
</div>
|
|
39
|
+
</Collapsible>
|
|
40
|
+
);
|
|
41
|
+
}
|