@springbrand/message-panel 0.1.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.
Files changed (38) hide show
  1. package/demo/camel-chat-showcase.tsx +1265 -0
  2. package/demo/fixtures.ts +292 -0
  3. package/demo/index.ts +4 -0
  4. package/demo/message-panel-chat-showcase.tsx +250 -0
  5. package/demo/message-panel-gallery.tsx +389 -0
  6. package/package.json +44 -0
  7. package/src/adapters/ai-sdk.ts +32 -0
  8. package/src/camel/camel-chat-messages.tsx +1420 -0
  9. package/src/camel/camel-model-select.tsx +88 -0
  10. package/src/camel/camel-plan-bar.tsx +115 -0
  11. package/src/camel/camel-prompt-input.tsx +540 -0
  12. package/src/camel/camel-tool-presentation.tsx +1270 -0
  13. package/src/camel/camel-turn.ts +315 -0
  14. package/src/camel/index.ts +7 -0
  15. package/src/camel/turn-summary-bar.tsx +81 -0
  16. package/src/chat-summary-panel.tsx +229 -0
  17. package/src/composer/chat-composer.tsx +216 -0
  18. package/src/composer/composer-attachments.tsx +138 -0
  19. package/src/composer/composer-trigger-popover.tsx +236 -0
  20. package/src/composer/composer.tsx +425 -0
  21. package/src/composer/drop-controller.ts +80 -0
  22. package/src/composer/index.ts +6 -0
  23. package/src/composer/types.ts +79 -0
  24. package/src/contracts.ts +139 -0
  25. package/src/conversation.tsx +76 -0
  26. package/src/index.ts +12 -0
  27. package/src/internal/cn.ts +3 -0
  28. package/src/message-panel.tsx +368 -0
  29. package/src/message.tsx +254 -0
  30. package/src/parts/index.tsx +1149 -0
  31. package/src/parts/kind.ts +67 -0
  32. package/src/parts/plan.ts +63 -0
  33. package/src/parts/primitives.tsx +439 -0
  34. package/src/registry.ts +44 -0
  35. package/src/status.tsx +132 -0
  36. package/src/styles/camel-chat.css +36 -0
  37. package/src/styles/index.css +2186 -0
  38. package/src/styles/tokens.css +31 -0
