48 lines
1.5 KiB
TypeScript
48 lines
1.5 KiB
TypeScript
import { useSyncExternalStore } from "react"
|
|
|
|
/** What the panel is talking to: the assistant, or a wren node in a flow. */
|
|
export type ChatTab = "build" | "control"
|
|
|
|
/**
|
|
* Whether the assistant is open, on which tab, and whether another panel is
|
|
* open beside it.
|
|
*
|
|
* Module state rather than context because the chat is mounted once in the
|
|
* sidebar and toggled from there, while the editor's own panels report in
|
|
* from a different tree: the two only have to agree on who takes which half
|
|
* of the lane.
|
|
*/
|
|
type State = { open: boolean; tab: ChatTab; panels: number }
|
|
|
|
let state: State = { open: false, tab: "build", panels: 0 }
|
|
const listeners = new Set<() => void>()
|
|
|
|
function set(next: Partial<State>) {
|
|
state = { ...state, ...next }
|
|
for (const listener of listeners) listener()
|
|
}
|
|
|
|
export const chatStore = {
|
|
/** Open it, on a given tab or on whichever was last shown. */
|
|
open: (tab?: ChatTab) => set(tab ? { open: true, tab } : { open: true }),
|
|
show: (tab: ChatTab) => set({ tab }),
|
|
close: () => set({ open: false }),
|
|
toggle: () => set({ open: !state.open }),
|
|
/** A floating panel is open; returns how to say it closed again. */
|
|
registerPanel: () => {
|
|
set({ panels: state.panels + 1 })
|
|
return () => set({ panels: state.panels - 1 })
|
|
},
|
|
}
|
|
|
|
export function useChat(): State {
|
|
return useSyncExternalStore(
|
|
(listener) => {
|
|
listeners.add(listener)
|
|
return () => listeners.delete(listener)
|
|
},
|
|
() => state,
|
|
() => state,
|
|
)
|
|
}
|