Skip to content

Commit

Permalink
feat: vision modal
Browse files Browse the repository at this point in the history
  • Loading branch information
Jazee6 committed Jun 3, 2024
1 parent c4653f7 commit e949c42
Show file tree
Hide file tree
Showing 16 changed files with 5,333 additions and 3,613 deletions.
19 changes: 2 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ docker run -d --name cloudflare-ai-web \
- 支持 Serverless 部署,无需服务器
- 支持开启访问密码,聊天记录本地存储
- 轻量化(~638 kB gzip)
- 支持`ChatGPT` `Gemini Pro`
- 支持`ChatGPT` `Gemini Pro` `Stable Diffusion` `llama-3` `通义千问`

### 模型支持

Expand All @@ -51,8 +51,7 @@ https://developers.cloudflare.com/workers-ai/models/
| CF_TOKEN | Cloudflare Workers AI Token |
| CF_GATEWAY | Cloudflare AI Gateway URL |
| OPENAI_API_KEY | OpenAI API Key (需要ChatGPT时填写) |
| G_API_KEY | Google AI API Key (需要GeminiPro时填写) |
| G_API_URL | Google AI 反代 (非美国ip填写,或参考以下配置) |
| G_API_KEY | Google AI API Key (需要GeminiPro时填写) |
| PASSWORD | 访问密码 (可选) |

#### CF_TOKEN
Expand All @@ -79,20 +78,6 @@ https://dash.cloudflare.com/

https://ai.google.dev/tutorials/rest_quickstart#set_up_your_api_key

#### G_API_URL

参考 https://github.com/Jazee6/gemini-proxy 搭建反代

或者在`nuxt.config.ts`中添加以下配置

```
nitro: {
vercel: {
regions: ["cle1", "iad1", "pdx1", "sfo1", "sin1", "syd1", "hnd1", "kix1"]
}
}
```

## Star History

