app.tsx 4.94 KB
Newer Older
Jeffrey Morgan's avatar
Jeffrey Morgan committed
1
import { useState } from 'react'
Jeffrey Morgan's avatar
Jeffrey Morgan committed
2
3
4
import path from 'path'
import os from 'os'
import { dialog, getCurrentWindow } from '@electron/remote'
Jeffrey Morgan's avatar
Jeffrey Morgan committed
5

Jeffrey Morgan's avatar
Jeffrey Morgan committed
6
const API_URL = 'http://127.0.0.1:7734'
Jeffrey Morgan's avatar
Jeffrey Morgan committed
7
8

type Message = {
Jeffrey Morgan's avatar
Jeffrey Morgan committed
9
  sender: 'bot' | 'human'
Jeffrey Morgan's avatar
Jeffrey Morgan committed
10
11
12
  content: string
}

Jeffrey Morgan's avatar
Jeffrey Morgan committed
13
14
15
const userInfo = os.userInfo()

async function generate(prompt: string, model: string, callback: (res: string) => void) {
16
  const result = await fetch(`${API_URL}/generate`, {
Jeffrey Morgan's avatar
Jeffrey Morgan committed
17
    method: 'POST',
18
19
20
    headers: {
      'Content-Type': 'application/json',
    },
Jeffrey Morgan's avatar
Jeffrey Morgan committed
21
    body: JSON.stringify({
Jeffrey Morgan's avatar
Jeffrey Morgan committed
22
23
      prompt,
      model,
Jeffrey Morgan's avatar
Jeffrey Morgan committed
24
25
26
    }),
  })

Jeffrey Morgan's avatar
Jeffrey Morgan committed
27
  if (!result.ok) {
Jeffrey Morgan's avatar
Jeffrey Morgan committed
28
29
30
31
32
33
34
35
36
37
38
39
    return
  }

  let reader = result.body.getReader()

  while (true) {
    const { done, value } = await reader.read()

    if (done) {
      break
    }

Jeffrey Morgan's avatar
Jeffrey Morgan committed
40
41
    let decoder = new TextDecoder()
    let str = decoder.decode(value)
Jeffrey Morgan's avatar
Jeffrey Morgan committed
42
43

    let re = /}\s*{/g
44
45
46
47
48
49
    str = '[' + str.replace(re, '},{') + ']'
    let messages = JSON.parse(str)

    for (const message of messages) {
      const choice = message.choices[0]
      if (choice.finish_reason === 'stop') {
Jeffrey Morgan's avatar
Jeffrey Morgan committed
50
51
        break
      }
52
53

      callback(choice.text)
Jeffrey Morgan's avatar
Jeffrey Morgan committed
54
55
56
57
58
    }
  }

  return
}
Jeffrey Morgan's avatar
Jeffrey Morgan committed
59