package/src/status.tsx ADDED
@@ -0,0 +1,132 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { ThinkingOrb, type OrbState } from "thinking-orbs";
3
+ import type { MessagePanelStatus } from "./contracts";
4
+ import { cn } from "./internal/cn";
5
+ import { resolvePartKind, toolNameOfPart } from "./parts/kind";
6
+
7
+ export type PanelVisualState =
8
+ | "empty"
9
+ | "hydrating"
10
+ | "thinking"
11
+ | "streaming"
12
+ | "ready"
13
+ | "error"
14
+ | "recovering"
15
+ | "background";
16
+
17
+ export function derivePanelVisualState({
18
+ messageCount,
19
+ status,
20
+ isHydrating = false,
21
+ isRecovering = false,
22
+ isServerStreaming = false,
23
+ }: {
24
+ messageCount: number;
25
+ status: MessagePanelStatus;
26
+ isHydrating?: boolean;
27
+ isRecovering?: boolean;
28
+ isServerStreaming?: boolean;
29
+ }): PanelVisualState {
30
+ if (isHydrating) return "hydrating";
31
+ if (status === "error") return "error";
32
+ if (isRecovering) return "recovering";
33
+ if (isServerStreaming) return "background";
34
+ if (status === "submitted") return "thinking";
35
+ if (status === "streaming") return "streaming";
36
+ return messageCount === 0 ? "empty" : "ready";
37
+ }
38
+
39
+ export function ThinkingIndicator({
40
+ label = "Thinking…",
41
+ state = "solving",
42
+ className,
43
+ }: {
44
+ label?: string;
45
+ state?: OrbState;
46
+ className?: string;
47
+ }) {
48
+ return (
49
+ <div className={cn("sb-thinking", className)} role="status" aria-label={label}>
50
+ <ThinkingOrb state={state} size={20} aria-hidden="true" />
51
+ <span>{label}</span>
52
+ </div>
53
+ );
54
+ }
55
+
56
+ export function deriveThinkingActivity(part?: unknown): {
57
+ state: OrbState;
58
+ label: string;
59
+ } {
60
+ const kind = resolvePartKind(part);
61
+ const toolName = toolNameOfPart(part);
62
+ if (kind === "reasoning") return { state: "solving", label: "Reasoning…" };
63
+ if (kind === "text") return { state: "composing", label: "Composing answer…" };
64
+ if (
65
+ ["plan", "image", "chart", "dashboard"].includes(kind ?? "") ||
66
+ (toolName != null &&
67
+ /(?:^|[_-])(?:image|chart|artifact|render|write_file)(?:$|[_-])/i.test(
68
+ toolName,
69
+ ))
70
+ ) {
71
+ return { state: "shaping", label: "Shaping result…" };
72
+ }
73
+ if (
74
+ toolName != null &&
75
+ /(?:^|[_-])(?:search|fetch|browse|browser|web)(?:$|[_-])/i.test(toolName)
76
+ ) {
77
+ return { state: "searching", label: "Searching…" };
78
+ }
79
+ if (toolName != null) {
80
+ return {
81
+ state: "working",
82
+ label: `Running ${toolName.replaceAll("_", " ")}…`,
83
+ };
84
+ }
85
+ return part === undefined
86
+ ? { state: "solving", label: "Thinking…" }
87
+ : { state: "working", label: "Working…" };
88
+ }
89
+
90
+ type LoadingPhase = "idle" | "loading" | "done";
91
+
92
+ export function TopLoadingBar({ status }: { status: MessagePanelStatus }) {
93
+ const [phase, setPhase] = useState<LoadingPhase>("idle");
94
+ const previous = useRef(status);
95
+
96
+ useEffect(() => {
97
+ const before = previous.current;
98
+ previous.current = status;
99
+ if (status === "submitted") {
100
+ setPhase("loading");
101
+ return;
102
+ }
103
+ if (before === "submitted" && status === "streaming") {
104
+ setPhase("done");
105
+ const timeout = window.setTimeout(() => setPhase("idle"), 500);
106
+ return () => window.clearTimeout(timeout);
107
+ }
108
+ if (status !== "streaming") setPhase("idle");
109
+ }, [status]);
110
+
111
+ if (phase === "idle") return null;
112
+ return (
113
+ <div className="sb-top-loading" role="progressbar" aria-label="等待响应">
114
+ <span data-phase={phase} />
115
+ </div>
116
+ );
117
+ }
118
+
119
+ export function PanelStatus({
120
+ kind,
121
+ children,
122
+ }: {
123
+ kind: "hydrating" | "recovering" | "background" | "error";
124
+ children: React.ReactNode;
125
+ }) {
126
+ return (
127
+ <div className="sb-panel-status" data-kind={kind} role={kind === "error" ? "alert" : "status"}>
128
+ <span aria-hidden="true" />
129
+ {children}
130
+ </div>
131
+ );
132
+ }
@@ -0,0 +1,36 @@
1
+ /* Streamdown inherits the app's shadcn tokens, as recommended by assistant-ui. */
2
+ .camel-chat-surface {
3
+ font-size: 14px;
4
+ }
5
+
6
+ .camel-chat-surface [data-camel-collapsible-content] {
7
+ display: grid;
8
+ grid-template-rows: 0fr;
9
+ opacity: 0;
10
+ visibility: hidden;
11
+ transition:
12
+ grid-template-rows 200ms ease-in,
13
+ opacity 150ms ease-in,
14
+ visibility 0s linear 200ms;
15
+ }
16
+
17
+ .camel-chat-surface [data-camel-collapsible-inner] {
18
+ min-height: 0;
19
+ overflow: hidden;
20
+ }
21
+
22
+ .camel-chat-surface
23
+ [data-camel-collapsible-content][data-state="open"] {
24
+ grid-template-rows: 1fr;
25
+ opacity: 1;
26
+ visibility: visible;
27
+ transition:
28
+ grid-template-rows 250ms ease-out,
29
+ opacity 200ms ease-out;
30
+ }
31
+
32
+ @media (prefers-reduced-motion: reduce) {
33
+ .camel-chat-surface [data-camel-collapsible-content] {
34
+ transition: none !important;
35
+ }
36
+ }