[![Star History Chart](https://api.star-history.com/svg?repos=Jazee6/cloudflare-ai-web&type=Date)](https://star-history.com/#Jazee6/cloudflare-ai-web&Date)
49 changes: 39 additions & 10 deletions components/ChatInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,46 +36,74 @@ function handleInput(e: KeyboardEvent) {
if (input.value.trim() === '') return
if (p.loading) return
p.handleSend(input.value, addHistory.value, fileList.value)
p.handleSend(input.value, addHistory.value, toRaw(fileList.value))
input.value = ''
fileList.value = []
}
const imageType = ['image/png', 'image/jpeg', 'image/webp', 'image/heic', 'image/heif']
function checkFile(file: File) {
if (fileList.value.length >= 5) {
alert('You can only upload up to 5 images')
return false
}
if (imageType.indexOf(file.type) === -1) {
alert(imageType.join(', ') + ' only')
return false
}
if (file.size > 1024 * 1024 * 15) {
alert('The image size should be less than 15MB')
return false
}
return true
}
function handleAddFiles() {
const input = document.createElement('input')
input.type = 'file'
input.accept = 'image/*'
input.accept = imageType.join(',')
input.multiple = true
input.onchange = () => {
const files = Array.from(input.files || [])
files.forEach(file => {
if (file.type.indexOf('image') === -1) return
if (fileList.value.length >= 5) return
if (!checkFile(file)) return
const url = URL.createObjectURL(file)
fileList.value.push({file, url})
})
}
input.click()
}
// TODO paste ?? size limit ?? tips
onUnmounted(() => {
fileList.value.forEach(i => {
URL.revokeObjectURL(i.url)
})
})
const handlePaste = (e: ClipboardEvent) => {
const files = Array.from(e.clipboardData?.files || [])
files.forEach(file => {
if (!checkFile(file)) return
const url = URL.createObjectURL(file)
fileList.value.push({file, url})
})
}
</script>

<template>
<div class="relative">
<div class="absolute bottom-10 w-full flex flex-col">
<UButton class="self-center drop-shadow-xl mb-1" color="white" @click="openModelSelect=!openModelSelect">
<UButton class="self-center drop-shadow-xl mb-1 blur-global" color="white"
@click="openModelSelect=!openModelSelect">
{{ selectedModel.name }}
<template #trailing>
<UIcon name="i-heroicons-chevron-down-solid"/>
</template>
</UButton>
<ul v-if="selectedModel.type === 'vision'" style="margin: 0"
<ul v-if="selectedModel.type === 'universal'" style="margin: 0"
class="flex flex-wrap bg-white dark:bg-[#121212] rounded-t-md">
<li v-for="file in fileList" :key="file.url" class="relative group/img">
<button @click="fileList.splice(fileList.indexOf(file), 1)"
Expand All @@ -86,7 +114,7 @@ onUnmounted(() => {
</svg>
</button>
<img :src="file.url"
class="w-16 h-16 m-1 shadow-xl object-contain cursor-pointer group-hover/img:brightness-75 transition-all rounded-md"
class="max-h-16 m-1 shadow-xl cursor-pointer group-hover/img:brightness-75 transition-all rounded-md"
alt="selected image" @click="handleImgZoom($event.target as HTMLImageElement)"/>
</li>
</ul>
Expand All @@ -96,11 +124,12 @@ onUnmounted(() => {
<UButton class="m-1" @click="addHistory = !addHistory" :color="addHistory?'primary':'gray'"
icon="i-heroicons-clock-solid"/>
</UTooltip>
<UTooltip v-if="selectedModel.type === 'vision'" :text="$t('add_image')">
<UTooltip v-if="selectedModel.type === 'universal'" :text="$t('add_image')">
<UButton @click="handleAddFiles" color="white" class="m-1" icon="i-heroicons-paper-clip-16-solid"/>
</UTooltip>
<UTextarea v-model="input" :placeholder="$t('please_input_text') + '...' "
@keydown.prevent.enter="handleInput($event)"
@paste="handlePaste"
autofocus :rows="1" autoresize
class="flex-1 max-h-48 overflow-y-auto p-1"/>
<UButton @click="handleInput($event)" :disabled="loading" class="m-1">
Expand Down
73 changes: 46 additions & 27 deletions components/ChatList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import MarkdownIt from "markdown-it"
import markdownit from "markdown-it"
import hljs from "highlight.js";
import 'highlight.js/styles/github-dark-dimmed.min.css'
import {handleImgZoom} from "~/utils/tools";
defineProps<{
history: HistoryItem[]
Expand All @@ -22,59 +21,79 @@ const md: MarkdownIt = markdownit({
</script>

<template>
<ul class="overflow-y-auto scrollbar-hide pt-24 pl-1 flex flex-col space-y-1">
<ul class="overflow-y-auto overflow-x-hidden scrollbar-hide pt-24 pb-16 pl-1 flex flex-col space-y-1">
<template v-for="(i,index) in history" :key="i.id">
<template v-if="!i.content">
<USkeleton class="loading-item"/>
</template>
<template v-else>
<li v-if="i.type==='text'||i.type==='image-prompt'" class="chat-item slide-top prose"
:class="[i.role==='user'?'send':'reply-text', index+1===history.length && loading ? 'loading':'' ]"
v-html="i.role === 'user'? i.content: md.render(i.content)"/>
<li v-else-if="i.type === 'image'">
<template v-for="img_url in i.src_url" :key="img_url">
<img @click="handleImgZoom($event.target as HTMLImageElement)"
class="img-item slide-top cursor-pointer hover:brightness-75 transition-all"
:src="img_url"
:alt="history[index-1].content"/>
</template>
</li>
<li v-else-if="i.type==='error'" class="chat-item slide-top reply-error">
{{ i.content }}
</li>
<template v-if="i.role==='user'">
<li v-if="i.type === 'text' || i.type === 'image-prompt'" class="user chat-item user-text">
{{ i.content }}
</li>
<li v-else-if="i.type === 'image'" class="user image-item">
<template v-for="img_url in i.src_url" :key="img_url">
<img @click="handleImgZoom($event.target as HTMLImageElement)" :src="img_url" :alt="img_url" class="image"
:class="i.src_url?.length === 1 ? 'max-h-64' : (i.src_url?.length === 2 ? 'max-h-32': 'max-h-16')"/>
</template>
</li>
</template>
<template v-else>
<li v-if="i.type === 'text'" v-html="md.render(i.content)" class="assistant chat-item assistant-text prose"
:class="index+1===history.length && loading ? 'loading':''"/>
<li v-else-if="i.type === 'image'" class="assistant image-item">
<template v-for="img_url in i.src_url" :key="img_url">
<img @click="handleImgZoom($event.target as HTMLImageElement)" :src="img_url" :alt="img_url"
class="image"/>
</template>
</li>
<li v-else-if="i.type==='error'" class="assistant chat-item assistant-error">
{{ i.content }}
</li>
</template>
</template>
</template>
</ul>
</template>
<style scoped>
<style scoped lang="postcss">
.loading-item {
@apply rounded-xl px-2 py-1.5 h-10 shrink-0 w-1/3 animate-pulse
}
.user {
@apply self-end slide-top
}
.assistant {
@apply slide-top
}
.chat-item {
max-width: 80%;
@apply break-words rounded-xl px-2 py-1.5
@apply break-words rounded-xl px-2 py-1.5 max-w-[95%] md:max-w-[80%]
}
.image-item {
@apply flex rounded-xl space-x-1 max-w-[95%] md:max-w-[60%]
}
.img-item {
max-width: 80%;
@apply rounded-xl
.image {
@apply cursor-pointer hover:brightness-75 transition-all rounded-md
}
.send {
@apply self-end bg-green-500 text-white dark:bg-green-700 dark:text-gray-300
.user-text {
@apply bg-green-500 text-white dark:bg-green-700 dark:text-gray-300
}
.reply-text {
.assistant-text {
@apply self-start bg-gray-200 text-black dark:bg-gray-400
}
.reply-error {
.assistant-error {
@apply self-start bg-red-200 dark:bg-red-400 dark:text-black
}
.send::selection {
.user-text::selection {
@apply text-neutral-900 bg-gray-300
}
Expand Down
2 changes: 1 addition & 1 deletion components/Footer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {version} from '../package.json'
<template>
<footer class="shrink-0 h-6">
<div class="text-center text-xs font-light">
V{{ version }} | Star on
{{ version }} | Star on
<a href="https://github.com/Jazee6/cloudflare-ai-web" target="_blank"
class="text-neutral-500 hover:text-neutral-900 underline dark:text-neutral-400 dark:hover:text-neutral-300">GitHub</a>
</div>
Expand Down
8 changes: 4 additions & 4 deletions components/ModelSelect.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import {imageGenModels, textGenModels} from "~/utils/db";
import {imageGenModels, textGenModels, uniModals} from "~/utils/db";
const {t} = useI18n()
const {selectedModel, openModelSelect} = useGlobalState()
Expand All @@ -13,9 +13,9 @@ watch(selectedModel, v => {
const groups = computed(() => [
{
key: 'vision',
label: t('vision'),
commands: visionModals.map(i => ({
key: 'universal',
label: t('universal'),
commands: uniModals.map(i => ({
id: i.id,
label: i.name
}))
Expand Down
4 changes: 2 additions & 2 deletions i18n.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default defineI18nConfig(() => ({
text_generation: '文本生成',
image_generation: '图像生成',
system_prompt: '系统提示',
vision: '视觉',
universal: '多模',
},
en: {
setting: 'Setting',
Expand All @@ -41,7 +41,7 @@ export default defineI18nConfig(() => ({
text_generation: 'Text generation',
image_generation: 'Image generation',
system_prompt: 'System prompt',
vision: 'Vision',
universal: 'Universal',
}
}
}))
9 changes: 2 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cloudflare-ai-web",
"version": "3.0.0beta",
"version": "3.0.0-beta.1",
"private": true,
"type": "module",
"scripts": {
Expand All @@ -20,17 +20,12 @@
"vue-router": "^4.3.0"
},
"dependencies": {
"@google/generative-ai": "^0.3.1",
"@google/generative-ai": "^0.12.0",
"@nuxt/ui": "^2.15.0",
"dexie": "^4.0.1",
"eventsource-parser": "^1.1.2",
"highlight.js": "^11.9.0",
"markdown-it": "^14.1.0"
},
"pnpm": {
"patchedDependencies": {
"@google/generative-ai@0.3.1": "patches/@google__generative-ai@0.3.1.patch"
}
},
"packageManager": "pnpm@9.1.2+sha512.127dc83b9ea10c32be65d22a8efb4a65fb952e8fefbdfded39bdc3c97efc32d31b48b00420df2c1187ace28c921c902f0cb5a134a4d032b8b5295cbfa2c681e2"
}
Loading

0 comments on commit e949c42

Please sign in to comment.