-
Notifications
You must be signed in to change notification settings - Fork 274
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
307 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './openAi' | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters