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
    str = '[' + str.replace(re, '},{') + ']'
    let messages = JSON.parse(str)

    for (const message of messages) {
      const choice = message.choices[0]
49
50
51

      callback(choice.text)

52
      if (choice.finish_reason === 'stop') {
Jeffrey Morgan's avatar
Jeffrey Morgan committed
53
54
55
56
57
58
59
        break
      }
    }
  }

  return
}
Jeffrey Morgan's avatar
Jeffrey Morgan committed
60

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

Jeffrey Morgan's avatar
Jeffrey Morgan committed
67
  return (
Jeffrey Morgan's avatar
Jeffrey Morgan committed
68
    <div className='flex min-h-screen flex-1 flex-col justify-between bg-white'>
Jeffrey Morgan's avatar
Jeffrey Morgan committed
69
      <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
70
        <div className='mx-auto w-full max-w-xl leading-none'>
Jeffrey Morgan's avatar
Jeffrey Morgan committed
71
          <h1 className='text-sm font-medium'>{path.basename(model).replace('.bin', '')}</h1>
Jeffrey Morgan's avatar
Jeffrey Morgan committed
72
73
        </div>
      </header>
Jeffrey Morgan's avatar
Jeffrey Morgan committed
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
      {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
91
                {m.sender === 'bot' && generating && i === messages.length - 1 && (
Jeffrey Morgan's avatar
Jeffrey Morgan committed
92
93
94
                  <span className='blink relative -top-[3px] left-1 text-[10px]'></span>
                )}
              </div>
Jeffrey Morgan's avatar
Jeffrey Morgan committed
95
            </div>
Jeffrey Morgan's avatar
Jeffrey Morgan committed
96
97
98
99
100
101
102
103
104
          ))}
        </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
105
              })
Jeffrey Morgan's avatar
Jeffrey Morgan committed
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
141
              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
142

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

Jeffrey Morgan's avatar
Jeffrey Morgan committed
145
146
147
148
149
150
                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
151
                })
Jeffrey Morgan's avatar
Jeffrey Morgan committed
152
153
154
155
156
                setGenerating(false)
              }
            }}
          ></textarea>
        )}
Jeffrey Morgan's avatar
Jeffrey Morgan committed
157
      </div>
Jeffrey Morgan's avatar
Jeffrey Morgan committed
158
159
160
    </div>
  )
}