@djangocfg/widget-chat 0.1.1
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/LICENSE +21 -0
- package/README.md +124 -0
- package/package.json +95 -0
- package/src/composer/composer.tsx +100 -0
- package/src/composer/context.ts +33 -0
- package/src/composer/controls/button.tsx +104 -0
- package/src/composer/controls/language-menu.tsx +123 -0
- package/src/composer/controls/send.tsx +83 -0
- package/src/composer/controls/think.tsx +50 -0
- package/src/composer/controls/tools.tsx +130 -0
- package/src/composer/controls/tooltip.tsx +10 -0
- package/src/composer/controls/voice.tsx +266 -0
- package/src/composer/field/field.tsx +140 -0
- package/src/composer/field/input.tsx +214 -0
- package/src/composer/field/keys.ts +44 -0
- package/src/composer/field/use-autosize.ts +134 -0
- package/src/composer/focus-field.ts +27 -0
- package/src/composer/index.ts +86 -0
- package/src/composer/stories/field.tsx +112 -0
- package/src/composer/stories/harness.tsx +127 -0
- package/src/composer/tools/menu.tsx +129 -0
- package/src/composer/tools/rows.tsx +180 -0
- package/src/composer/tools/use-menu-nav.ts +66 -0
- package/src/composer/tools/use-recent-tools.ts +30 -0
- package/src/composer/tray/banner.tsx +157 -0
- package/src/composer/tray/command-progress.tsx +116 -0
- package/src/composer/tray/picked-tools.tsx +104 -0
- package/src/composer/tray/queue.tsx +202 -0
- package/src/composer/tray/use-replace-text.ts +58 -0
- package/src/composer/use-draft-persistence.ts +79 -0
- package/src/content/index.ts +16 -0
- package/src/content/media-content.tsx +38 -0
- package/src/content/response.tsx +221 -0
- package/src/core/adapter.ts +29 -0
- package/src/core/capabilities.ts +286 -0
- package/src/core/chunks.ts +66 -0
- package/src/core/content/incomplete-math.ts +118 -0
- package/src/core/content/index.ts +9 -0
- package/src/core/draft-storage.ts +62 -0
- package/src/core/events.ts +88 -0
- package/src/core/focus/index.ts +10 -0
- package/src/core/focus/roving.ts +56 -0
- package/src/core/group-parts.ts +80 -0
- package/src/core/index.ts +34 -0
- package/src/core/links/index.ts +33 -0
- package/src/core/links/prose.ts +191 -0
- package/src/core/links/scheme.ts +98 -0
- package/src/core/links/tool-id.ts +26 -0
- package/src/core/media/allowed-src.ts +138 -0
- package/src/core/media/block.ts +254 -0
- package/src/core/media/index.ts +22 -0
- package/src/core/media/kinds.ts +85 -0
- package/src/core/outbound.ts +86 -0
- package/src/core/parts.ts +169 -0
- package/src/core/queue/index.ts +15 -0
- package/src/core/queue/queue.ts +100 -0
- package/src/core/recall/boundary.ts +77 -0
- package/src/core/recall/from-transcript.ts +47 -0
- package/src/core/recall/index.ts +23 -0
- package/src/core/recall/ladder.ts +171 -0
- package/src/core/reload-decision.ts +36 -0
- package/src/core/scroll/index.ts +17 -0
- package/src/core/scroll/missed.ts +106 -0
- package/src/core/scroll/sent.ts +54 -0
- package/src/core/text-replace.ts +20 -0
- package/src/core/tool-name.ts +31 -0
- package/src/core/tool-picks-set.ts +48 -0
- package/src/core/tool-picks.ts +142 -0
- package/src/core/tool-prompt.ts +47 -0
- package/src/core/tools/capability.ts +34 -0
- package/src/core/tools/catalogue.ts +109 -0
- package/src/core/tools/index.ts +17 -0
- package/src/core/tools/layout.ts +44 -0
- package/src/core/tools/nav.ts +32 -0
- package/src/core/tools/rank.ts +60 -0
- package/src/core/tools/recent.ts +39 -0
- package/src/core/transcript.ts +301 -0
- package/src/core/voice/earcons.ts +58 -0
- package/src/core/voice/index.ts +26 -0
- package/src/core/voice/language-search.ts +62 -0
- package/src/core/voice/languages.ts +267 -0
- package/src/core/voice/resolve-language.ts +93 -0
- package/src/core/voice/session.ts +196 -0
- package/src/core/voice/web-speech.ts +170 -0
- package/src/i18n/index.ts +87 -0
- package/src/i18n/locales/ar.ts +97 -0
- package/src/i18n/locales/da.ts +90 -0
- package/src/i18n/locales/de.ts +90 -0
- package/src/i18n/locales/en.ts +90 -0
- package/src/i18n/locales/es.ts +90 -0
- package/src/i18n/locales/fr.ts +90 -0
- package/src/i18n/locales/it.ts +90 -0
- package/src/i18n/locales/ja.ts +83 -0
- package/src/i18n/locales/ko.ts +83 -0
- package/src/i18n/locales/nl.ts +90 -0
- package/src/i18n/locales/no.ts +90 -0
- package/src/i18n/locales/pl.ts +96 -0
- package/src/i18n/locales/ptBR.ts +90 -0
- package/src/i18n/locales/ru.ts +96 -0
- package/src/i18n/locales/sv.ts +90 -0
- package/src/i18n/locales/tr.ts +89 -0
- package/src/i18n/locales/zh.ts +83 -0
- package/src/i18n/plural.ts +62 -0
- package/src/i18n/types.ts +257 -0
- package/src/index.ts +18 -0
- package/src/primitives/index.ts +43 -0
- package/src/primitives/message.tsx +85 -0
- package/src/primitives/part-registry.tsx +44 -0
- package/src/primitives/stories-harness.tsx +118 -0
- package/src/primitives/suggestions.tsx +112 -0
- package/src/primitives/tool-call.tsx +145 -0
- package/src/primitives/tooltip.tsx +48 -0
- package/src/primitives/transcript.tsx +285 -0
- package/src/primitives/use-dismiss.ts +100 -0
- package/src/primitives/use-missed-count.ts +30 -0
- package/src/primitives/use-overflows.ts +69 -0
- package/src/primitives/use-return-on-send.ts +29 -0
- package/src/primitives/use-roving-focus.ts +69 -0
- package/src/primitives/use-select-all-transcript.ts +111 -0
- package/src/primitives/use-stick-to-bottom.ts +83 -0
- package/src/primitives/use-turn-activity.ts +25 -0
- package/src/store/context.ts +28 -0
- package/src/store/hooks.ts +215 -0
- package/src/store/index.ts +31 -0
- package/src/store/provider.tsx +97 -0
- package/src/store/store.ts +394 -0
- package/src/store/subscribers.ts +55 -0
- package/src/store/surfaces.ts +174 -0
- package/src/store/use-draft.ts +168 -0
- package/src/store/use-queue.ts +61 -0
- package/src/store/use-recall.ts +264 -0
- package/src/store/use-reload-on-revision.ts +67 -0
- package/src/store/use-speech-language.ts +84 -0
- package/src/store/use-voice.ts +116 -0
- package/src/styles/block.css +128 -0
- package/src/styles/composer.css +449 -0
- package/src/styles/index.css +47 -0
- package/src/styles/markdown.css +368 -0
- package/src/styles/surface.css +102 -0
- package/src/styles/tool.css +128 -0
- package/src/styles/turn-mark.css +97 -0
- package/src/testing/index.ts +15 -0
- package/src/testing/mock-adapter.ts +69 -0
- package/src/testing/mock-tools.ts +101 -0
- package/src/testing/mock-voice.ts +81 -0
- package/src/ui/chat-composer.tsx +286 -0
- package/src/ui/chat-reply-chip.tsx +42 -0
- package/src/ui/chat-suggestions.tsx +87 -0
- package/src/ui/chat-transcript.tsx +157 -0
- package/src/ui/chat.tsx +197 -0
- package/src/ui/collapsible-body.tsx +180 -0
- package/src/ui/collapsible-row.tsx +81 -0
- package/src/ui/day-divider.tsx +49 -0
- package/src/ui/index.ts +54 -0
- package/src/ui/jump-to-latest.tsx +60 -0
- package/src/ui/link-chip.tsx +73 -0
- package/src/ui/message-actions.tsx +163 -0
- package/src/ui/message-quote.tsx +47 -0
- package/src/ui/message.tsx +295 -0
- package/src/ui/pending.tsx +85 -0
- package/src/ui/renderers/data.tsx +63 -0
- package/src/ui/renderers/reasoning.tsx +73 -0
- package/src/ui/renderers/tool.tsx +176 -0
- package/src/ui/turn-mark.tsx +31 -0
- package/src/utils/day-heading.ts +46 -0
- package/src/utils/error-message.ts +10 -0
- package/src/utils/index.ts +12 -0
- package/src/utils/markdown-text.ts +175 -0
- package/src/widgets/index.ts +14 -0
- package/src/widgets/media.tsx +239 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
import type { StreamChunk } from "../core/chunks";
|
|
4
|
+
import type { ChatMessage } from "../core/parts";
|
|
5
|
+
import type { PartRenderProps } from "./part-registry";
|
|
6
|
+
import { ChatProvider, useChatStore } from "../store";
|
|
7
|
+
import { mockAdapter } from "../testing/mock-adapter";
|
|
8
|
+
import { Message } from "./message";
|
|
9
|
+
import { PartRegistryProvider, type PartRegistry } from "./part-registry";
|
|
10
|
+
import { Transcript } from "./transcript";
|
|
11
|
+
// From `ui/`, which primitives may not import — allowed here because the
|
|
12
|
+
// workbench never ships (`layers.test.ts` exempts it by name). Both are styled,
|
|
13
|
+
// and the tool renderer is the SHIPPED one, so a story cannot pass against a
|
|
14
|
+
// card the product does not draw.
|
|
15
|
+
import { PendingIndicator } from "../ui/pending";
|
|
16
|
+
import { ToolRenderer } from "../ui/renderers/tool";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The workbench the transcript stories share.
|
|
20
|
+
*
|
|
21
|
+
* One set of renderers, not one per story file: a second copy would let a
|
|
22
|
+
* story pass against renderers nothing ships, which is the failure mode a
|
|
23
|
+
* workbench exists to prevent.
|
|
24
|
+
*
|
|
25
|
+
* Light and dark come from the preview's `withThemeByClassName` decorator and
|
|
26
|
+
* its toolbar (`.storybook/preview.tsx`). Do NOT add a switcher or duplicate a
|
|
27
|
+
* story per theme; semantic token classes make one story set correct in both.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const bare = "grid gap-3 p-4 text-foreground";
|
|
31
|
+
|
|
32
|
+
function TextPart({ part }: PartRenderProps) {
|
|
33
|
+
if (part.type !== "text") return null;
|
|
34
|
+
return <p className="m-0 whitespace-pre-wrap">{part.text}</p>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ReasoningPart({ part }: PartRenderProps) {
|
|
38
|
+
if (part.type !== "reasoning") return null;
|
|
39
|
+
return (
|
|
40
|
+
<details>
|
|
41
|
+
<summary className="text-muted-foreground">Thinking</summary>
|
|
42
|
+
<p className="m-0 text-muted-foreground">{part.text}</p>
|
|
43
|
+
</details>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Tool calls render through the SHIPPED renderer, not a copy.
|
|
49
|
+
*
|
|
50
|
+
* This workbench used to keep its own: a pulsing dot, no disclosure, no running
|
|
51
|
+
* border. Every story passed while showing a screen the product no longer had —
|
|
52
|
+
* the exact failure a workbench exists to prevent. `ui/` is reachable here
|
|
53
|
+
* because `layers.test.ts` exempts this file by name, the same allowance
|
|
54
|
+
* `PendingIndicator` already uses.
|
|
55
|
+
*
|
|
56
|
+
* The subagent thread comes with it: `PartRegistryProvider` is a context, so a
|
|
57
|
+
* nested tool resolves through this same registry with nothing re-registered.
|
|
58
|
+
*/
|
|
59
|
+
const renderers: PartRegistry = {
|
|
60
|
+
text: TextPart,
|
|
61
|
+
reasoning: ReasoningPart,
|
|
62
|
+
tool: ToolRenderer,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export interface HarnessProps {
|
|
66
|
+
readonly script: readonly StreamChunk[];
|
|
67
|
+
readonly delayMs?: number;
|
|
68
|
+
readonly prompt?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Seed the transcript instead of streaming it. `ToolPart.messages` has no
|
|
71
|
+
* chunk in the wire vocabulary — an adapter that ran a subagent populates it
|
|
72
|
+
* directly — so `store.load()` is the only honest way to reach that state.
|
|
73
|
+
*/
|
|
74
|
+
readonly history?: readonly ChatMessage[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Drives one turn on mount so a story shows a conversation, not an empty box. */
|
|
78
|
+
export function Harness({ script, delayMs = 40, prompt = "Show me the log", history }: HarnessProps) {
|
|
79
|
+
return (
|
|
80
|
+
<ChatProvider adapter={mockAdapter({ script, delayMs })}>
|
|
81
|
+
<PartRegistryProvider renderers={renderers}>
|
|
82
|
+
{history === undefined ? <AutoSend prompt={prompt} /> : <LoadHistory messages={history} />}
|
|
83
|
+
<Transcript
|
|
84
|
+
empty={<p className="text-muted-foreground">No messages yet.</p>}
|
|
85
|
+
className="chat-transcript bg-background"
|
|
86
|
+
pending={() => (
|
|
87
|
+
<PendingIndicator
|
|
88
|
+
className="text-muted-foreground flex items-center gap-2 py-3 text-sm"
|
|
89
|
+
label="Thinking"
|
|
90
|
+
/>
|
|
91
|
+
)}
|
|
92
|
+
>
|
|
93
|
+
{(id) => (
|
|
94
|
+
<div className={bare}>
|
|
95
|
+
<Message id={id} />
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
</Transcript>
|
|
99
|
+
</PartRegistryProvider>
|
|
100
|
+
</ChatProvider>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function AutoSend({ prompt }: { readonly prompt: string }) {
|
|
105
|
+
const store = useChatStore();
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
void store.send({ text: prompt });
|
|
108
|
+
}, [store, prompt]);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function LoadHistory({ messages }: { readonly messages: readonly ChatMessage[] }) {
|
|
113
|
+
const store = useChatStore();
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
store.load(messages);
|
|
116
|
+
}, [store, messages]);
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import type { ChatSuggestion } from "../core/capabilities";
|
|
4
|
+
import { emptyDraft } from "../core/outbound";
|
|
5
|
+
import { useCapabilities, useChatStore, useHistoryLoaded, useMessageIds } from "../store";
|
|
6
|
+
import { useTurnActivity } from "./use-turn-activity";
|
|
7
|
+
|
|
8
|
+
export interface SuggestionsProps {
|
|
9
|
+
/**
|
|
10
|
+
* Render one row. Called with the suggestion and the handler that sends it.
|
|
11
|
+
*
|
|
12
|
+
* A render prop rather than a built-in button, for the reason `jumpToLatest`
|
|
13
|
+
* is one: the chrome and the icon are the host's, and this package ships no
|
|
14
|
+
* icon library. A primitive that decides its own chrome is how a primitive
|
|
15
|
+
* turns back into 83 props.
|
|
16
|
+
*/
|
|
17
|
+
readonly children: (props: SuggestionRenderProps) => ReactNode;
|
|
18
|
+
/**
|
|
19
|
+
* Wrapped around the rows. Absent ⇒ they render as siblings, which is what a
|
|
20
|
+
* host supplying its own container wants.
|
|
21
|
+
*/
|
|
22
|
+
readonly className?: string;
|
|
23
|
+
/**
|
|
24
|
+
* The list's accessible name. Copy is the host's — no locales here.
|
|
25
|
+
*
|
|
26
|
+
* Without it the group is an unnamed landmark and a screen reader announces
|
|
27
|
+
* only "list", with nothing to say what these rows start.
|
|
28
|
+
*/
|
|
29
|
+
readonly label?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SuggestionRenderProps {
|
|
33
|
+
readonly suggestion: ChatSuggestion;
|
|
34
|
+
/** Send this suggestion's prompt. Already bound; the row only calls it. */
|
|
35
|
+
readonly onSelect: () => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The starter rows for an empty room.
|
|
40
|
+
*
|
|
41
|
+
* Renders NOTHING when the capability is absent, the list is empty, or the room
|
|
42
|
+
* is not empty — never an empty container, which would still spend the parent's
|
|
43
|
+
* `gap` and push the composer down.
|
|
44
|
+
*
|
|
45
|
+
* It owns the emptiness test itself, so every mount point agrees on it.
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* Whether starter rows will render right now.
|
|
49
|
+
*
|
|
50
|
+
* Exported because the transcript's "no messages yet" line is a FALLBACK for
|
|
51
|
+
* exactly this: when rows are offered they already say the room is empty and
|
|
52
|
+
* name what to do, so the line becomes a caption for the obvious. One predicate,
|
|
53
|
+
* two readers, so the two can never both appear or both vanish.
|
|
54
|
+
*/
|
|
55
|
+
export function useHasSuggestions(): boolean {
|
|
56
|
+
const { suggestions } = useCapabilities();
|
|
57
|
+
const ids = useMessageIds();
|
|
58
|
+
const historyLoaded = useHistoryLoaded();
|
|
59
|
+
const activity = useTurnActivity();
|
|
60
|
+
|
|
61
|
+
// The two suppressions `<Transcript>` applies to its own empty state, both
|
|
62
|
+
// bought with incidents: a first turn sent into a fresh room is not an empty
|
|
63
|
+
// room, and an unanswered fetch is not either — a 90-message room would
|
|
64
|
+
// otherwise open on starter prompts while history loads.
|
|
65
|
+
const roomIsEmpty = ids.length === 0 && historyLoaded && activity === null;
|
|
66
|
+
|
|
67
|
+
return roomIsEmpty && (suggestions?.items.length ?? 0) > 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function Suggestions({ children, className, label }: SuggestionsProps) {
|
|
71
|
+
const { suggestions } = useCapabilities();
|
|
72
|
+
const store = useChatStore();
|
|
73
|
+
const show = useHasSuggestions();
|
|
74
|
+
const items = suggestions?.items ?? [];
|
|
75
|
+
|
|
76
|
+
if (!show || suggestions === undefined) return null;
|
|
77
|
+
|
|
78
|
+
const { onPick, replyLanguage } = suggestions;
|
|
79
|
+
// `store.send`, not the adapter: it queues behind a running turn and mints the
|
|
80
|
+
// optimistic row, so a picked row and a typed message are one event
|
|
81
|
+
// downstream. `prompt`, never `title` — the title is an abbreviation.
|
|
82
|
+
//
|
|
83
|
+
// The language sentence is appended, not hidden: whatever is sent is what the
|
|
84
|
+
// transcript stores forever, and a hidden marker would need every surface
|
|
85
|
+
// (web, messengers, search, export) to strip it and keep stripping it.
|
|
86
|
+
const pick = (suggestion: ChatSuggestion) => {
|
|
87
|
+
const text = replyLanguage
|
|
88
|
+
? `${suggestion.prompt} Reply in ${replyLanguage}.`
|
|
89
|
+
: suggestion.prompt;
|
|
90
|
+
void store.send({ ...emptyDraft, text });
|
|
91
|
+
onPick?.(suggestion);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Built here so the JSX below only lays out values that exist: an inline
|
|
95
|
+
// `() => pick(s)` is a new function per row per render.
|
|
96
|
+
const rows = items.map((suggestion) => ({
|
|
97
|
+
id: suggestion.id,
|
|
98
|
+
node: children({ suggestion, onSelect: () => pick(suggestion) }),
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
// `listitem` on a wrapper, not on the host's row: a button carrying both roles
|
|
102
|
+
// loses one of them depending on the reader.
|
|
103
|
+
return (
|
|
104
|
+
<div className={className} role="list" aria-label={label}>
|
|
105
|
+
{rows.map((row) => (
|
|
106
|
+
<div key={row.id} role="listitem" className="contents">
|
|
107
|
+
{row.node}
|
|
108
|
+
</div>
|
|
109
|
+
))}
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { subagentMessages, type ChatMessage, type ToolPart } from "../core/parts";
|
|
4
|
+
import { MessageContent } from "./message";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A tool call, as composed parts.
|
|
8
|
+
*
|
|
9
|
+
* The root takes `part` and `children` and nothing else. Everything that would
|
|
10
|
+
* have been `showInput` / `showOutput` / `inputLabel` is a component the host
|
|
11
|
+
* either renders or does not — AI Elements' shape, and the concrete cure for
|
|
12
|
+
* "a boolean prop is a component someone declined to write".
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const ToolPartContext = createContext<ToolPart | null>(null);
|
|
16
|
+
|
|
17
|
+
function useToolPart(): ToolPart {
|
|
18
|
+
const part = useContext(ToolPartContext);
|
|
19
|
+
if (part === null) throw new Error("tool-call parts must render inside <ToolCall>");
|
|
20
|
+
return part;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ToolCallProps {
|
|
24
|
+
readonly part: ToolPart;
|
|
25
|
+
readonly children: ReactNode;
|
|
26
|
+
readonly className?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Disclosure state, as DATA on the root. The host owns the rule; this only
|
|
29
|
+
* publishes the answer, so a stylesheet can turn the chevron without the
|
|
30
|
+
* primitive knowing a chevron exists.
|
|
31
|
+
*/
|
|
32
|
+
readonly open?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function ToolCall({ part, children, className, open }: ToolCallProps) {
|
|
36
|
+
return (
|
|
37
|
+
<ToolPartContext.Provider value={part}>
|
|
38
|
+
<div
|
|
39
|
+
className={className}
|
|
40
|
+
data-tool={part.toolName}
|
|
41
|
+
data-state={part.toolState.state}
|
|
42
|
+
// Absent rather than `false`: CSS selects on presence, and
|
|
43
|
+
// `data-open="false"` matches `[data-open]` just as well as "true".
|
|
44
|
+
data-open={open === true ? "" : undefined}
|
|
45
|
+
>
|
|
46
|
+
{children}
|
|
47
|
+
</div>
|
|
48
|
+
</ToolPartContext.Provider>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ToolCallHeaderProps {
|
|
53
|
+
readonly className?: string;
|
|
54
|
+
/** Receives the live state so a host can label it without reading context. */
|
|
55
|
+
readonly children?: (part: ToolPart) => ReactNode;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function ToolCallHeader({ className, children }: ToolCallHeaderProps) {
|
|
59
|
+
const part = useToolPart();
|
|
60
|
+
return (
|
|
61
|
+
<div className={className}>{children === undefined ? part.toolName : children(part)}</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ToolCallInputProps {
|
|
66
|
+
readonly className?: string;
|
|
67
|
+
readonly children?: (input: unknown, streaming: boolean) => ReactNode;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Arguments, including while they stream in.
|
|
72
|
+
*
|
|
73
|
+
* During `input-streaming` the payload is a PARTIAL JSON string, not an object.
|
|
74
|
+
* Handing a host `JSON.parse`d garbage — or throwing — is how a tool card
|
|
75
|
+
* breaks the whole transcript mid-turn, so the raw text is passed through with
|
|
76
|
+
* `streaming` true and the host decides what to show.
|
|
77
|
+
*/
|
|
78
|
+
export function ToolCallInput({ className, children }: ToolCallInputProps) {
|
|
79
|
+
const { toolState } = useToolPart();
|
|
80
|
+
const streaming = toolState.state === "input-streaming";
|
|
81
|
+
const input = streaming ? toolState.inputText : toolState.input;
|
|
82
|
+
if (children !== undefined) return <div className={className}>{children(input, streaming)}</div>;
|
|
83
|
+
return <div className={className}>{typeof input === "string" ? input : JSON.stringify(input)}</div>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ToolCallOutputProps {
|
|
87
|
+
readonly className?: string;
|
|
88
|
+
readonly children?: (output: unknown) => ReactNode;
|
|
89
|
+
/** Rendered instead when the call failed. Absent ⇒ the error text is shown. */
|
|
90
|
+
readonly error?: (message: string) => ReactNode;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function ToolCallOutput({ className, children, error }: ToolCallOutputProps) {
|
|
94
|
+
const { toolState } = useToolPart();
|
|
95
|
+
|
|
96
|
+
if (toolState.state === "output-error") {
|
|
97
|
+
return (
|
|
98
|
+
<div className={className} data-error="">
|
|
99
|
+
{error === undefined ? toolState.error : error(toolState.error)}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
// Nothing to show yet is NOT an empty output: rendering an empty box while a
|
|
104
|
+
// tool runs reads as "it returned nothing".
|
|
105
|
+
if (toolState.state !== "output-available") return null;
|
|
106
|
+
|
|
107
|
+
const { output } = toolState;
|
|
108
|
+
if (children !== undefined) return <div className={className}>{children(output)}</div>;
|
|
109
|
+
return (
|
|
110
|
+
<div className={className}>{typeof output === "string" ? output : JSON.stringify(output)}</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface SubagentThreadProps {
|
|
115
|
+
readonly className?: string;
|
|
116
|
+
readonly children?: (message: ChatMessage) => ReactNode;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The subagent's own conversation, nested and read-only.
|
|
121
|
+
*
|
|
122
|
+
* Read-only is a property of the data, not a flag: these messages live inside a
|
|
123
|
+
* completed tool call, so there is no composer and nothing to send. Renders
|
|
124
|
+
* nothing when the tool ran no subagent, which is the ordinary case.
|
|
125
|
+
*
|
|
126
|
+
* Part renderers are inherited from the enclosing registry — see
|
|
127
|
+
* `part-registry.tsx`. A nested thread that had to re-register them would
|
|
128
|
+
* silently lose whichever the host forgot.
|
|
129
|
+
*/
|
|
130
|
+
export function SubagentThread({ className, children }: SubagentThreadProps) {
|
|
131
|
+
const messages = subagentMessages(useToolPart());
|
|
132
|
+
if (messages === undefined) return null;
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div className={className} data-subagent="">
|
|
136
|
+
{messages.map((message) =>
|
|
137
|
+
children === undefined ? (
|
|
138
|
+
<MessageContent key={message.id} message={message} />
|
|
139
|
+
) : (
|
|
140
|
+
<div key={message.id}>{children(message)}</div>
|
|
141
|
+
),
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "@djangocfg/ui-core";
|
|
3
|
+
|
|
4
|
+
export interface ChatTooltipProps {
|
|
5
|
+
/** The hover copy. Falsy → the child renders BARE, keeping `asChild` chains intact. */
|
|
6
|
+
readonly label?: ReactNode;
|
|
7
|
+
readonly children: ReactNode;
|
|
8
|
+
readonly side?: "top" | "bottom" | "left" | "right";
|
|
9
|
+
readonly align?: "start" | "center" | "end";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Hover name for one icon-only control in this package.
|
|
14
|
+
*
|
|
15
|
+
* The ordinary design-system Tooltip, not the status bar's: that wrapper is
|
|
16
|
+
* shaped for a live multi-line readout, and importing it here would couple a
|
|
17
|
+
* chat control to another surface's presentation.
|
|
18
|
+
*
|
|
19
|
+
* In `primitives` rather than beside the composer because two surfaces use it —
|
|
20
|
+
* the composer row and the per-message action row. A control that names itself
|
|
21
|
+
* on hover is not a composer idea, and leaving it under `composer/` would make
|
|
22
|
+
* the message row import a component whose name says it belongs elsewhere.
|
|
23
|
+
*
|
|
24
|
+
* The provider is mounted app-wide, so this adds none — a second one would give
|
|
25
|
+
* a row its own delay and skip behaviour.
|
|
26
|
+
*
|
|
27
|
+
* Content MIRRORS each control's `aria-label`. Radix links it with
|
|
28
|
+
* `aria-describedby`, so the name is not doubled; a different sentence here
|
|
29
|
+
* would be the only version a pointer user ever reads.
|
|
30
|
+
*/
|
|
31
|
+
export function ChatTooltip({
|
|
32
|
+
label,
|
|
33
|
+
children,
|
|
34
|
+
side = "top",
|
|
35
|
+
align = "center",
|
|
36
|
+
}: ChatTooltipProps): ReactElement {
|
|
37
|
+
// Bare, never a Fragment: an outer `PopoverTrigger asChild` clones a single
|
|
38
|
+
// element's ref and a Fragment breaks that clone.
|
|
39
|
+
if (!label) return children as ReactElement;
|
|
40
|
+
return (
|
|
41
|
+
<Tooltip>
|
|
42
|
+
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
|
43
|
+
<TooltipContent side={side} align={align} className="max-w-64">
|
|
44
|
+
{label}
|
|
45
|
+
</TooltipContent>
|
|
46
|
+
</Tooltip>
|
|
47
|
+
);
|
|
48
|
+
}
|