| 1 | import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell"; |
| 2 | import { |
| 3 | type ModelSelection, |
| 4 | repositoryGroupingKeyOf, |
| 5 | type ServerProvider, |
| 6 | } from "@t3tools/contracts"; |
| 7 | import { |
| 8 | buildNewThreadLink, |
| 9 | parsePromptLinkRepository, |
| 10 | PROMPT_LINK_MAX_PROMPT_LENGTH, |
| 11 | type PromptLinkRepository, |
| 12 | } from "@t3tools/shared/promptLinks"; |
| 13 | |
| 14 | import { type ComposerThreadTarget, useComposerDraftStore } from "~/composerDraftStore"; |
| 15 | import { writeTextToClipboard } from "~/hooks/useCopyToClipboard"; |
| 16 | import { configuredHostedAppUrl } from "~/hostedPairing"; |
| 17 | |
| 18 | import { stackedThreadToast, toastManager } from "../ui/toast"; |
| 19 | |
| 20 | /** A shareable link to a prompt on the hosted app, or null when the prompt cannot fit in one. */ |
| 21 | export function promptLinkFor( |
| 22 | prompt: string, |
| 23 | context?: { |
| 24 | readonly repository: PromptLinkRepository | null; |
| 25 | readonly provider: string | null; |
| 26 | readonly model: string | null; |
| 27 | }, |
| 28 | ): string | null { |
| 29 | const trimmed = prompt.trim(); |
| 30 | if (trimmed.length === 0 || trimmed.length > PROMPT_LINK_MAX_PROMPT_LENGTH) return null; |
| 31 | return buildNewThreadLink({ prompt: trimmed, ...context, origin: configuredHostedAppUrl() }); |
| 32 | } |
| 33 | |
| 34 | /** The project's GitHub repository, the only kind a prompt link can name. */ |
| 35 | export function promptLinkRepositoryOf( |
| 36 | project: Pick<EnvironmentProject, "repositoryIdentity"> | undefined, |
| 37 | ): PromptLinkRepository | null { |
| 38 | const identity = project?.repositoryIdentity; |
| 39 | if (identity == null) return null; |
| 40 | const key = repositoryGroupingKeyOf(identity); |
| 41 | return key.startsWith("github.com/") |
| 42 | ? parsePromptLinkRepository(key.slice("github.com/".length)) |
| 43 | : null; |
| 44 | } |
| 45 | |
| 46 | /** |
| 47 | * The draft in a composer as a link: its text and, with `withContext`, the project's repository |
| 48 | * and the model the composer has picked. Null when the composer holds no prompt to share. |
| 49 | */ |
| 50 | export function composerPromptLink(input: { |
| 51 | readonly target: ComposerThreadTarget; |
| 52 | readonly project: EnvironmentProject | undefined; |
| 53 | readonly providers: ReadonlyArray<ServerProvider>; |
| 54 | readonly threadModelSelection: ModelSelection | null; |
| 55 | readonly withContext: boolean; |
| 56 | }): string | null { |
| 57 | const draft = useComposerDraftStore.getState().getComposerDraft(input.target); |
| 58 | if (!draft) return null; |
| 59 | if (!input.withContext) return promptLinkFor(draft.prompt); |
| 60 | const selection = |
| 61 | (draft.activeProvider ? draft.modelSelectionByProvider[draft.activeProvider] : undefined) ?? |
| 62 | input.threadModelSelection; |
| 63 | const provider = selection |
| 64 | ? (input.providers.find((candidate) => candidate.instanceId === selection.instanceId)?.driver ?? |
| 65 | null) |
| 66 | : null; |
| 67 | return promptLinkFor(draft.prompt, { |
| 68 | repository: promptLinkRepositoryOf(input.project), |
| 69 | provider, |
| 70 | model: provider === null ? null : (selection?.model ?? null), |
| 71 | }); |
| 72 | } |
| 73 | |
| 74 | /** Copies a prompt link and says so. */ |
| 75 | export async function copyPromptLink(link: string): Promise<void> { |
| 76 | try { |
| 77 | if (!(await writeTextToClipboard(link))) return; |
| 78 | toastManager.add({ type: "success", title: "Prompt link copied" }); |
| 79 | } catch (error) { |
| 80 | toastManager.add( |
| 81 | stackedThreadToast({ |
| 82 | type: "error", |
| 83 | title: "Could not copy the prompt link", |
| 84 | description: error instanceof Error ? error.message : "An error occurred.", |
| 85 | }), |
| 86 | ); |
| 87 | } |
| 88 | } |