| 1 | import { useAtomValue } from "@effect/atom-react"; |
| 2 | import { usesChatGptSharing } from "@t3tools/shared/usageLimits"; |
| 3 | import { useState } from "react"; |
| 4 | import { environmentPresentations } from "../../state/presentation"; |
| 5 | import { OpenAI } from "../Icons"; |
| 6 | import { Button } from "../ui/button"; |
| 7 | import { |
| 8 | Dialog, |
| 9 | DialogPopup, |
| 10 | DialogHeader, |
| 11 | DialogTitle, |
| 12 | DialogDescription, |
| 13 | DialogFooter, |
| 14 | } from "../ui/dialog"; |
| 15 | import { ChatGptUsageButton } from "./ChatGptUsageButton"; |
| 16 | |
| 17 | const STORAGE_KEY = "t3:chatgpt-sharing-welcome:v1"; |
| 18 | function readAcknowledgedProfiles(): string[] { |
| 19 | try { |
| 20 | const value: unknown = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "[]"); |
| 21 | return Array.isArray(value) |
| 22 | ? value.filter((key): key is string => typeof key === "string") |
| 23 | : []; |
| 24 | } catch { |
| 25 | return []; |
| 26 | } |
| 27 | } |
| 28 | |
| 29 | /** Read the verified environment snapshot, never the browser callback acknowledgment. */ |
| 30 | export function ChatGptWelcomeCoordinator() { |
| 31 | const presentations = useAtomValue(environmentPresentations.presentationsAtom); |
| 32 | const [acknowledged, setAcknowledged] = useState(readAcknowledgedProfiles); |
| 33 | const profiles = [...presentations].flatMap(([environmentId, presentation]) => |
| 34 | presentation.connection.phase !== "connected" |
| 35 | ? [] |
| 36 | : (presentation.serverConfig?.providers ?? []).filter(usesChatGptSharing).map((provider) => ({ |
| 37 | key: JSON.stringify([ |
| 38 | environmentId, |
| 39 | provider.instanceId, |
| 40 | provider.auth.profileId ?? provider.auth.email ?? "default", |
| 41 | ]), |
| 42 | environmentLabel: presentation.entry.target.label, |
| 43 | providerName: provider.displayName ?? "Codex", |
| 44 | })), |
| 45 | ); |
| 46 | const next = profiles.find((profile) => !acknowledged.includes(profile.key)); |
| 47 | const dismiss = () => { |
| 48 | if (!next) return; |
| 49 | const updated = [...acknowledged, next.key]; |
| 50 | setAcknowledged(updated); |
| 51 | try { |
| 52 | localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)); |
| 53 | } catch { |
| 54 | /* Session dismissal still works. */ |
| 55 | } |
| 56 | }; |
| 57 | return ( |
| 58 | <Dialog |
| 59 | open={next !== undefined} |
| 60 | onOpenChange={(open) => { |
| 61 | if (!open) dismiss(); |
| 62 | }} |
| 63 | > |
| 64 | <DialogPopup> |
| 65 | <DialogHeader> |
| 66 | <OpenAI className="mb-2 size-8" aria-hidden="true" /> |
| 67 | <DialogTitle>Your ChatGPT plan is connected</DialogTitle> |
| 68 | <DialogDescription> |
| 69 | Eligible usage in The Knife uses your ChatGPT plan. Manage your shared usage and any |
| 70 | credit settings in ChatGPT. |
| 71 | </DialogDescription> |
| 72 | <p className="text-xs text-muted-foreground"> |
| 73 | {next?.providerName} on {next?.environmentLabel} |
| 74 | </p> |
| 75 | </DialogHeader> |
| 76 | <DialogFooter> |
| 77 | <ChatGptUsageButton /> |
| 78 | <Button onClick={dismiss}>Continue</Button> |
| 79 | </DialogFooter> |
| 80 | </DialogPopup> |
| 81 | </Dialog> |
| 82 | ); |
| 83 | } |