app.tsx 4.91 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:5001'
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)
42
43
44
45
46
47
48
    let re = /}{/g
    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
49
50
        break
      }
51
52

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

  return
}
Jeffrey Morgan's avatar
Jeffrey Morgan committed
58

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

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

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

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