summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/context/prompt.tsx
blob: 8d3590cd99683d95bb67809c244ec51e06af949d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import { createStore } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { batch, createMemo } from "solid-js"
import { useParams } from "@solidjs/router"
import { TextSelection } from "./local"
import { persisted } from "@/utils/persist"

interface PartBase {
  content: string
  start: number
  end: number
}

export interface TextPart extends PartBase {
  type: "text"
}

export interface FileAttachmentPart extends PartBase {
  type: "file"
  path: string
  selection?: TextSelection
}

export interface ImageAttachmentPart {
  type: "image"
  id: string
  filename: string
  mime: string
  dataUrl: string
}

export type ContentPart = TextPart | FileAttachmentPart | ImageAttachmentPart
export type Prompt = ContentPart[]

export const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]

export function isPromptEqual(promptA: Prompt, promptB: Prompt): boolean {
  if (promptA.length !== promptB.length) return false
  for (let i = 0; i < promptA.length; i++) {
    const partA = promptA[i]
    const partB = promptB[i]
    if (partA.type !== partB.type) return false
    if (partA.type === "text" && partA.content !== (partB as TextPart).content) {
      return false
    }
    if (partA.type === "file" && partA.path !== (partB as FileAttachmentPart).path) {
      return false
    }
    if (partA.type === "image" && partA.id !== (partB as ImageAttachmentPart).id) {
      return false
    }
  }
  return true
}

function cloneSelection(selection?: TextSelection) {
  if (!selection) return undefined
  return { ...selection }
}

function clonePart(part: ContentPart): ContentPart {
  if (part.type === "text") return { ...part }
  if (part.type === "image") return { ...part }
  return {
    ...part,
    selection: cloneSelection(part.selection),
  }
}

function clonePrompt(prompt: Prompt): Prompt {
  return prompt.map(clonePart)
}

export const { use: usePrompt, provider: PromptProvider } = createSimpleContext({
  name: "Prompt",
  init: () => {
    const params = useParams()
    const name = createMemo(() => `${params.dir}/prompt${params.id ? "/" + params.id : ""}.v1`)

    const [store, setStore, _, ready] = persisted(
      name(),
      createStore<{
        prompt: Prompt
        cursor?: number
      }>({
        prompt: clonePrompt(DEFAULT_PROMPT),
        cursor: undefined,
      }),
    )

    return {
      ready,
      current: createMemo(() => store.prompt),
      cursor: createMemo(() => store.cursor),
      dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
      set(prompt: Prompt, cursorPosition?: number) {
        const next = clonePrompt(prompt)
        batch(() => {
          setStore("prompt", next)
          if (cursorPosition !== undefined) setStore("cursor", cursorPosition)
        })
      },
      reset() {
        batch(() => {
          setStore("prompt", clonePrompt(DEFAULT_PROMPT))
          setStore("cursor", 0)
        })
      },
    }
  },
})