Skip to content

Commit

Permalink
feat: openai api
Browse files Browse the repository at this point in the history
  • Loading branch information
liou666 committed Mar 25, 2023
1 parent 014c035 commit a898eca
Show file tree
Hide file tree
Showing 11 changed files with 307 additions and 21 deletions.
2 changes: 2 additions & 0 deletions electron/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ async function createWindow() {
// Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation
nodeIntegration: true,
contextIsolation: false,
webSecurity: false,

},
})

Expand Down
6 changes: 3 additions & 3 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import Header from './components/Header.vue'
import Aside from './components/Nav.vue'
import Content from './components/Content.vue'
import Aside from '@/components/Nav.vue'
import Content from '@/components/Content.vue'
import Header from '@/components/Header.vue'
console.log('[App.vue]', `Electron ${process.versions.electron}!`)
</script>

Expand Down
98 changes: 83 additions & 15 deletions src/components/Content.vue
Original file line number Diff line number Diff line change
@@ -1,26 +1,94 @@
<script setup lang="ts">
const chatMessages = ref<{
content: string
role: 'user' | 'assistant' | 'system'
}[]>([])
import Button from '@/components/widgets/Button.vue'
import { generateDashboardInfo, generateText } from '@/server/api'
import { useScroll } from '@/hooks'
// states
const chatMessages = ref<ChatMessage[]>([])
const message = ref('')
const loading = ref(false)
// hooks
const { el, scrollToBottom } = useScroll()
// effects
watch(chatMessages.value, () => nextTick(() => scrollToBottom()))
// methods
const roleClass = (role: string) => {
switch (role) {
case 'user':
return 'bg-gradient-to-br from-green-400 to-blue-300 rounded-full p-4'
case 'assistant':
return 'bg-gradient-to-br from-blue-300 to-red-600 rounded-full p-4'
case 'system':
return 'bg-gray-500'
}
}
const onSubmit = async () => {
if (!message.value) return
chatMessages.value.push({
content: message.value,
role: 'user',
})
message.value = ''
loading.value = true
const res = await generateText(chatMessages.value)
chatMessages.value.push({
content: res,
role: 'assistant',
})
loading.value = false
}
</script>

<template>
<div rounded-md bg-white>
content
<div v-for="item, i in chatMessages" :key="i">
{{ item.content }}
<div flex flex-col p-2 rounded-md bg-gray-500>
<div ref="el" class="hide-scrollbar flex-1 overflow-auto">
<div
v-for="item, i in chatMessages"
:key="i"
center-y odd:flex-row-reverse
>
<div :class="roleClass(item.role)" />
<div relative>
<p mx-2 px-2 py-1 chat-box>
{{ item.content }}
</p>
</div>
</div>
</div>
<!-- <div flex px-4 fixed bottom-5>

<div class="flex h-10 w-[-webkit-fill-available] mt-1">
<Button
mr-1
i-carbon:microphon
>
<i i-carbon:microphone />
</Button>
<input
class="
text-slate-400 placeholder:text-slate-400 placeholder:opacity-30 bg-slate-500/40 border-0 text-lg outline-none rounded w-full h-full px-3
"
v-if="!loading"
v-model="message"
type="text"
placeholder="Type your message here..."
input-box p-3 flex-1
>
<div btn>
hi
<div v-else class="loading-btn">
AI Is Thinking...
</div>
</div> -->
<Button
:disabled="loading"
mx-1
@click="onSubmit"
>
<i i-carbon:send-alt />
</Button>
<Button
:disabled="loading"
@click="chatMessages = []"
>
<i i-carbon:trash-can />
</Button>
</div>
</div>
</template>
3 changes: 1 addition & 2 deletions src/components/Header.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@ const openKey = useLocalStorage('openKey', '')
<i mr-2 rotate-115 i-ic:baseline-key />
<div v-if="isKeyMode" w-55>
<input
v-model="openKey"
type="password"
class="input"
:value="openKey"
@input="openKey = ($event.target as HTMLInputElement).value"
>
<i icon-btn i-carbon-checkmark @click="toggle()" />
<i icon-btn i-carbon-close @click="toggle()" />
Expand Down
12 changes: 12 additions & 0 deletions src/components/widgets/Button.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script setup lang="ts">
const emit = defineEmits(['click'])
</script>

<template>
<button
border-0 btn
@click="emit('click')"
>
<slot />
</button>
</template>
27 changes: 27 additions & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export function useScroll<T extends HTMLElement=HTMLElement>() {
const el = ref<T | null>(null)
const scrollToBottom = () => {
el.value?.scrollTo({
top: el.value.scrollHeight,
behavior: 'smooth',
})
}
const scrollToTop = () => {
el.value?.scrollTo({
top: 0,
behavior: 'smooth',
})
}
const scrollTo = (y: number) => {
el.value?.scrollTo({
top: y,
behavior: 'smooth',
})
}
return {
el,
scrollToBottom,
scrollToTop,
scrollTo,
}
}
19 changes: 19 additions & 0 deletions src/server/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { OpenAi } from '@/utils'
const apiKey = import.meta.env.VITE_OPENAI_API_KEY
const proxy = import.meta.env.VITE_SERVE_PROXY

const openai = new OpenAi(apiKey, proxy)

export const generateText = async (messages: ChatMessage[]) => {
const { url, initOptions } = openai.generateTurboPayload({ messages })
const response = await fetch(url, initOptions)
const data = await response.json()
return data.choices[0].message.content
}