Jeffrey Morgan's avatar
Jeffrey Morgan committed
60
export default function () {
Jeffrey Morgan's avatar
Jeffrey Morgan committed
61
62
  const [prompt, setPrompt] = useState('')
  const [messages, setMessages] = useState<Message[]>([])
Jeffrey Morgan's avatar
Jeffrey Morgan committed
63
64
  const [model, setModel] = useState('')
  const [generating, setGenerating] = useState(false)
Jeffrey Morgan's avatar
Jeffrey Morgan committed
65

Jeffrey Morgan's avatar
Jeffrey Morgan committed
66
  return (
Jeffrey Morgan's avatar
Jeffrey Morgan committed
67
    <div className='flex min-h-screen flex-1 flex-col justify-between bg-white'>
Jeffrey Morgan's avatar
Jeffrey Morgan committed
68
      <header className='drag sticky top-0 z-50 flex h-14 w-full flex-row items-center border-b border-black/10 bg-white/75 backdrop-blur-md'>
Jeffrey Morgan's avatar
Jeffrey Morgan committed
69
        <div className='mx-auto w-full max-w-xl leading-none'>
Jeffrey Morgan's avatar
Jeffrey Morgan committed
70
          <h1 className='text-sm font-medium'>{path.basename(model).replace('.bin', '')}</h1>
Jeffrey Morgan's avatar
Jeffrey Morgan committed
71
72
        </div>
      </header>
Jeffrey Morgan's avatar
Jeffrey Morgan committed
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
      {model ? (
        <section className='mx-auto mb-10 w-full max-w-xl flex-1 break-words'>
          {messages.map((m, i) => (
            <div className='my-4 flex gap-4' key={i}>
              <div className='flex-none pr-1 text-lg'>
                {m.sender === 'human' ? (
                  <div className='mt-px flex h-6 w-6 items-center justify-center rounded-md bg-neutral-200 text-sm text-neutral-700'>
                    {userInfo.username[0].toUpperCase()}
                  </div>
                ) : (
                  <div className='mt-0.5 flex h-6 w-6 items-center justify-center rounded-md bg-blue-600 text-sm text-white'>
                    {path.basename(model)[0].toUpperCase()}
                  </div>
                )}
              </div>
              <div className='flex-1 text-gray-800'>
                {m.content}
Eva Ho's avatar
Eva Ho committed
90
                {m.sender === 'bot' && generating && i === messages.length - 1 && (
Jeffrey Morgan's avatar
Jeffrey Morgan committed
91
92
93
                  <span className='blink relative -top-[3px] left-1 text-[10px]'></span>
                )}
              </div>
Jeffrey Morgan's avatar
Jeffrey Morgan committed
94
            </div>
Jeffrey Morgan's avatar
Jeffrey Morgan committed
95
96
97
98
99
100
101
102
103
          ))}
        </section>
      ) : (
        <section className='flex flex-1 select-none flex-col items-center justify-center pb-20'>
          <h2 className='text-3xl font-light text-neutral-400'>No model selected</h2>
          <button
            onClick={async () => {
              const res = await dialog.showOpenDialog(getCurrentWindow(), {
                properties: ['openFile', 'multiSelections'],
Jeffrey Morgan's avatar
Jeffrey Morgan committed
104
              })
Jeffrey Morgan's avatar
Jeffrey Morgan committed
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
              if (res.canceled) {
                return
              }

              setModel(res.filePaths[0])
            }}
            className='rounded-dm my-8 rounded-md bg-blue-600 px-4 py-2 text-sm text-white hover:brightness-110'
          >
            Open file...
          </button>
        </section>
      )}
      <div className='sticky bottom-0 bg-gradient-to-b from-transparent to-white'>
        {model && (
          <textarea
            autoFocus
            rows={1}
            value={prompt}
            placeholder='Send a message...'
            onChange={e => setPrompt(e.target.value)}
            className='mx-auto my-4 block w-full max-w-xl resize-none rounded-xl border border-gray-200 px-5 py-3.5 text-[15px] shadow-lg shadow-black/5 focus:outline-none'
            onKeyDownCapture={async e => {
              if (e.key === 'Enter' && !e.shiftKey) {
                e.preventDefault()

                if (generating) {
                  return
                }

                if (!prompt) {
                  return
                }

                await setMessages(messages => {
                  return [...messages, { sender: 'human', content: prompt }, { sender: 'bot', content: '' }]
                })
Jeffrey Morgan's avatar
Jeffrey Morgan committed
141

Jeffrey Morgan's avatar
Jeffrey Morgan committed
142
                setPrompt('')
Jeffrey Morgan's avatar
Jeffrey Morgan committed
143

Jeffrey Morgan's avatar
Jeffrey Morgan committed
144
145
146
147
148
149
                setGenerating(true)
                await generate(prompt, model, res => {
                  setMessages(messages => {
                    let last = messages[messages.length - 1]
                    return [...messages.slice(0, messages.length - 1), { ...last, content: last.content + res }]
                  })
Jeffrey Morgan's avatar
Jeffrey Morgan committed
150
                })
Jeffrey Morgan's avatar
Jeffrey Morgan committed
151
152
153
154
155
                setGenerating(false)
              }
            }}
          ></textarea>
        )}
Jeffrey Morgan's avatar
Jeffrey Morgan committed
156
      </div>
Jeffrey Morgan's avatar
Jeffrey Morgan committed
157
158
159
    </div>
  )
}