@aomi-labs/widget-lib 1.0.0 → 1.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.
Files changed (76) hide show
  1. package/dist/accordion.json +18 -0
  2. package/dist/alert.json +17 -0
  3. package/dist/aomi-frame.json +24 -0
  4. package/dist/assistant-thread-list.json +22 -0
  5. package/dist/assistant-thread.json +27 -0
  6. package/dist/assistant-threadlist-sidebar.json +20 -0
  7. package/dist/assistant-tool-fallback.json +20 -0
  8. package/dist/avatar.json +17 -0
  9. package/dist/badge.json +17 -0
  10. package/dist/breadcrumb.json +17 -0
  11. package/dist/button.json +18 -0
  12. package/dist/card.json +15 -0
  13. package/dist/collapsible.json +17 -0
  14. package/dist/command.json +21 -0
  15. package/dist/dialog.json +18 -0
  16. package/dist/drawer.json +17 -0
  17. package/dist/input.json +15 -0
  18. package/dist/label.json +15 -0
  19. package/dist/notification.json +20 -0
  20. package/dist/popover.json +17 -0
  21. package/dist/registry.json +429 -0
  22. package/dist/separator.json +17 -0
  23. package/dist/sheet.json +18 -0
  24. package/dist/sidebar.json +18 -0
  25. package/dist/skeleton.json +15 -0
  26. package/dist/sonner.json +17 -0
  27. package/dist/tooltip.json +17 -0
  28. package/package.json +27 -85
  29. package/src/components/aomi-frame.tsx +221 -0
  30. package/src/components/assistant-ui/attachment.tsx +235 -0
  31. package/src/components/assistant-ui/markdown-text.tsx +228 -0
  32. package/src/components/assistant-ui/thread-list.tsx +106 -0
  33. package/src/components/assistant-ui/thread.tsx +476 -0
  34. package/src/components/assistant-ui/threadlist-sidebar.tsx +66 -0
  35. package/src/components/assistant-ui/tool-fallback.tsx +48 -0
  36. package/src/components/assistant-ui/tooltip-icon-button.tsx +42 -0
  37. package/src/components/control-bar/api-key-input.tsx +122 -0
  38. package/src/components/control-bar/index.tsx +58 -0
  39. package/src/components/control-bar/model-select.tsx +120 -0
  40. package/src/components/control-bar/namespace-select.tsx +117 -0
  41. package/src/components/control-bar/wallet-connect.tsx +75 -0
  42. package/src/components/test/ThreadContextTest.tsx +204 -0
  43. package/src/components/tools/example-tool/ExampleTool.tsx +102 -0
  44. package/src/components/ui/accordion.tsx +58 -0
  45. package/src/components/ui/alert.tsx +62 -0
  46. package/src/components/ui/avatar.tsx +53 -0
  47. package/src/components/ui/badge.tsx +37 -0
  48. package/src/components/ui/breadcrumb.tsx +109 -0
  49. package/src/components/ui/button.tsx +59 -0
  50. package/src/components/ui/card.tsx +86 -0
  51. package/src/components/ui/collapsible.tsx +12 -0
  52. package/src/components/ui/command.tsx +156 -0
  53. package/src/components/ui/dialog.tsx +143 -0
  54. package/src/components/ui/drawer.tsx +118 -0
  55. package/src/components/ui/input.tsx +21 -0
  56. package/src/components/ui/label.tsx +20 -0
  57. package/src/components/ui/notification.tsx +57 -0
  58. package/src/components/ui/popover.tsx +33 -0
  59. package/src/components/ui/separator.tsx +28 -0
  60. package/src/components/ui/sheet.tsx +139 -0
  61. package/src/components/ui/sidebar.tsx +827 -0
  62. package/src/components/ui/skeleton.tsx +15 -0
  63. package/src/components/ui/sonner.tsx +29 -0
  64. package/src/components/ui/tooltip.tsx +61 -0
  65. package/src/hooks/use-mobile.ts +21 -0
  66. package/src/index.ts +26 -0
  67. package/src/registry.ts +218 -0
  68. package/{dist/styles.css → src/themes/default.css} +21 -3
  69. package/src/themes/tokens.config.ts +39 -0
  70. package/README.md +0 -41
  71. package/dist/index.cjs +0 -3780
  72. package/dist/index.cjs.map +0 -1
  73. package/dist/index.d.cts +0 -302
  74. package/dist/index.d.ts +0 -302
  75. package/dist/index.js +0 -3696
  76. package/dist/index.js.map +0 -1
