Skip to content
Merged
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
13 changes: 9 additions & 4 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
},
},
"packages/chat-widget": {
"name": "@pql/chat-widget",
"version": "0.1.0",
"name": "@hasura/chat-widget",
"version": "0.1.4",
"dependencies": {
"react-hot-toast": "^2.4.1",
"react-markdown": "^10.1.0",
"react-syntax-highlighter": "^15.5.0",
"remark-gfm": "^4.0.1",
Expand Down Expand Up @@ -44,12 +45,12 @@
"packages": {
"@babel/runtime": ["@babel/runtime@7.28.2", "", {}, "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA=="],

"@hasura/chat-widget": ["@hasura/chat-widget@workspace:packages/chat-widget"],

"@hasura/promptql": ["@hasura/promptql@0.4.0", "", { "dependencies": { "@opentelemetry/api": "*" } }, "sha512-nKSrmAuFlNm4Gpj8zqVUjwuKAy0gSi13g0poQ7DoczC99zV9vx2BUIXMySDmZfpNFbECi9r4Q9qRTsaxqouC3A=="],

"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],

"@pql/chat-widget": ["@pql/chat-widget@workspace:packages/chat-widget"],

"@sinclair/typebox": ["@sinclair/typebox@0.34.38", "", {}, "sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA=="],

"@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="],
Expand Down Expand Up @@ -134,6 +135,8 @@

"format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="],

"goober": ["goober@2.1.16", "", { "peerDependencies": { "csstype": "^3.0.10" } }, "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g=="],

"hast-util-parse-selector": ["hast-util-parse-selector@2.2.5", "", {}, "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ=="],

"hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
Expand Down Expand Up @@ -270,6 +273,8 @@

"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],

"react-hot-toast": ["react-hot-toast@2.5.2", "", { "dependencies": { "csstype": "^3.1.3", "goober": "^2.1.16" }, "peerDependencies": { "react": ">=16", "react-dom": ">=16" } }, "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw=="],

"react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="],

"react-syntax-highlighter": ["react-syntax-highlighter@15.6.1", "", { "dependencies": { "@babel/runtime": "^7.3.1", "highlight.js": "^10.4.1", "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.27.0", "refractor": "^3.6.0" }, "peerDependencies": { "react": ">= 0.14.0" } }, "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg=="],
Expand Down
3 changes: 2 additions & 1 deletion packages/chat-widget/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"dependencies": {
"react-markdown": "^10.1.0",
"react-syntax-highlighter": "^15.5.0",
"remark-gfm": "^4.0.1"
"remark-gfm": "^4.0.1",
"react-hot-toast": "^2.4.1"
}
}
7 changes: 5 additions & 2 deletions packages/chat-widget/src/components/ChatContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from "react";
import toast from "react-hot-toast";
import { ChatMessages } from "./ChatMessages";
import { ChatInput } from "./ChatInput";
import { Conversation, Message } from "../types";
Expand All @@ -17,6 +18,7 @@ interface ChatContainerProps {
updateLastMessage: (content: string, streaming?: boolean) => void;
clearConversation: () => void;
theme?: "light" | "dark" | "auto";
brandColor?: string;
}