export const generateDashboardInfo = async () => {
const { url, initOptions } = openai.generateDashboardPayload()
const response = await fetch(url, initOptions)
const data = await response.json()
return data
}
29 changes: 29 additions & 0 deletions src/types..d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
interface MessageCommon {
content: string
}

interface UserMessage extends MessageCommon {
role: 'user'
}

interface AssistantMessage extends MessageCommon {
role: 'assistant'
}

interface SystemMessage extends MessageCommon {
role: 'system'
}

interface ChatMessage extends MessageCommon {
role: UserMessage['role'] | AssistantMessage['role'] | SystemMessage['role']
}

interface RequestInitWithDispatcher extends RequestInit {
dispatcher?: any
}

interface ImagePayload {
prompt: string
n?: number
size?: string
}
2 changes: 2 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './openAi'

123 changes: 123 additions & 0 deletions src/utils/openAi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import type { ParsedEvent, ReconnectInterval } from 'eventsource-parser'
import { createParser } from 'eventsource-parser'

export interface OpenAiPayload {
url: string
initOptions: RequestInit
}

export class OpenAi {
private apiKey: string
private proxy: string
private header: Record<string, string>

constructor(apiKey: string, proxy = 'https://api.openai.com') {
this.apiKey = apiKey
this.proxy = proxy
this.header = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
}
}

// gpt-3.5-turbo
private getTurboUrl() {
return `${this.proxy}/v1/chat/completions`
}

// common chat text-davinci-003
private getChatUrl() {
return `${this.proxy}/v1/chat/generations`
}

// image-ai
private getImageUrl() {
return `${this.proxy}/v1/images/generations`
}

// 账户余额信息
private getDashboardUrl() {
return `${this.proxy}/v1/dashboard/billing/credit_grants`
}

generateTurboPayload(body: Record<string, any> = {}): OpenAiPayload {
return {
url: this.getTurboUrl(),
initOptions: {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
},
method: 'POST',
body: JSON.stringify({ ...body, model: 'gpt-3.5-turbo' }),
},
}
}

generateChatPayload(body: Record<string, any> = {}): OpenAiPayload {
return {
url: this.getChatUrl(),
initOptions: {
headers: this.header,
method: 'POST',
body: JSON.stringify({
...body,
model: 'text-davinci-003',
}),
},
}
}

generateImagePayload(body: Record<string, any> = {}): OpenAiPayload {
return {
url: this.getImageUrl(),
initOptions: {
headers: this.header,
method: 'POST',
body: JSON.stringify({ ...body }),
},
}
}

generateDashboardPayload(): OpenAiPayload {
return {
url: this.getDashboardUrl(),
initOptions: {
method: 'GET',
headers: this.header,
},
}
}
}

export const parseOpenAIStream = (rawResponse: Response) => {
const encoder = new TextEncoder()
const decoder = new TextDecoder()
const stream = new ReadableStream({
async start(controller) {
const streamParser = (event: ParsedEvent | ReconnectInterval) => {
if (event.type === 'event') {
const data = event.data
if (data === '[DONE]') {
controller.close()
return
}
try {
const json = JSON.parse(data)
const text = json.choices[0].delta?.content || ''
const queue = encoder.encode(text)
controller.enqueue(queue)
}
catch (e) {
controller.error(e)
}
}
}
const parser = createParser(streamParser)
for await (const chunk of rawResponse.body as any)
parser.feed(decoder.decode(chunk))
},
})

return stream
}
7 changes: 6 additions & 1 deletion unocss.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,17 @@ import {

export default defineConfig({
shortcuts: [
['btn', 'px-4 py-1 rounded inline-block bg-teal-700 text-white cursor-pointer hover:bg-teal-800 disabled:cursor-default disabled:bg-gray-600 disabled:opacity-50'],
['btn', 'px-4 py-1 flex items-center rounded inline-block bg-teal-700 text-white cursor-pointer hover:bg-teal-800 disabled:cursor-default disabled:bg-gray-600 disabled:opacity-50'],
['gray-btn', 'px-4 flex items-center text-xl text-white rounded bg-gray-500 hover:shadow-md duration-300 cursor-pointer border-0 font-sans disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-gray-300 disabled:text-gray-500'],
['loading-btn', 'animate-pulse w-full h-full px-3 flex items-center justify-center dark:text-slate-400 dark:placeholder:text-slate-400 dark:placeholder:opacity-30 dark:bg-slate-500/40'],
['icon-btn', 'text-[0.9em] inline-block cursor-pointer select-none opacity-75 transition duration-200 ease-in-out hover:opacity-100 text-neutral-900 hover:text-black !outline-none'],
['center', 'flex items-center justify-center'],
['center-y', 'flex items-center'],
['center-x', 'flex justify-center'],
['input', ' flex-1 text-neutral-700 bg-transparent border-0 border-b border-neutral focus:border-neutral-700 text-left overflow-hidden overflow-ellipsis pr-1 outline-none'],
['input-box', 'dark:text-slate-400 dark:placeholder:text-slate-400 dark:placeholder:opacity-30 dark:bg-slate-500/40 border-0 text-lg outline-none rounded px-3;'],
['chat-box', 'bg-white dark:bg-slate-500 dark:text-slate-200 rounded'],

],
presets: [
presetUno(),
Expand Down

0 comments on commit a898eca

Please sign in to comment.