-
Notifications
You must be signed in to change notification settings - Fork 963
feat: add shared ai-chat package #1202
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
564a50a
27c8890
a009d09
04d6aad
2a3f01c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| { | ||
| "name": "@superset/ai-chat", | ||
| "version": "0.0.1", | ||
| "description": "Shared AI chat hooks and utilities", | ||
| "type": "module", | ||
| "main": "./src/index.ts", | ||
| "exports": { | ||
| ".": "./src/index.ts", | ||
| "./components": "./src/components/index.ts", | ||
| "./stream": "./src/stream/index.ts" | ||
| }, | ||
| "scripts": { | ||
| "test": "bun test", | ||
| "typecheck": "tsc --noEmit --emitDeclarationOnly false" | ||
| }, | ||
| "dependencies": { | ||
| "@anthropic-ai/claude-agent-sdk": "^0.2.19", | ||
| "@anthropic-ai/sdk": "^0.72.1", | ||
| "@durable-streams/client": "^0.2.0", | ||
| "@durable-streams/state": "^0.2.0", | ||
| "@superset/ui": "workspace:*", | ||
| "@tanstack/db": "0.5.22", | ||
| "@tanstack/react-db": "0.1.66", | ||
| "lucide-react": "^0.563.0", | ||
| "zod": "^4.3.5" | ||
| }, | ||
| "devDependencies": { | ||
| "@superset/typescript": "workspace:*", | ||
| "@types/node": "^24.9.1", | ||
| "@types/react": "~19.1.0", | ||
| "bun-types": "^1.3.1", | ||
| "react": "19.1.0", | ||
| "typescript": "^5.9.3" | ||
| }, | ||
| "peerDependencies": { | ||
| "react": "^18.0.0 || ^19.0.0" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| /** | ||
| * Chat input component with send button | ||
| */ | ||
|
|
||
| import { Button } from "@superset/ui/button"; | ||
| import { Textarea } from "@superset/ui/textarea"; | ||
| import { cn } from "@superset/ui/utils"; | ||
| import { Send } from "lucide-react"; | ||
| import { type KeyboardEvent, useCallback, useRef, useState } from "react"; | ||
|
|
||
| export interface ChatInputProps { | ||
| onSend: (content: string) => void; | ||
| onTypingChange?: (isTyping: boolean) => void; | ||
| disabled?: boolean; | ||
| placeholder?: string; | ||
| className?: string; | ||
| /** Button style: "icon" shows Send icon, "text" shows "Send" label */ | ||
| buttonVariant?: "icon" | "text"; | ||
| /** Auto-resize textarea as content grows (default: true) */ | ||
| autoResize?: boolean; | ||
| /** Controlled value (optional - if provided, component is controlled) */ | ||
| value?: string; | ||
| /** Controlled onChange (optional - required if value is provided) */ | ||
| onChange?: (value: string) => void; | ||
| } | ||
|
|
||
| export function ChatInput({ | ||
| onSend, | ||
| onTypingChange, | ||
| disabled = false, | ||
| placeholder = "Type a message...", | ||
| className, | ||
| buttonVariant = "icon", | ||
| autoResize = true, | ||
| value: controlledValue, | ||
| onChange: controlledOnChange, | ||
| }: ChatInputProps) { | ||
| const [internalValue, setInternalValue] = useState(""); | ||
| const textareaRef = useRef<HTMLTextAreaElement>(null); | ||
| const typingTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); | ||
|
|
||
| // Support both controlled and uncontrolled modes | ||
| const isControlled = controlledValue !== undefined; | ||
| const value = isControlled ? controlledValue : internalValue; | ||
| const setValue = isControlled | ||
| ? (v: string) => controlledOnChange?.(v) | ||
| : setInternalValue; | ||
|
|
||
| const handleSubmit = useCallback(() => { | ||
| const trimmed = value.trim(); | ||
| if (!trimmed || disabled) return; | ||
|
|
||
| onSend(trimmed); | ||
| setValue(""); | ||
| onTypingChange?.(false); | ||
|
|
||
| // Reset textarea height and focus | ||
| if (textareaRef.current) { | ||
| if (autoResize) { | ||
| textareaRef.current.style.height = "auto"; | ||
| } | ||
| textareaRef.current.focus(); | ||
| } | ||
| }, [value, disabled, onSend, onTypingChange, autoResize, setValue]); | ||
|
|
||
| const handleKeyDown = useCallback( | ||
| (e: KeyboardEvent<HTMLTextAreaElement>) => { | ||
| if (e.key === "Enter" && !e.shiftKey) { | ||
| e.preventDefault(); | ||
| handleSubmit(); | ||
| } | ||
| }, | ||
| [handleSubmit], | ||
| ); | ||
|
|
||
| const handleChange = useCallback( | ||
| (e: React.ChangeEvent<HTMLTextAreaElement>) => { | ||
| const newValue = e.target.value; | ||
| setValue(newValue); | ||
|
|
||
| // Auto-resize textarea | ||
| if (autoResize) { | ||
| const textarea = e.target; | ||
| textarea.style.height = "auto"; | ||
| textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`; | ||
| } | ||
|
|
||
| // Typing indicator with debounce | ||
| if (typingTimeoutRef.current) { | ||
| clearTimeout(typingTimeoutRef.current); | ||
| } | ||
|
|
||
| if (newValue.trim()) { | ||
| onTypingChange?.(true); | ||
| typingTimeoutRef.current = setTimeout(() => { | ||
| onTypingChange?.(false); | ||
| }, 2000); | ||
| } else { | ||
| onTypingChange?.(false); | ||
| } | ||
|
Comment on lines
+88
to
+100
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Memory leak: Clear typing timeout on unmount. The Proposed fix: Add cleanup effect+import { type KeyboardEvent, useCallback, useEffect, useRef, useState } from "react";
-import { type KeyboardEvent, useCallback, useRef, useState } from "react";Add a cleanup effect after the refs: const textareaRef = useRef<HTMLTextAreaElement>(null);
const typingTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+ // Cleanup timeout on unmount
+ useEffect(() => {
+ return () => {
+ if (typingTimeoutRef.current) {
+ clearTimeout(typingTimeoutRef.current);
+ }
+ };
+ }, []);
+
// Support both controlled and uncontrolled modes🤖 Prompt for AI Agents |
||
| }, | ||
| [onTypingChange, autoResize, setValue], | ||
| ); | ||
|
|
||
| return ( | ||
| <div className={cn("flex gap-2 items-end", className)}> | ||
| <Textarea | ||
| ref={textareaRef} | ||
| value={value} | ||
| onChange={handleChange} | ||
| onKeyDown={handleKeyDown} | ||
| placeholder={placeholder} | ||
| disabled={disabled} | ||
| rows={1} | ||
| className="min-h-[40px] max-h-[200px] resize-none" | ||
| /> | ||
| {buttonVariant === "icon" ? ( | ||
| <Button | ||
| size="icon" | ||
| onClick={handleSubmit} | ||
| disabled={disabled || !value.trim()} | ||
| className="shrink-0" | ||
| > | ||
| <Send className="h-4 w-4" /> | ||
| </Button> | ||
| ) : ( | ||
| <Button | ||
| onClick={handleSubmit} | ||
| disabled={disabled || !value.trim()} | ||
| size="default" | ||
| > | ||
| Send | ||
| </Button> | ||
| )} | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { ChatInput, type ChatInputProps } from "./ChatInput"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| /** | ||
| * Presence bar showing viewers and typing indicators | ||
| */ | ||
|
|
||
| import { Avatar, AvatarFallback, AvatarImage } from "@superset/ui/avatar"; | ||
| import { cn } from "@superset/ui/utils"; | ||
| import type { PresenceUser } from "../../types"; | ||
|
|
||
| export interface PresenceBarProps { | ||
| viewers: PresenceUser[]; | ||
| typingUsers: PresenceUser[]; | ||
| className?: string; | ||
| } | ||
|
|
||
| function getInitials(name: string): string { | ||
| return name | ||
| .split(" ") | ||
| .map((n) => n[0]) | ||
| .join("") | ||
| .toUpperCase() | ||
| .slice(0, 2); | ||
| } | ||
|
|
||
| export function PresenceBar({ | ||
| viewers, | ||
| typingUsers, | ||
| className, | ||
| }: PresenceBarProps) { | ||
| if (viewers.length === 0 && typingUsers.length === 0) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <div | ||
| className={cn( | ||
| "flex items-center gap-2 px-4 py-2 border-b border-border bg-muted/50", | ||
| className, | ||
| )} | ||
| > | ||
| {/* Viewers */} | ||
| {viewers.length > 0 && ( | ||
| <div className="flex items-center gap-1"> | ||
| <div className="flex -space-x-2"> | ||
| {viewers.slice(0, 5).map((user) => ( | ||
| <Avatar | ||
| key={user.userId} | ||
| className="h-6 w-6 border-2 border-background" | ||
| > | ||
| {user.image && <AvatarImage src={user.image} />} | ||
| <AvatarFallback className="text-xs"> | ||
| {getInitials(user.name)} | ||
| </AvatarFallback> | ||
| </Avatar> | ||
| ))} | ||
| </div> | ||
| {viewers.length > 5 && ( | ||
| <span className="text-xs text-muted-foreground"> | ||
| +{viewers.length - 5} | ||
| </span> | ||
| )} | ||
| <span className="text-xs text-muted-foreground ml-1">viewing</span> | ||
| </div> | ||
| )} | ||
|
|
||
| {/* Typing indicator */} | ||
| {typingUsers.length > 0 && ( | ||
| <div className="flex items-center gap-1 ml-auto"> | ||
| <span className="text-xs text-muted-foreground"> | ||
| {typingUsers.length === 1 | ||
| ? `${typingUsers[0]?.name} is typing` | ||
| : `${typingUsers.length} people typing`} | ||
| </span> | ||
| <span className="flex gap-0.5"> | ||
| <span className="h-1.5 w-1.5 rounded-full bg-primary animate-bounce [animation-delay:-0.3s]" /> | ||
| <span className="h-1.5 w-1.5 rounded-full bg-primary animate-bounce [animation-delay:-0.15s]" /> | ||
| <span className="h-1.5 w-1.5 rounded-full bg-primary animate-bounce" /> | ||
| </span> | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { PresenceBar, type PresenceBarProps } from "./PresenceBar"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export { ChatInput, type ChatInputProps } from "./ChatInput"; | ||
| export { PresenceBar, type PresenceBarProps } from "./PresenceBar"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export * from "./components"; | ||
| export * from "./stream"; | ||
| export * from "./types"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 109
🌐 Web query:
@anthropic-ai/sdk 0.72.1 release notes breaking changes changelog💡 Result:
For
@anthropic-ai/sdkv0.72.1 (published ~late Jan 2026 per UNPKG’s “latest” tag), the canonical release notes are in the package’sCHANGELOG.mdincluded in the npm tarball and browsable on UNPKG. (app.unpkg.com)CHANGELOG.md(in the v0.72.1 package listing). (app.unpkg.com)CHANGELOG.mdin this environment (the “raw” fetch endpoint errors), so I can’t confirm whether v0.72.1 itself lists any breaking changes. (app.unpkg.com)If you paste the
## 0.72.1section fromCHANGELOG.mdhere, I’ll summarize it and call out any breaking changes.Citations:
🌐 Web query:
@anthropic-ai/sdk 0.72.1 security vulnerabilities advisories💡 Result:
As of Feb 4, 2026, there are no published security advisories for the GitHub repo that backs
@anthropic-ai/sdk(Anthropic’s TypeScript SDK), and no known direct vulnerabilities have been reported for@anthropic-ai/sdk@0.72.1in major package vulnerability trackers:anthropics/anthropic-sdk-typescript): “There aren’t any published security advisories.” [1]@anthropic-ai/sdk): “No direct vulnerabilities have been found” and the “latest non vulnerable version” is 0.72.1. [2]@anthropic-ai/sdk(latest). [3]If you need “actionable” assurance, run a dependency scan (e.g.,
npm audit, Snyk/Dependabot) for transitive issues, since “no direct vulnerabilities” does not cover vulnerable dependencies. [2]🏁 Script executed:
Repository: superset-sh/superset
Length of output: 9095
Test structured output format migration from
output_formattooutput_config.Version 0.72.0 includes a breaking change: the Messages API migrated from using
output_formattooutput_configfor structured outputs. If your code uses structured outputs or references the OutputFormat export, this upgrade will require code changes. Review the commit and test your implementation to ensure compatibility.🤖 Prompt for AI Agents