diff --git a/apps/desktop/package.json b/apps/desktop/package.json index d564488f91a..e787691e885 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -87,6 +87,7 @@ "@sentry/electron": "7.16.0", "@streamdown/mermaid": "1.0.2", "@superset/auth": "workspace:*", + "@superset/chat": "workspace:*", "@superset/chat-legacy": "workspace:*", "@superset/db": "workspace:*", "@superset/host-service": "workspace:*", diff --git a/apps/desktop/src/renderer/lib/persisted-keys/persisted-key-registry.test-data.ts b/apps/desktop/src/renderer/lib/persisted-keys/persisted-key-registry.test-data.ts index 38ce55359df..7f40ecff6f9 100644 --- a/apps/desktop/src/renderer/lib/persisted-keys/persisted-key-registry.test-data.ts +++ b/apps/desktop/src/renderer/lib/persisted-keys/persisted-key-registry.test-data.ts @@ -51,6 +51,10 @@ export const PERSISTED_KEY_REGISTRY: ReadonlyArray< "src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/state/paneScrollStateCache/paneScrollStateCache.ts", ["v2-pane-scroll-state-v1"], ], + [ + "src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatV3Pane/components/Composer/Composer.tsx", + ["chat-v3-draft:*"], + ], ["src/renderer/stores/changes/store.ts", ["changes-store"]], ["src/renderer/stores/prompt-history.ts", ["prompt-history"]], ["src/renderer/stores/tabs/store.ts", ["tabs-storage"]], diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatV3Pane/ChatV3Pane.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatV3Pane/ChatV3Pane.tsx new file mode 100644 index 00000000000..7a289ec9a0c --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatV3Pane/ChatV3Pane.tsx @@ -0,0 +1,69 @@ +import type { UserContent } from "@superset/chat/protocol"; +import { useCallback, useState } from "react"; +import type { HarnessId } from "./components/NewSessionView"; +import { NewSessionView } from "./components/NewSessionView"; +import { SessionPicker } from "./components/SessionPicker"; +import { SessionView } from "./components/SessionView"; +import { useSessionClient } from "./hooks/useSessionClient"; + +export function ChatV3Pane({ + onSessionIdChange, + sessionId, + workspaceId, +}: { + workspaceId: string; + sessionId: string | null; + onSessionIdChange: (sessionId: string | null) => void; +}) { + const { client, wiring } = useSessionClient(sessionId); + const [harness, setHarness] = useState("claude-code"); + const [pendingFirstPrompt, setPendingFirstPrompt] = useState< + UserContent[] | null + >(null); + + const createSession = useCallback( + async (content: UserContent[] | null) => { + const created = await wiring.transport.createSession({ + commandId: crypto.randomUUID(), + workspaceId, + harness, + }); + setPendingFirstPrompt(content); + onSessionIdChange(created.sessionId); + }, + [wiring.transport, workspaceId, harness, onSessionIdChange], + ); + + const picker = ( + onSessionIdChange(null)} + onSelect={onSessionIdChange} + transport={wiring.transport} + workspaceId={workspaceId} + /> + ); + + if (!client || !sessionId) { + return ( + void createSession(content)} + workspaceId={workspaceId} + /> + ); + } + + return ( + setPendingFirstPrompt(null)} + pendingFirstPrompt={pendingFirstPrompt} + sessionId={sessionId} + /> + ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatV3Pane/components/Composer/Composer.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatV3Pane/components/Composer/Composer.tsx new file mode 100644 index 00000000000..e06854b6b7f --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatV3Pane/components/Composer/Composer.tsx @@ -0,0 +1,109 @@ +import type { OutboxEntry } from "@superset/chat/core"; +import type { UserContent } from "@superset/chat/protocol"; +import { Button } from "@superset/ui/button"; +import { Textarea } from "@superset/ui/textarea"; +import { ArrowUp, Square } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; + +const DRAFT_DEBOUNCE_MS = 300; + +export type ComposerProps = { + draftKey: string; + outbox: OutboxEntry[]; + onSend: (content: UserContent[]) => OutboxEntry | null; + placeholder?: string; + disabled?: boolean; + onCancelTurn?: (() => void) | null; +}; + +export function Composer({ + disabled, + draftKey, + onCancelTurn, + onSend, + outbox, + placeholder, +}: ComposerProps) { + const [value, setValue] = useState( + () => window.localStorage.getItem(draftKey) ?? "", + ); + const valueRef = useRef(value); + valueRef.current = value; + const pendingRef = useRef<{ clientId: string; sentText: string } | null>( + null, + ); + + useEffect(() => { + const timer = setTimeout(() => { + if (value === "") window.localStorage.removeItem(draftKey); + else window.localStorage.setItem(draftKey, value); + }, DRAFT_DEBOUNCE_MS); + return () => clearTimeout(timer); + }, [value, draftKey]); + + useEffect(() => { + return () => { + const latest = valueRef.current; + if (latest === "") window.localStorage.removeItem(draftKey); + else window.localStorage.setItem(draftKey, latest); + }; + }, [draftKey]); + + useEffect(() => { + const pending = pendingRef.current; + if (!pending) return; + if (outbox.some((entry) => entry.clientId === pending.clientId)) return; + pendingRef.current = null; + if (valueRef.current === pending.sentText) { + setValue(""); + window.localStorage.removeItem(draftKey); + } + }, [outbox, draftKey]); + + const send = () => { + const text = valueRef.current; + if (text.trim() === "" || disabled) return; + const entry = onSend([{ type: "text", text }]); + if (entry === null) { + setValue(""); + window.localStorage.removeItem(draftKey); + return; + } + pendingRef.current = { clientId: entry.clientId, sentText: text }; + }; + + return ( +
+