@@ -0,0 +1,122 @@
1
+ "use client";
2
+
3
+ import { useState, type FC } from "react";
4
+ import { KeyIcon, CheckIcon, EyeIcon, EyeOffIcon } from "lucide-react";
5
+ import { useControl, cn } from "@aomi-labs/react";
6
+ import { Button } from "@/components/ui/button";
7
+ import { Input } from "@/components/ui/input";
8
+ import { Label } from "@/components/ui/label";
9
+ import {
10
+ Dialog,
11
+ DialogContent,
12
+ DialogDescription,
13
+ DialogHeader,
14
+ DialogTitle,
15
+ DialogTrigger,
16
+ DialogFooter,
17
+ } from "@/components/ui/dialog";
18
+
19
+ export type ApiKeyInputProps = {
20
+ className?: string;
21
+ title?: string;
22
+ description?: string;
23
+ };
24
+
25
+ export const ApiKeyInput: FC<ApiKeyInputProps> = ({
26
+ className,
27
+ title = "Aomi API Key",
28
+ description = "Enter your API key to authenticate with Aomi services.",
29
+ }) => {
30
+ const { state, setState } = useControl();
31
+ const [open, setOpen] = useState(false);
32
+ const [inputValue, setInputValue] = useState("");
33
+ const [showKey, setShowKey] = useState(false);
34
+
35
+ const hasApiKey = Boolean(state.apiKey);
36
+
37
+ return (
38
+ <Dialog open={open} onOpenChange={setOpen}>
39
+ <DialogTrigger asChild>
40
+ <Button
41
+ variant="ghost"
42
+ size="icon"
43
+ className={cn("relative rounded-full", className)}
44
+ aria-label={hasApiKey ? "API key configured" : "Set API key"}
45
+ >
46
+ <KeyIcon className={cn("h-4 w-4", hasApiKey && "text-green-500")} />
47
+ {hasApiKey && (
48
+ <span className="absolute -right-0.5 -top-0.5 h-2 w-2 rounded-full bg-green-500" />
49
+ )}
50
+ </Button>
51
+ </DialogTrigger>
52
+ <DialogContent className="max-w-[280px] pl-4 rounded-3xl">
53
+ <DialogHeader className="border-0">
54
+ <DialogTitle>{title}</DialogTitle>
55
+ <DialogDescription>{description}</DialogDescription>
56
+ </DialogHeader>
57
+ <div className="grid gap-4 py-4">
58
+ <div className="grid gap-2">
59
+ <Label htmlFor="api-key" className="mb-2">
60
+ API Key
61
+ </Label>
62
+ <div className="relative">
63
+ <Input
64
+ id="api-key"
65
+ type={showKey ? "text" : "password"}
66
+ placeholder={hasApiKey ? "********" : "Enter your API key"}
67
+ value={inputValue}
68
+ onChange={(e) => setInputValue(e.target.value)}
69
+ className="rounded-full pr-10"
70
+ />
71
+ <Button
72
+ type="button"
73
+ variant="ghost"
74
+ size="icon"
75
+ className="absolute right-0 top-0 h-full px-3 hover:bg-transparent"
76
+ onClick={() => setShowKey(!showKey)}
77
+ aria-label={showKey ? "Hide API key" : "Show API key"}
78
+ >
79
+ {showKey ? (
80
+ <EyeIcon className="h-4 w-4" />
81
+ ) : (
82
+ <EyeOffIcon className="h-4 w-4" />
83
+ )}
84
+ </Button>
85
+ </div>
86
+ {hasApiKey && (
87
+ <p className="text-muted-foreground text-xs">
88
+ <CheckIcon className="mr-1 inline h-3 w-3 text-green-500" />
89
+ API key is configured
90
+ </p>
91
+ )}
92
+ </div>
93
+ </div>
94
+ <DialogFooter>
95
+ {hasApiKey && (
96
+ <Button
97
+ variant="outline"
98
+ onClick={() => {
99
+ setState({ apiKey: null });
100
+ setInputValue("");
101
+ }}
102
+ >
103
+ Clear
104
+ </Button>
105
+ )}
106
+ <Button
107
+ onClick={() => {
108
+ if (inputValue.trim()) {
109
+ setState({ apiKey: inputValue.trim() });
110
+ setOpen(false);
111
+ setInputValue("");
112
+ }
113
+ }}
114
+ disabled={!inputValue.trim()}
115
+ >
116
+ Save
117
+ </Button>
118
+ </DialogFooter>
119
+ </DialogContent>
120
+ </Dialog>
121
+ );
122
+ };
@@ -0,0 +1,58 @@
1
+ "use client";
2
+
3
+ import type { ReactNode, FC } from "react";
4
+ import { cn } from "@aomi-labs/react";
5
+ import { ModelSelect } from "./model-select";
6
+ import { NamespaceSelect } from "./namespace-select";
7
+ import { ApiKeyInput } from "./api-key-input";
8
+ import { WalletConnect } from "./wallet-connect";
9
+
10
+ // =============================================================================
11
+ // Types
12
+ // =============================================================================
13
+
14
+ export type ControlBarProps = {
15
+ className?: string;
16
+ /** Custom controls to render alongside built-in ones */
17
+ children?: ReactNode;
18
+ /** Hide the model selector */
19
+ hideModel?: boolean;
20
+ /** Hide the namespace/agent selector */
21
+ hideNamespace?: boolean;
22
+ /** Hide the API key input */
23
+ hideApiKey?: boolean;
24
+ /** Hide the wallet connect button (default: true) */
25
+ hideWallet?: boolean;
26
+ };
27
+
28
+ // =============================================================================
29
+ // Main Component
30
+ // =============================================================================
31
+
32
+ export const ControlBar: FC<ControlBarProps> = ({
33
+ className,
34
+ children,
35
+ hideModel = false,
36
+ hideNamespace = false,
37
+ hideApiKey = false,
38
+ hideWallet = true,
39
+ }) => {
40
+ return (
41
+ <div className={cn("flex items-center gap-2", className)}>
42
+ {!hideModel && <ModelSelect />}
43
+ {!hideNamespace && <NamespaceSelect />}
44
+ {!hideWallet && <WalletConnect />}
45
+ {children}
46
+ {!hideApiKey && <ApiKeyInput />}
47
+ </div>
48
+ );
49
+ };
50
+
51
+ // =============================================================================
52
+ // Re-exports for granular usage
53
+ // =============================================================================
54
+
55
+ export { ModelSelect, type ModelSelectProps } from "./model-select";
56
+ export { NamespaceSelect, type NamespaceSelectProps } from "./namespace-select";
57
+ export { ApiKeyInput, type ApiKeyInputProps } from "./api-key-input";
58
+ export { WalletConnect, type WalletConnectProps } from "./wallet-connect";
@@ -0,0 +1,120 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, type FC } from "react";
4
+ import { ChevronDownIcon, CheckIcon } from "lucide-react";
5
+ import { useControl, cn } from "@aomi-labs/react";
6
+ import { Button } from "@/components/ui/button";
7
+ import {
8
+ Popover,
9
+ PopoverContent,
10
+ PopoverTrigger,
11
+ } from "@/components/ui/popover";
12
+
13
+ export type ModelSelectProps = {
14
+ className?: string;
15
+ placeholder?: string;
16
+ };
17
+
18
+ export const ModelSelect: FC<ModelSelectProps> = ({
19
+ className,
20
+ placeholder = "Select model",
21
+ }) => {
22
+ const {
23
+ state,
24
+ getAvailableModels,
25
+ getCurrentThreadControl,
26
+ onModelSelect,
27
+ isProcessing,
28
+ } = useControl();
29
+ const [open, setOpen] = useState(false);
30
+
31
+ // Fetch available models on mount
32
+ useEffect(() => {
33
+ void getAvailableModels();
34
+ }, [getAvailableModels]);
35
+
36
+ // Get current thread's selected model (or fall back to default)
37
+ const threadControl = getCurrentThreadControl();
38
+ const selectedModel =
39
+ threadControl.model ?? state.defaultModel ?? state.availableModels[0];
40
+
41
+ const models = state.availableModels.length > 0 ? state.availableModels : [];
42
+
43
+ // Don't render if no models available
44
+ if (models.length === 0) {
45
+ return (
46
+ <Button
47
+ variant="ghost"
48
+ disabled
49
+ className={cn(
50
+ "h-8 w-auto min-w-[100px] rounded-full px-2 text-xs",
51
+ "text-muted-foreground",
52
+ className,
53
+ )}
54
+ >
55
+ <span className="truncate">Loading...</span>
56
+ </Button>
57
+ );
58
+ }
59
+
60
+ return (
61
+ <Popover open={open} onOpenChange={setOpen}>
62
+ <PopoverTrigger asChild>
63
+ <Button
64
+ variant="ghost"
65
+ role="combobox"
66
+ aria-expanded={open}
67
+ disabled={isProcessing}
68
+ className={cn(
69
+ "h-8 w-auto min-w-[100px] justify-between rounded-full px-2 text-xs",
70
+ "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
71
+ isProcessing && "cursor-not-allowed opacity-50",
72
+ className,
73
+ )}
74
+ >
75
+ <span className="truncate">{selectedModel || placeholder}</span>
76
+ <ChevronDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
77
+ </Button>
78
+ </PopoverTrigger>
79
+ <PopoverContent
80
+ align="center"
81
+ sideOffset={-40}
82
+ className="w-[220px] rounded-3xl p-1 shadow-none"
83
+ >
84
+ <div className="flex flex-col gap-0.5">
85
+ {models.map((model) => (
86
+ <button
87
+ key={model}
88
+ disabled={isProcessing}
89
+ onClick={() => {
90
+ console.log("[ModelSelect] clicked", { model, isProcessing });
91
+ if (isProcessing) return;
92
+ setOpen(false);
93
+ console.log("[ModelSelect] calling onModelSelect", { model });
94
+ void onModelSelect(model)
95
+ .then(() => {
96
+ console.log("[ModelSelect] onModelSelect completed", {
97
+ model,
98
+ });
99
+ })
100
+ .catch((err) => {
101
+ console.error("[ModelSelect] onModelSelect failed:", err);
102
+ });
103
+ }}
104
+ className={cn(
105
+ "flex w-full items-center justify-between gap-2 rounded-full px-3 py-2 text-sm outline-none",
106
+ "hover:bg-accent hover:text-accent-foreground",
107
+ "focus:bg-accent focus:text-accent-foreground",
108
+ selectedModel === model && "bg-accent",
109
+ isProcessing && "cursor-not-allowed opacity-50",
110
+ )}
111
+ >
112
+ <span>{model}</span>
113
+ {selectedModel === model && <CheckIcon className="h-4 w-4" />}
114
+ </button>
115
+ ))}
116
+ </div>
117
+ </PopoverContent>
118
+ </Popover>
119
+ );
120
+ };
@@ -0,0 +1,117 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, type FC } from "react";
4
+ import { ChevronDownIcon, CheckIcon } from "lucide-react";
5
+ import { useControl, cn } from "@aomi-labs/react";
6
+ import { Button } from "@/components/ui/button";
7
+ import {
8
+ Popover,
9
+ PopoverContent,
10
+ PopoverTrigger,
11
+ } from "@/components/ui/popover";
12
+
13
+ export type NamespaceSelectProps = {
14
+ className?: string;
15
+ placeholder?: string;
16
+ };
17
+
18
+ export const NamespaceSelect: FC<NamespaceSelectProps> = ({
19
+ className,
20
+ placeholder = "Select agent",
21
+ }) => {
22
+ const {
23
+ state,
24
+ getAuthorizedNamespaces,
25
+ getCurrentThreadControl,
26
+ onNamespaceSelect,
27
+ isProcessing,
28
+ } = useControl();
29
+ const [open, setOpen] = useState(false);
30
+
31
+ // Fetch authorized namespaces on mount
32
+ useEffect(() => {
33
+ void getAuthorizedNamespaces();
34
+ }, [getAuthorizedNamespaces]);
35
+
36
+ // Get current thread's selected namespace (or fall back to default)
37
+ const threadControl = getCurrentThreadControl();
38
+ const selectedNamespace =
39
+ threadControl.namespace ?? state.defaultNamespace ?? "default";
40
+
41
+ const namespaces = state.authorizedNamespaces;
42
+
43
+ // Show loading state if no namespaces yet
44
+ if (namespaces.length === 0) {
45
+ return (
46
+ <Button
47
+ variant="ghost"
48
+ disabled
49
+ className={cn(
50
+ "h-8 w-auto min-w-[100px] rounded-full px-2 text-xs",
51
+ "text-muted-foreground",
52
+ className,
53
+ )}
54
+ >
55
+ <span className="truncate">{selectedNamespace}</span>
56
+ </Button>
57
+ );
58
+ }
59
+
60
+ return (
61
+ <Popover open={open} onOpenChange={setOpen}>
62
+ <PopoverTrigger asChild>
63
+ <Button
64
+ variant="ghost"
65
+ role="combobox"
66
+ aria-expanded={open}
67
+ disabled={isProcessing}
68
+ className={cn(
69
+ "h-8 w-auto min-w-[100px] justify-between rounded-full px-3 text-xs",
70
+ "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
71
+ isProcessing && "cursor-not-allowed opacity-50",
72
+ className,
73
+ )}
74
+ >
75
+ <span className="truncate">{selectedNamespace ?? placeholder}</span>
76
+ <ChevronDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
77
+ </Button>
78
+ </PopoverTrigger>
79
+ <PopoverContent
80
+ align="center"
81
+ sideOffset={-40}
82
+ className="w-[180px] rounded-3xl p-1 shadow-none"
83
+ >
84
+ <div className="flex flex-col gap-0.5">
85
+ {namespaces.map((ns: string) => (
86
+ <button
87
+ key={ns}
88
+ disabled={isProcessing}
89
+ onClick={() => {
90
+ console.log("[NamespaceSelect] clicked", { ns, isProcessing });
91
+ if (isProcessing) return;
92
+ console.log("[NamespaceSelect] calling onNamespaceSelect", {
93
+ ns,
94
+ });
95
+ onNamespaceSelect(ns);
96
+ setOpen(false);
97
+ console.log("[NamespaceSelect] onNamespaceSelect completed", {
98
+ ns,
99
+ });
100
+ }}
101
+ className={cn(
102
+ "flex w-full items-center justify-between gap-2 rounded-full px-3 py-2 text-sm outline-none",
103
+ "hover:bg-accent hover:text-accent-foreground",
104
+ "focus:bg-accent focus:text-accent-foreground",
105
+ selectedNamespace === ns && "bg-accent",
106
+ isProcessing && "cursor-not-allowed opacity-50",
107
+ )}
108
+ >
109
+ <span>{ns}</span>
110
+ {selectedNamespace === ns && <CheckIcon className="h-4 w-4" />}
111
+ </button>
112
+ ))}
113
+ </div>
114
+ </PopoverContent>
115
+ </Popover>
116
+ );
117
+ };
@@ -0,0 +1,75 @@
1
+ "use client";
2
+
3
+ import { useEffect, type FC } from "react";
4
+ import { useAccount, useConnect, useDisconnect } from "wagmi";
5
+ import { cn, formatAddress, getNetworkName, useUser } from "@aomi-labs/react";
6
+
7
+ export type WalletConnectProps = {
8
+ className?: string;
9
+ connectLabel?: string;
10
+ onConnectionChange?: (connected: boolean) => void;
11
+ };
12
+
13
+ export const WalletConnect: FC<WalletConnectProps> = ({
14
+ className,
15
+ connectLabel = "Connect Wallet",
16
+ onConnectionChange,
17
+ }) => {
18
+ const { address, isConnected, chainId } = useAccount();
19
+ const { connect, connectors } = useConnect();
20
+ const { disconnect } = useDisconnect();
21
+ const { setUser } = useUser();
22
+
23
+ // Sync wallet state to UserContext
24
+ useEffect(() => {
25
+ setUser({
26
+ address: address ?? undefined,
27
+ chainId: chainId ?? undefined,
28
+ isConnected,
29
+ });
30
+ onConnectionChange?.(isConnected);
31
+ }, [address, chainId, isConnected, setUser, onConnectionChange]);
32
+
33
+ const handleClick = () => {
34
+ if (isConnected) {
35
+ disconnect();
36
+ } else {
37
+ const connector = connectors[0];
38
+ if (connector) {
39
+ connect({ connector });
40
+ }
41
+ }
42
+ };
43
+
44
+ const networkName = getNetworkName(chainId);
45
+
46
+ return (
47
+ <button
48
+ type="button"
49
+ onClick={handleClick}
50
+ className={cn(
51
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium",
52
+ "rounded-full px-5 py-2.5",
53
+ "bg-neutral-900 text-white",
54
+ "hover:bg-neutral-800",
55
+ "dark:bg-neutral-900 dark:text-white",
56
+ "dark:hover:bg-neutral-800",
57
+ "transition-colors",
58
+ "focus-visible:ring-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
59
+ "disabled:pointer-events-none disabled:opacity-50",
60
+ className,
61
+ )}
62
+ aria-label={isConnected ? "Disconnect wallet" : "Connect wallet"}
63
+ >
64
+ <span>
65
+ {isConnected && address ? formatAddress(address) : connectLabel}
66
+ </span>
67
+ {isConnected && networkName && (
68
+ <>
69
+ <span className="opacity-50">•</span>
70
+ <span className="opacity-50">{networkName}</span>
71
+ </>
72
+ )}
73
+ </button>
74
+ );
75
+ };
@@ -0,0 +1,204 @@
1
+ "use client";
2
+
3
+ import { useThreadContext } from "@aomi-labs/react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
6
+
7
+ /**
8
+ * Test Component for Thread Context
9
+ *
10
+ * This component demonstrates and tests the ThreadContext functionality.
11
+ * Use this to verify Phase 2 is working correctly.
12
+ *
13
+ * Usage:
14
+ * 1. Wrap your app with <ThreadContextProvider>
15
+ * 2. Add <ThreadContextTest /> somewhere in your component tree
16
+ * 3. Click buttons to test thread operations
17
+ */
18
+ export function ThreadContextTest() {
19
+ const {
20
+ currentThreadId,
21
+ setCurrentThreadId,
22
+ threads,
23
+ threadMetadata,
24
+ setThreadMessages,
25
+ updateThreadMetadata,
26
+ } = useThreadContext();
27
+
28
+ // Test: Create a new thread
29
+ const handleCreateThread = () => {
30
+ const newThreadId = `thread-${Date.now()}`;
31
+
32
+ // Add empty messages for new thread
33
+ setThreadMessages(newThreadId, []);
34
+
35
+ // Add metadata for new thread
36
+ updateThreadMetadata(newThreadId, {
37
+ title: `Thread ${newThreadId.slice(-4)}`,
38
+ status: "regular",
39
+ });
40
+
41
+ // Switch to new thread
42
+ setCurrentThreadId(newThreadId);
43
+
44
+ console.log("✅ Created thread:", newThreadId);
45
+ };
46
+
47
+ // Test: Switch thread
48
+ const handleSwitchThread = (threadId: string) => {
49
+ setCurrentThreadId(threadId);
50
+ console.log("✅ Switched to thread:", threadId);
51
+ };
52
+
53
+ // Test: Archive current thread
54
+ const handleArchiveThread = () => {
55
+ updateThreadMetadata(currentThreadId, { status: "archived" });
56
+ console.log("✅ Archived thread:", currentThreadId);
57
+ };
58
+
59
+ // Test: Unarchive current thread
60
+ const handleUnarchiveThread = () => {
61
+ updateThreadMetadata(currentThreadId, { status: "regular" });
62
+ console.log("✅ Unarchived thread:", currentThreadId);
63
+ };
64
+
65
+ // Get current thread info
66
+ const currentMessages = threads.get(currentThreadId) || [];
67
+ const currentMeta = threadMetadata.get(currentThreadId);
68
+
69
+ // Get all thread IDs
70
+ const allThreadIds = Array.from(threads.keys());
71
+ const regularThreads = allThreadIds.filter(
72
+ (id) => threadMetadata.get(id)?.status === "regular",
73
+ );
74
+ const archivedThreads = allThreadIds.filter(
75
+ (id) => threadMetadata.get(id)?.status === "archived",
76
+ );
77
+
78
+ return (
79
+ <Card className="mx-auto my-8 w-full max-w-2xl">
80
+ <CardHeader>
81
+ <CardTitle>🧪 Thread Context Test</CardTitle>
82
+ </CardHeader>
83
+ <CardContent className="space-y-4">
84
+ {/* Current Thread Info */}
85
+ <div className="bg-muted/50 rounded border p-4">
86
+ <h3 className="mb-2 font-semibold">Current Thread</h3>
87
+ <div className="space-y-1 text-sm">
88
+ <p>
89
+ <strong>ID:</strong> {currentThreadId}
90
+ </p>
91
+ <p>
92
+ <strong>Title:</strong> {currentMeta?.title || "N/A"}
93
+ </p>
94
+ <p>
95
+ <strong>Status:</strong> {currentMeta?.status || "N/A"}
96
+ </p>
97
+ <p>
98
+ <strong>Messages:</strong> {currentMessages.length}
99
+ </p>
100
+ </div>
101
+ </div>
102
+
103
+ {/* Actions */}
104
+ <div className="space-y-2">
105
+ <h3 className="font-semibold">Actions</h3>
106
+ <div className="flex flex-wrap gap-2">
107
+ <Button onClick={handleCreateThread} variant="default">
108
+ Create New Thread
109
+ </Button>
110
+ <Button
111
+ onClick={handleArchiveThread}
112
+ variant="outline"
113
+ disabled={currentMeta?.status === "archived"}
114
+ >
115
+ Archive Current
116
+ </Button>
117
+ <Button
118
+ onClick={handleUnarchiveThread}
119
+ variant="outline"
120
+ disabled={currentMeta?.status === "regular"}
121
+ >
122
+ Unarchive Current
123
+ </Button>
124
+ </div>
125
+ </div>
126
+
127
+ {/* Thread List */}
128
+ <div className="space-y-2">
129
+ <h3 className="font-semibold">
130
+ Regular Threads ({regularThreads.length})
131
+ </h3>
132
+ <div className="space-y-1">
133
+ {regularThreads.map((threadId) => {
134
+ const meta = threadMetadata.get(threadId);
135
+ const isActive = threadId === currentThreadId;
136
+ return (
137
+ <button
138
+ key={threadId}
139
+ onClick={() => handleSwitchThread(threadId)}
140
+ className={`w-full rounded border px-3 py-2 text-left text-sm ${
141
+ isActive
142
+ ? "bg-primary text-primary-foreground border-primary"
143
+ : "bg-background hover:bg-muted border-border"
144
+ }`}
145
+ >
146
+ {meta?.title || threadId}
147
+ {isActive && " (active)"}
148
+ </button>
149
+ );
150
+ })}
151
+ </div>
152
+ </div>
153
+
154
+ {archivedThreads.length > 0 && (
155
+ <div className="space-y-2">
156
+ <h3 className="font-semibold">
157
+ Archived Threads ({archivedThreads.length})
158
+ </h3>
159
+ <div className="space-y-1">
160
+ {archivedThreads.map((threadId) => {
161
+ const meta = threadMetadata.get(threadId);
162
+ const isActive = threadId === currentThreadId;
163
+ return (
164
+ <button
165
+ key={threadId}
166
+ onClick={() => handleSwitchThread(threadId)}
167
+ className={`w-full rounded border px-3 py-2 text-left text-sm opacity-60 ${
168
+ isActive
169
+ ? "bg-primary text-primary-foreground border-primary"
170
+ : "bg-background hover:bg-muted border-border"
171
+ }`}
172
+ >
173
+ {meta?.title || threadId}
174
+ {isActive && " (active)"}
175
+ </button>
176
+ );
177
+ })}
178
+ </div>
179
+ </div>
180
+ )}
181
+
182
+ {/* Debug Info */}
183
+ <details className="rounded border p-2 text-xs">
184
+ <summary className="mb-2 cursor-pointer font-semibold">
185
+ Debug Info
186
+ </summary>
187
+ <pre className="max-h-40 overflow-auto whitespace-pre-wrap">
188
+ {JSON.stringify(
189
+ {
190
+ currentThreadId,
191
+ totalThreads: allThreadIds.length,
192
+ regularCount: regularThreads.length,
193
+ archivedCount: archivedThreads.length,
194
+ threadIds: allThreadIds,
195
+ },
196
+ null,
197
+ 2,
198
+ )}
199
+ </pre>
200
+ </details>
201
+ </CardContent>
202
+ </Card>
203
+ );
204
+ }