export function ChatContainer({
Expand All @@ -30,6 +32,7 @@ export function ChatContainer({
updateLastMessage,
clearConversation,
theme,
brandColor,
}: ChatContainerProps) {
const handleMessage = (message: any) => {
console.log("handleMessage called with:", message);
Expand All @@ -50,7 +53,7 @@ export function ChatContainer({

const handleError = (error: string) => {
console.error("Chat error:", error);
// Could show error toast here
toast.error(`Chat error: ${error}`);
};

const { sendMessage } = useStreamingChat({
Expand Down Expand Up @@ -87,7 +90,7 @@ export function ChatContainer({

return (
<div className={styles.container}>
<ChatMessages messages={conversation.messages} isLoading={isLoading} theme={theme} />
<ChatMessages messages={conversation.messages} isLoading={isLoading} theme={theme} brandColor={brandColor} />
<ChatInput placeholder={placeholder} onSendMessage={handleSendMessage} disabled={isLoading} />
</div>
);
Expand Down
12 changes: 6 additions & 6 deletions packages/chat-widget/src/components/ChatMessage.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,18 @@
color: var(--pql-text);
}

.text[data-loading="true"] {
font-size: 18px;
animation: loadingPulse 1.4s infinite ease-in-out;
.text[data-streaming="true"] {
border: 2px solid var(--brand-color, #b6fc34);
animation: brandGlow 2s infinite ease-in-out;
}

@keyframes loadingPulse {
@keyframes brandGlow {
0%,
100% {
opacity: 0.4;
box-shadow: 0 0 5px var(--brand-color, #b6fc34);
}
50% {
opacity: 1;
box-shadow: 0 0 20px var(--brand-color, #b6fc34), 0 0 30px var(--brand-color, #b6fc34);
}
}

Expand Down
10 changes: 8 additions & 2 deletions packages/chat-widget/src/components/ChatMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import styles from "./ChatMessage.module.css";
interface ChatMessageProps {
message: Message;
theme?: "light" | "dark" | "auto";
brandColor?: string;
}

export function ChatMessage({ message, theme = "auto" }: ChatMessageProps) {
export function ChatMessage({ message, theme = "auto", brandColor = "#2563eb" }: ChatMessageProps) {
const isUser = message.role === "user";
const isStreaming = message.streaming && !isUser;

// Add this debug log
console.log("ChatMessage theme:", theme);
Expand All @@ -32,7 +34,11 @@ export function ChatMessage({ message, theme = "auto" }: ChatMessageProps) {
<div className={`${styles.message} ${styles[message.role]}`}>
<div className={styles.avatar}>{isUser ? "U" : "A"}</div>
<div className={styles.content}>
<div className={styles.text} data-loading={message.content === "..." ? "true" : undefined}>
<div
className={styles.text}
data-loading={message.content === "..." ? "true" : undefined}
data-streaming={isStreaming ? "true" : undefined}
style={isStreaming ? ({ "--brand-color": brandColor } as React.CSSProperties) : undefined}>
{isUser ? (
<>
{message.content}
Expand Down
5 changes: 3 additions & 2 deletions packages/chat-widget/src/components/ChatMessages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ interface ChatMessagesProps {
messages: Message[];
isLoading: boolean;
theme?: "light" | "dark" | "auto";
brandColor?: string;
}

export function ChatMessages({ messages, isLoading, theme }: ChatMessagesProps) {
export function ChatMessages({ messages, isLoading, theme, brandColor }: ChatMessagesProps) {
const messagesEndRef = useRef<HTMLDivElement>(null);

useEffect(() => {
Expand All @@ -30,7 +31,7 @@ export function ChatMessages({ messages, isLoading, theme }: ChatMessagesProps)
<div className={styles.container}>
<div className={styles.messages}>
{messages.map((message) => (
<ChatMessage key={message.id} message={message} theme={theme} />
<ChatMessage key={message.id} message={message} theme={theme} brandColor={brandColor} />
))}
<div ref={messagesEndRef} />
</div>
Expand Down
14 changes: 7 additions & 7 deletions packages/chat-widget/src/components/ChatWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState } from "react";
import { Toaster } from "react-hot-toast";
import { ChatContainer } from "./ChatContainer";
import { ChatWidgetProps } from "../types";
import { useConversation } from "../hooks/useConversation";
Expand All @@ -10,13 +11,15 @@ export function ChatWidget({
className,
placeholder = "Ask a question...",
title = "Documentation Chat",
brandColor = "#b6fc34",
}: ChatWidgetProps) {
const [isOpen, setIsOpen] = useState(false);
const conversationState = useConversation();
const { conversation, isLoading, setIsLoading, addMessage, updateLastMessage, clearConversation } = conversationState;

return (
<>
<Toaster position="top-right" />
{/* Floating Button */}
<button
className={`${styles.floatingButton} ${styles[theme]}`}
Expand All @@ -33,7 +36,8 @@ export function ChatWidget({
<div
className={`${styles.modal} ${styles[theme]} ${className || ""}`}
onClick={(e) => e.stopPropagation()}
data-theme={theme}>
data-theme={theme}
style={{ "--brand-color": brandColor } as React.CSSProperties}>
<div className={styles.modalHeader}>
<h3>{title}</h3>
<div className={styles.headerButtons}>
Expand All @@ -56,13 +60,9 @@ export function ChatWidget({
serverUrl={serverUrl}
placeholder={placeholder}
title={title}
conversation={conversation}
isLoading={isLoading}
setIsLoading={setIsLoading}
addMessage={addMessage}
updateLastMessage={updateLastMessage}
clearConversation={clearConversation}
theme={theme}
brandColor={brandColor}
{...conversationState}
/>
</div>
</div>
Expand Down
16 changes: 16 additions & 0 deletions packages/chat-widget/src/hooks/useStreamingChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ export function useStreamingChat({
async (content: string, history: Array<{ role: string; content: string }>) => {
setIsLoading(true);

// Set a timeout to reset loading state if no response
const timeoutId = setTimeout(() => {
setIsLoading(false);
onError("Request timed out");
}, 120000); // 2 minutes

try {
const response = await fetch(`${serverUrl}/chat/conversations/${conversationId}/messages`, {
method: "POST",
Expand All @@ -31,6 +37,8 @@ export function useStreamingChat({
}),
});

clearTimeout(timeoutId);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
Expand Down Expand Up @@ -84,7 +92,15 @@ export function useStreamingChat({
streaming: false,
});
} catch (error) {
clearTimeout(timeoutId);
onError(error instanceof Error ? error.message : "Unknown error");

// Add a final empty assistant message to clear the streaming state
onMessage({
role: "assistant",
content: "",
streaming: false,
});
} finally {
setIsLoading(false);
}
Expand Down
1 change: 1 addition & 0 deletions packages/chat-widget/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ export interface ChatWidgetProps {
className?: string;
placeholder?: string;
title?: string;
brandColor?: string;
}
1 change: 1 addition & 0 deletions packages/chat-widget/test-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ function App() {
theme={theme}
title="PromptQL Docs Chat"
placeholder="Ask about PromptQL..."
brandColor="#b6fc34"
/>
</div>
</main>
Expand Down
2 changes: 1 addition & 1 deletion pql/app/metadata/pg.hml
Original file line number Diff line number Diff line change
Expand Up @@ -1298,7 +1298,7 @@ definition:
type:
type: nullable
underlying_type:
name: boolean
name: Float
type: named
id:
arguments: {}
Expand Down