@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.
- package/dist/accordion.json +18 -0
- package/dist/alert.json +17 -0
- package/dist/aomi-frame.json +24 -0
- package/dist/assistant-thread-list.json +22 -0
- package/dist/assistant-thread.json +27 -0
- package/dist/assistant-threadlist-sidebar.json +20 -0
- package/dist/assistant-tool-fallback.json +20 -0
- package/dist/avatar.json +17 -0
- package/dist/badge.json +17 -0
- package/dist/breadcrumb.json +17 -0
- package/dist/button.json +18 -0
- package/dist/card.json +15 -0
- package/dist/collapsible.json +17 -0
- package/dist/command.json +21 -0
- package/dist/dialog.json +18 -0
- package/dist/drawer.json +17 -0
- package/dist/input.json +15 -0
- package/dist/label.json +15 -0
- package/dist/notification.json +20 -0
- package/dist/popover.json +17 -0
- package/dist/registry.json +429 -0
- package/dist/separator.json +17 -0
- package/dist/sheet.json +18 -0
- package/dist/sidebar.json +18 -0
- package/dist/skeleton.json +15 -0
- package/dist/sonner.json +17 -0
- package/dist/tooltip.json +17 -0
- package/package.json +27 -85
- package/src/components/aomi-frame.tsx +221 -0
- package/src/components/assistant-ui/attachment.tsx +235 -0
- package/src/components/assistant-ui/markdown-text.tsx +228 -0
- package/src/components/assistant-ui/thread-list.tsx +106 -0
- package/src/components/assistant-ui/thread.tsx +476 -0
- package/src/components/assistant-ui/threadlist-sidebar.tsx +66 -0
- package/src/components/assistant-ui/tool-fallback.tsx +48 -0
- package/src/components/assistant-ui/tooltip-icon-button.tsx +42 -0
- package/src/components/control-bar/api-key-input.tsx +122 -0
- package/src/components/control-bar/index.tsx +58 -0
- package/src/components/control-bar/model-select.tsx +120 -0
- package/src/components/control-bar/namespace-select.tsx +117 -0
- package/src/components/control-bar/wallet-connect.tsx +75 -0
- package/src/components/test/ThreadContextTest.tsx +204 -0
- package/src/components/tools/example-tool/ExampleTool.tsx +102 -0
- package/src/components/ui/accordion.tsx +58 -0
- package/src/components/ui/alert.tsx +62 -0
- package/src/components/ui/avatar.tsx +53 -0
- package/src/components/ui/badge.tsx +37 -0
- package/src/components/ui/breadcrumb.tsx +109 -0
- package/src/components/ui/button.tsx +59 -0
- package/src/components/ui/card.tsx +86 -0
- package/src/components/ui/collapsible.tsx +12 -0
- package/src/components/ui/command.tsx +156 -0
- package/src/components/ui/dialog.tsx +143 -0
- package/src/components/ui/drawer.tsx +118 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +20 -0
- package/src/components/ui/notification.tsx +57 -0
- package/src/components/ui/popover.tsx +33 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +139 -0
- package/src/components/ui/sidebar.tsx +827 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/sonner.tsx +29 -0
- package/src/components/ui/tooltip.tsx +61 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/index.ts +26 -0
- package/src/registry.ts +218 -0
- package/{dist/styles.css → src/themes/default.css} +21 -3
- package/src/themes/tokens.config.ts +39 -0
- package/README.md +0 -41
- package/dist/index.cjs +0 -3780
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -302
- package/dist/index.d.ts +0 -302
- package/dist/index.js +0 -3696
- 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
|
+
}
|