Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/portal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@
"@next/mdx": "15.3.2",
"@radix-ui/react-dialog": "1.1.10",
"@radix-ui/react-dropdown-menu": "^2.1.11",
"@radix-ui/react-popover": "^1.1.10",
"@radix-ui/react-select": "^2.2.2",
"@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-tabs": "^1.1.8",
"@radix-ui/react-tooltip": "1.2.3",
"@tanstack/react-query": "5.74.4",
"@tryghost/content-api": "^1.11.22",
"class-variance-authority": "^0.7.1",
Expand Down
10 changes: 10 additions & 0 deletions apps/portal/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ html {
--success-text: 142.09 70.56% 35.29%;
--warning-text: 38 92% 40%;
--destructive-text: 357.15deg 100% 68.72%;
--nebula-pink-foreground: 321 90% 51%;

/* Borders */
--border: 0 0% 85%;
Expand Down Expand Up @@ -83,6 +84,7 @@ html {
--destructive-text: 360 72% 55%;
--success-text: 142 75% 50%;
--inverted-foreground: 0 0% 0%;
--nebula-pink-foreground: 321 90% 51%;

/* Borders */
--border: 0 0% 15%;
Expand Down Expand Up @@ -159,3 +161,11 @@ button {
.hide-scrollbar::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}

.no-scrollbar {
scrollbar-width: none; /* Firefox */
}

.no-scrollbar::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}
13 changes: 13 additions & 0 deletions apps/portal/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { Fira_Code, Inter } from "next/font/google";
import Script from "next/script";
import NextTopLoader from "nextjs-toploader";
import { StickyTopContainer } from "../components/Document/StickyTopContainer";
import { CustomChatButton } from "../components/SiwaChat/CustomChatButton";
import { examplePrompts } from "../components/SiwaChat/examplePrompts";
import { Banner } from "../components/others/Banner";
import { EnableSmoothScroll } from "../components/others/SmoothScroll";
import { PHProvider } from "../lib/posthog/Posthog";
Expand Down Expand Up @@ -67,6 +69,17 @@ export default function RootLayout({
/>
<EnableSmoothScroll />

{/* Siwa AI Chat Widget Floating Button */}
<CustomChatButton
label="Ask AI"
networks={null}
pageType="support"
customApiParams={{}}
examplePrompts={examplePrompts}
authToken={undefined}
requireLogin={false}
/>

<div className="relative flex min-h-screen flex-col">
<StickyTopContainer>
{/* Note: Please change id as well when changing text or href so that new banner is shown to user even if user dismissed the older one */}
Expand Down
212 changes: 212 additions & 0 deletions apps/portal/src/components/SiwaChat/ChatBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
"use client";

import { ArrowUpIcon, CircleStopIcon } from "lucide-react";
import { useState } from "react";
import { cn } from "../../lib/utils";
// NOTE: You will need to update these imports to match your portal's structure or stub them if not available
import { Button } from "../ui/button";
import { AutoResizeTextarea } from "../ui/textarea";
import type { NebulaUserMessage } from "./types";

// Define proper TypeScript interfaces
interface ChatContext {
chainId?: number;
contractAddress?: string;
functionName?: string;
parameters?: Record<string, unknown>;
[key: string]: unknown;
}

interface ConnectedWallet {
address: string;
walletId: string;
chainId?: number;
isActive?: boolean;
}

// Props interfaces for better organization
interface MessageHandlingProps {
sendMessage: (message: NebulaUserMessage) => void;
prefillMessage?: string;
placeholder: string;
}

interface ChatStateProps {
isChatStreaming: boolean;
abortChatStream: () => void;
isConnectingWallet: boolean;
}

interface WalletProps {
connectedWallets: ConnectedWallet[];
setActiveWallet: (wallet: ConnectedWallet) => void;
}

interface ContextProps {
context: ChatContext | undefined;
setContext: (context: ChatContext | undefined) => void;
showContextSelector: boolean;
}

interface UIProps {
className?: string;
onLoginClick?: () => void;
}

// Combined props interface
interface ChatBarProps
extends MessageHandlingProps,
ChatStateProps,
Partial<WalletProps>,
Partial<ContextProps>,
UIProps {}

export function ChatBar(props: ChatBarProps) {
const [message, setMessage] = useState(props.prefillMessage || "");

const handleSendMessage = () => {
if (message.trim() === "") return;

const userMessage: NebulaUserMessage = {
role: "user",
content: [{ type: "text", text: message }],
};

props.sendMessage(userMessage);
setMessage("");
};

return (
<div
className={cn(
"overflow-hidden rounded-3xl border border-border bg-card",
props.className,
)}
>
<div className="p-2">
<MessageInput
message={message}
setMessage={setMessage}
placeholder={props.placeholder}
isChatStreaming={props.isChatStreaming}
onSend={handleSendMessage}
/>
<ChatActions
isChatStreaming={props.isChatStreaming}
isConnectingWallet={props.isConnectingWallet}
abortChatStream={props.abortChatStream}
onSendMessage={handleSendMessage}
canSend={message.trim() !== ""}
/>
</div>
</div>
);
}

// Updated MessageInput component
interface MessageInputProps {
message: string;
setMessage: (message: string) => void;
placeholder: string;
isChatStreaming: boolean;
onSend: () => void;
}

function MessageInput({
message,
setMessage,
placeholder,
isChatStreaming,
onSend,
}: MessageInputProps) {
return (
<div className="max-h-[200px] overflow-y-auto">
<AutoResizeTextarea
placeholder={placeholder}
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={(e) => {
if (e.shiftKey) return;
if (e.key === "Enter" && !isChatStreaming) {
e.preventDefault();
onSend();
}
}}
className="min-h-[60px] resize-none border-none bg-transparent pt-2 leading-relaxed focus-visible:ring-0 focus-visible:ring-offset-0"
disabled={isChatStreaming}
/>
</div>
);
}

// Updated ChatActions component
interface ChatActionsProps {
isChatStreaming: boolean;
isConnectingWallet: boolean;
abortChatStream: () => void;
onSendMessage: () => void;
canSend: boolean;
}

function ChatActions({
isChatStreaming,
isConnectingWallet,
abortChatStream,
onSendMessage,
canSend,
}: ChatActionsProps) {
return (
<div className="flex items-end justify-between gap-3 px-2 pb-2">
<div className="grow" />
<div className="flex items-center gap-2">
{isChatStreaming ? (
<StopButton onStop={abortChatStream} />
) : (
<SendButton
onSend={onSendMessage}
disabled={!canSend || isConnectingWallet}
/>
)}
</div>
</div>
);
}

// Decomposed component for stop button
interface StopButtonProps {
onStop: () => void;
}

function StopButton({ onStop }: StopButtonProps) {
return (
<Button
variant="default"
className="!h-auto w-auto shrink-0 gap-2 p-2"
onClick={onStop}
aria-label="Stop generating"
>
<CircleStopIcon className="size-4" />
Stop
</Button>
);
}

// Decomposed component for send button
interface SendButtonProps {
onSend: () => void;
disabled: boolean;
}

function SendButton({ onSend, disabled }: SendButtonProps) {
return (
<Button
aria-label="Send"
disabled={disabled}
className="!h-auto w-auto border border-nebula-pink-foreground p-2 disabled:opacity-100"
variant="pink"
onClick={onSend}
>
<ArrowUpIcon className="size-4" />
</Button>
);
}
Loading
Loading