Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5efd7b0533 | ||
|
|
a80d9163b8 | ||
|
|
d25148c731 |
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
SD_API_BASE_URL=
|
||||
OPENAI_API_KEY=
|
||||
178
components/ai/AiChooseFood.vue
Normal file
178
components/ai/AiChooseFood.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<script lang="ts" setup>
|
||||
import type { StuffItem } from '~/types'
|
||||
import { meat, staple, vegetable } from '~/data/food'
|
||||
|
||||
import { useEmojiAnimation } from '~/composables/animation'
|
||||
import type { AIRecipeInfo } from '~/packages/ai/src'
|
||||
import { generateRecipeInfo, getRecipeImage } from '~/utils/api'
|
||||
|
||||
const rStore = useRecipeStore()
|
||||
const curStuff = computed(() => rStore.selectedStuff)
|
||||
|
||||
const recipeBtnRef = ref<HTMLButtonElement>()
|
||||
const { playAnimation } = useEmojiAnimation(recipeBtnRef)
|
||||
|
||||
const gtm = useGtm()
|
||||
const recipePanelRef = ref()
|
||||
const { isVisible, show } = useInvisibleElement(recipePanelRef)
|
||||
|
||||
function toggleStuff(item: StuffItem, category = '', _e?: Event) {
|
||||
rStore.toggleStuff(item.name)
|
||||
|
||||
if (curStuff.value.includes(item.name))
|
||||
playAnimation(item.emoji)
|
||||
|
||||
gtm?.trackEvent({
|
||||
event: 'click',
|
||||
category: `${category}_${item.name}`,
|
||||
action: 'click_stuff',
|
||||
label: '食材',
|
||||
})
|
||||
gtm?.trackEvent({
|
||||
event: 'click_stuff',
|
||||
action: item.name,
|
||||
})
|
||||
}
|
||||
|
||||
// cook recipe
|
||||
const cooking = ref(false)
|
||||
const recipeImg = ref('')
|
||||
|
||||
const aiRecipeInfo = ref<AIRecipeInfo>({
|
||||
名称: '名称',
|
||||
介绍: '介绍',
|
||||
})
|
||||
|
||||
async function cook() {
|
||||
cooking.value = true
|
||||
const foods = rStore.selectedStuff
|
||||
|
||||
// reset
|
||||
aiRecipeInfo.value = ({
|
||||
名称: '起名中...',
|
||||
介绍: '正在思考怎么介绍...',
|
||||
})
|
||||
recipeImg.value = ''
|
||||
|
||||
// generate
|
||||
const [info, img] = await Promise.all([generateRecipeInfo(foods), getRecipeImage(foods)])
|
||||
aiRecipeInfo.value = info
|
||||
recipeImg.value = img
|
||||
|
||||
cooking.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2 m="t-4" text="xl" font="bold" p="1">
|
||||
🥘 先选一下食材
|
||||
</h2>
|
||||
<div>
|
||||
<h2 opacity="90" text="base" font="bold" p="1">
|
||||
🥬 菜菜们
|
||||
</h2>
|
||||
<div>
|
||||
<VegetableTag
|
||||
v-for="item, i in vegetable" :key="i"
|
||||
:active="curStuff.includes(item.name)"
|
||||
@click="toggleStuff(item, 'vegetable')"
|
||||
>
|
||||
<span v-if="item.emoji" class="inline-flex">{{ item.emoji }}</span>
|
||||
<span v-else-if="item.image" class="inline-flex">
|
||||
<img class="inline-flex" w="2" h="2" width="10" height="10" :src="item.image" :alt="item.name">
|
||||
</span>
|
||||
<span class="inline-flex" m="l-1">{{ item.name }}</span>
|
||||
</VegetableTag>
|
||||
</div>
|
||||
</div>
|
||||
<div m="y-4">
|
||||
<h2 opacity="90" text="base" font="bold" p="1">
|
||||
🥩 肉肉们
|
||||
</h2>
|
||||
<div>
|
||||
<MeatTag
|
||||
v-for="item, i in meat" :key="i"
|
||||
:active="curStuff.includes(item.name)"
|
||||
@click="toggleStuff(item, 'meat')"
|
||||
>
|
||||
<span>{{ item.emoji }}</span>
|
||||
<span m="l-1">{{ item.name }}</span>
|
||||
</MeatTag>
|
||||
</div>
|
||||
</div>
|
||||
<div m="y-4">
|
||||
<h2 opacity="90" text="base" font="bold" p="1">
|
||||
🍚 主食
|
||||
</h2>
|
||||
<div>
|
||||
<StapleTag
|
||||
v-for="item, i in staple" :key="i"
|
||||
:active="curStuff.includes(item.name)"
|
||||
@click="toggleStuff(item, 'staple')"
|
||||
>
|
||||
<span>{{ item.emoji }}</span>
|
||||
<span m="l-1">{{ item.name }}</span>
|
||||
</StapleTag>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div m="t-4">
|
||||
<h2 text="xl" font="bold" p="1">
|
||||
🍳 再选一下厨具
|
||||
</h2>
|
||||
<div>
|
||||
<ToolTag
|
||||
v-for="item, i in tools" :key="i"
|
||||
:active="curTool === item.name"
|
||||
@click="rStore.clickTool(item)"
|
||||
>
|
||||
<span v-if="item.emoji" class="inline-flex">
|
||||
{{ item.emoji }}
|
||||
</span>
|
||||
<span v-else-if="item.icon" class="inline-flex">
|
||||
<div :class="item.icon" />
|
||||
</span>
|
||||
<span class="inline-flex" m="l-1">{{ item.label || item.name }}</span>
|
||||
</ToolTag>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<Transition>
|
||||
<BasketButton ref="recipeBtnRef" :is-visible="isVisible" @click="show" />
|
||||
</Transition>
|
||||
|
||||
<button
|
||||
m-auto
|
||||
flex items-center justify-center
|
||||
class="rounded bg-yellow px-4 py-2 text-orange-900 font-black shadow hover:shadow-md active:shadow-inset"
|
||||
@click="cook()"
|
||||
>
|
||||
<div v-if="cooking" class="mr-2 inline-flex" i-svg-spinners:clock />
|
||||
<span>做黑暗料理 🥘</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="recipe-panel relative shadow transition"
|
||||
m="x-2 y-4" p="2"
|
||||
bg="gray-400/8"
|
||||
>
|
||||
<div text="xl" font="bold" p="1">
|
||||
「{{ aiRecipeInfo['名称'] }}」
|
||||
</div>
|
||||
|
||||
<div class="cook-recipes text-center" p="2">
|
||||
<img
|
||||
v-if="recipeImg"
|
||||
class="m-auto w-25 rounded shadow transition hover:shadow-md"
|
||||
:src="recipeImg"
|
||||
alt="recipes"
|
||||
>
|
||||
<div v-else class="m-auto h-25 w-25 rounded bg-gray shadow transition hover:shadow-md" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ aiRecipeInfo['介绍'] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -25,7 +25,7 @@ const filteredRecipes = computedAsync(async () => {
|
||||
|
||||
<template>
|
||||
<YlfIconButton
|
||||
absolute right-3 top-5
|
||||
absolute right-3 top-4
|
||||
class="icon-btn hover:text-yellow-400 !outline-none"
|
||||
text-xl
|
||||
title="切换" @click="openModal"
|
||||
|
||||
29
data/food.ts
29
data/food.ts
@@ -7,63 +7,78 @@ export const vegetable: StuffItem[] = [
|
||||
{
|
||||
name: '土豆',
|
||||
emoji: '🥔',
|
||||
en: 'potato',
|
||||
},
|
||||
{
|
||||
name: '胡萝卜',
|
||||
emoji: '🥕',
|
||||
en: 'carrot',
|
||||
},
|
||||
{
|
||||
name: '花菜',
|
||||
emoji: '🥦',
|
||||
en: 'cauliflower',
|
||||
},
|
||||
{
|
||||
name: '白萝卜',
|
||||
emoji: '🥣',
|
||||
en: 'radish',
|
||||
},
|
||||
{
|
||||
name: '西葫芦',
|
||||
emoji: '🥒',
|
||||
en: 'zucchini',
|
||||
},
|
||||
{
|
||||
name: '番茄',
|
||||
emoji: '🍅',
|
||||
alias: '西红柿',
|
||||
en: 'tomato',
|
||||
},
|
||||
{
|
||||
name: '芹菜',
|
||||
emoji: '🥬',
|
||||
en: 'celery',
|
||||
},
|
||||
{
|
||||
name: '黄瓜',
|
||||
emoji: '🥒',
|
||||
en: 'cucumber',
|
||||
},
|
||||
{
|
||||
name: '洋葱',
|
||||
emoji: '🧅',
|
||||
en: 'onion',
|
||||
},
|
||||
{
|
||||
name: '莴笋',
|
||||
emoji: '🎍',
|
||||
en: 'lettuce',
|
||||
},
|
||||
{
|
||||
name: '菌菇',
|
||||
emoji: '🍄',
|
||||
en: 'mushroom',
|
||||
},
|
||||
{
|
||||
name: '茄子',
|
||||
emoji: '🍆',
|
||||
en: 'eggplant',
|
||||
},
|
||||
{
|
||||
name: '豆腐',
|
||||
emoji: '🍲',
|
||||
en: 'tofu',
|
||||
},
|
||||
{
|
||||
name: '包菜',
|
||||
emoji: '🥗',
|
||||
en: 'cabbage',
|
||||
},
|
||||
{
|
||||
name: '白菜',
|
||||
emoji: '🥬',
|
||||
en: 'cabbage',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -74,42 +89,52 @@ export const meat: StuffItem[] = [
|
||||
{
|
||||
name: '午餐肉',
|
||||
emoji: '🥓',
|
||||
en: 'bacon',
|
||||
},
|
||||
{
|
||||
name: '香肠',
|
||||
emoji: '🌭',
|
||||
en: 'sausage',
|
||||
},
|
||||
{
|
||||
name: '腊肠',
|
||||
emoji: '🌭',
|
||||
en: 'sausage',
|
||||
},
|
||||
{
|
||||
name: '鸡肉',
|
||||
emoji: '🐤',
|
||||
en: 'chicken',
|
||||
},
|
||||
{
|
||||
name: '猪肉',
|
||||
emoji: '🐷',
|
||||
en: 'pork',
|
||||
},
|
||||
{
|
||||
name: '鸡蛋',
|
||||
emoji: '🥚',
|
||||
en: 'egg',
|
||||
},
|
||||
{
|
||||
name: '虾',
|
||||
emoji: '🦐',
|
||||
en: 'shrimp',
|
||||
},
|
||||
{
|
||||
name: '牛肉',
|
||||
emoji: '🐮',
|
||||
en: 'beef',
|
||||
},
|
||||
{
|
||||
name: '骨头',
|
||||
emoji: '🦴',
|
||||
en: 'bone',
|
||||
},
|
||||
{
|
||||
name: '鱼(Todo)',
|
||||
emoji: '🐟',
|
||||
en: 'fish',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -120,18 +145,22 @@ export const staple: StuffItem[] = [
|
||||
{
|
||||
name: '面食',
|
||||
emoji: '🍝',
|
||||
en: 'noodles',
|
||||
},
|
||||
{
|
||||
name: '面包',
|
||||
emoji: '🍞',
|
||||
en: 'bread',
|
||||
},
|
||||
{
|
||||
name: '米',
|
||||
emoji: '🍚',
|
||||
en: 'rice',
|
||||
},
|
||||
{
|
||||
name: '方便面',
|
||||
emoji: '🍜',
|
||||
en: 'instant noodles',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
6
layouts/preview.vue
Normal file
6
layouts/preview.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<main class="cook-main text-center text-gray-700 dark:text-gray-200" p="t-8 b-$cook-bottom-menu-height">
|
||||
<slot />
|
||||
<DarkToggle absolute left-3 top-4 />
|
||||
</main>
|
||||
</template>
|
||||
49
package.json
49
package.json
@@ -2,7 +2,7 @@
|
||||
"type": "module",
|
||||
"version": "1.2.2",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@8.14.3",
|
||||
"packageManager": "pnpm@8.15.5",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
@@ -28,45 +28,46 @@
|
||||
"vue-about-me": "^1.2.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^2.6.3",
|
||||
"@headlessui/vue": "^1.7.17",
|
||||
"@iconify-json/carbon": "^1.1.28",
|
||||
"@antfu/eslint-config": "^2.9.0",
|
||||
"@headlessui/vue": "^1.7.19",
|
||||
"@iconify-json/carbon": "^1.1.31",
|
||||
"@iconify-json/fe": "^1.1.10",
|
||||
"@iconify-json/gg": "^1.1.9",
|
||||
"@iconify-json/ic": "^1.1.17",
|
||||
"@iconify-json/mdi": "^1.1.64",
|
||||
"@iconify-json/ri": "^1.1.19",
|
||||
"@iconify-json/ri": "^1.1.20",
|
||||
"@iconify-json/svg-spinners": "^1.1.2",
|
||||
"@iconify-json/twemoji": "^1.1.15",
|
||||
"@nuxt/devtools": "^1.0.8",
|
||||
"@nuxt/test-utils": "^3.10.0",
|
||||
"@nuxtjs/color-mode": "^3.3.2",
|
||||
"@nuxt/devtools": "^1.1.3",
|
||||
"@nuxt/test-utils": "^3.12.0",
|
||||
"@nuxtjs/color-mode": "^3.3.3",
|
||||
"@pinia/nuxt": "^0.5.1",
|
||||
"@pinia/testing": "^0.1.3",
|
||||
"@unocss/eslint-config": "^0.58.4",
|
||||
"@unocss/nuxt": "^0.58.4",
|
||||
"@vite-pwa/nuxt": "^0.4.0",
|
||||
"@vue/test-utils": "^2.4.4",
|
||||
"@vueuse/nuxt": "^10.7.2",
|
||||
"@yunlefun/vue": "^0.0.9",
|
||||
"@unocss/eslint-config": "^0.58.6",
|
||||
"@unocss/nuxt": "^0.58.6",
|
||||
"@vite-pwa/nuxt": "^0.6.0",
|
||||
"@vue/test-utils": "^2.4.5",
|
||||
"@vueuse/nuxt": "^10.9.0",
|
||||
"@yunlefun/vue": "^0.1.1",
|
||||
"@zadigetvoltaire/nuxt-gtm": "^0.0.13",
|
||||
"bumpp": "^9.3.0",
|
||||
"bumpp": "^9.4.0",
|
||||
"consola": "^3.2.3",
|
||||
"dexie": "^3.2.4",
|
||||
"eslint": "^8.56.0",
|
||||
"dexie": "^3.2.7",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-format": "^0.1.0",
|
||||
"fake-indexeddb": "^5.0.2",
|
||||
"happy-dom": "^13.3.1",
|
||||
"happy-dom": "^14.3.1",
|
||||
"jsdom": "^24.0.0",
|
||||
"nuxt": "^3.9.3",
|
||||
"nuxt-vitest": "^0.11.5",
|
||||
"pinia": "^2.1.7",
|
||||
"sass": "^1.70.0",
|
||||
"sass": "^1.72.0",
|
||||
"serve": "^14.2.1",
|
||||
"star-markdown-css": "^0.4.2",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.3",
|
||||
"unocss": "^0.58.4",
|
||||
"vitest": "^1.2.2",
|
||||
"vue-tsc": "^1.8.27"
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^5.4.3",
|
||||
"unocss": "^0.58.6",
|
||||
"vitest": "^1.4.0",
|
||||
"vue-tsc": "^2.0.7"
|
||||
}
|
||||
}
|
||||
|
||||
1
packages/ai/.env.example
Normal file
1
packages/ai/.env.example
Normal file
@@ -0,0 +1 @@
|
||||
OPENAI_API_KEY=
|
||||
5
packages/ai/package.json
Normal file
5
packages/ai/package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"openai": "^4.29.2"
|
||||
}
|
||||
}
|
||||
197
packages/ai/pnpm-lock.yaml
generated
Normal file
197
packages/ai/pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,197 @@
|
||||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
devDependencies:
|
||||
openai:
|
||||
specifier: ^4.29.2
|
||||
version: 4.29.2
|
||||
|
||||
packages:
|
||||
|
||||
/@types/node-fetch@2.6.11:
|
||||
resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==}
|
||||
dependencies:
|
||||
'@types/node': 18.19.26
|
||||
form-data: 4.0.0
|
||||
dev: true
|
||||
|
||||
/@types/node@18.19.26:
|
||||
resolution: {integrity: sha512-+wiMJsIwLOYCvUqSdKTrfkS8mpTp+MPINe6+Np4TAGFWWRWiBQ5kSq9nZGCSPkzx9mvT+uEukzpX4MOSCydcvw==}
|
||||
dependencies:
|
||||
undici-types: 5.26.5
|
||||
dev: true
|
||||
|
||||
/abort-controller@3.0.0:
|
||||
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||
engines: {node: '>=6.5'}
|
||||
dependencies:
|
||||
event-target-shim: 5.0.1
|
||||
dev: true
|
||||
|
||||
/agentkeepalive@4.5.0:
|
||||
resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==}
|
||||
engines: {node: '>= 8.0.0'}
|
||||
dependencies:
|
||||
humanize-ms: 1.2.1
|
||||
dev: true
|
||||
|
||||
/asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
dev: true
|
||||
|
||||
/base-64@0.1.0:
|
||||
resolution: {integrity: sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==}
|
||||
dev: true
|
||||
|
||||
/charenc@0.0.2:
|
||||
resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==}
|
||||
dev: true
|
||||
|
||||
/combined-stream@1.0.8:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
dev: true
|
||||
|
||||
/crypt@0.0.2:
|
||||
resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==}
|
||||
dev: true
|
||||
|
||||
/delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
dev: true
|
||||
|
||||
/digest-fetch@1.3.0:
|
||||
resolution: {integrity: sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==}
|
||||
dependencies:
|
||||
base-64: 0.1.0
|
||||
md5: 2.3.0
|
||||
dev: true
|
||||
|
||||
/event-target-shim@5.0.1:
|
||||
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/form-data-encoder@1.7.2:
|
||||
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
|
||||
dev: true
|
||||
|
||||
/form-data@4.0.0:
|
||||
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
|
||||
engines: {node: '>= 6'}
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
mime-types: 2.1.35
|
||||
dev: true
|
||||
|
||||
/formdata-node@4.4.1:
|
||||
resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
|
||||
engines: {node: '>= 12.20'}
|
||||
dependencies:
|
||||
node-domexception: 1.0.0
|
||||
web-streams-polyfill: 4.0.0-beta.3
|
||||
dev: true
|
||||
|
||||
/humanize-ms@1.2.1:
|
||||
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
dev: true
|
||||
|
||||
/is-buffer@1.1.6:
|
||||
resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==}
|
||||
dev: true
|
||||
|
||||
/md5@2.3.0:
|
||||
resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==}
|
||||
dependencies:
|
||||
charenc: 0.0.2
|
||||
crypt: 0.0.2
|
||||
is-buffer: 1.1.6
|
||||
dev: true
|
||||
|
||||
/mime-db@1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/mime-types@2.1.35:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
dev: true
|
||||
|
||||
/ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
dev: true
|
||||
|
||||
/node-domexception@1.0.0:
|
||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||
engines: {node: '>=10.5.0'}
|
||||
dev: true
|
||||
|
||||
/node-fetch@2.7.0:
|
||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
peerDependencies:
|
||||
encoding: ^0.1.0
|
||||
peerDependenciesMeta:
|
||||
encoding:
|
||||
optional: true
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
dev: true
|
||||
|
||||
/openai@4.29.2:
|
||||
resolution: {integrity: sha512-cPkT6zjEcE4qU5OW/SoDDuXEsdOLrXlAORhzmaguj5xZSPlgKvLhi27sFWhLKj07Y6WKNWxcwIbzm512FzTBNQ==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@types/node': 18.19.26
|
||||
'@types/node-fetch': 2.6.11
|
||||
abort-controller: 3.0.0
|
||||
agentkeepalive: 4.5.0
|
||||
digest-fetch: 1.3.0
|
||||
form-data-encoder: 1.7.2
|
||||
formdata-node: 4.4.1
|
||||
node-fetch: 2.7.0
|
||||
web-streams-polyfill: 3.3.3
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: true
|
||||
|
||||
/tr46@0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
dev: true
|
||||
|
||||
/undici-types@5.26.5:
|
||||
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||
dev: true
|
||||
|
||||
/web-streams-polyfill@3.3.3:
|
||||
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||
engines: {node: '>= 8'}
|
||||
dev: true
|
||||
|
||||
/web-streams-polyfill@4.0.0-beta.3:
|
||||
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
|
||||
engines: {node: '>= 14'}
|
||||
dev: true
|
||||
|
||||
/webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
dev: true
|
||||
|
||||
/whatwg-url@5.0.0:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
dependencies:
|
||||
tr46: 0.0.3
|
||||
webidl-conversions: 3.0.1
|
||||
dev: true
|
||||
53
packages/ai/src/api.ts
Normal file
53
packages/ai/src/api.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import consola from 'consola'
|
||||
import type OpenAI from 'openai'
|
||||
import { baseChatCompletionCreateParams, baseModel, config, openai } from './config'
|
||||
|
||||
// TODO: pass params
|
||||
|
||||
export async function getCompletion(msg: string) {
|
||||
const chatCompletion = await openai.chat.completions.create({
|
||||
...baseChatCompletionCreateParams,
|
||||
messages: [{ role: 'user', content: msg }],
|
||||
model: baseModel,
|
||||
})
|
||||
|
||||
return chatCompletion.choices
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 ai 生成的菜谱信息
|
||||
*/
|
||||
export async function getAIRecipeInfo(zhFoods: string[]) {
|
||||
/**
|
||||
* 限制输入长度
|
||||
*/
|
||||
const promptFoods = zhFoods.join('、').slice(0, config.inputMaxLength)
|
||||
|
||||
// 尽可能少的 token
|
||||
const tooltip = `
|
||||
使用以下材料【${promptFoods}】做一道菜,请为这道菜起个名字,最好带有文化底蕴。
|
||||
不要使用生僻字和标点符号。
|
||||
并给出一个有趣的不超过100字的介绍。
|
||||
格式类型:{
|
||||
"名称": "",
|
||||
"介绍": ""
|
||||
}
|
||||
直接给出可以被 JSON.parse 解析的字符串,不需要解释内容。`
|
||||
|
||||
const messages: OpenAI.ChatCompletionMessageParam[] = [
|
||||
{
|
||||
role: 'system',
|
||||
content: tooltip,
|
||||
},
|
||||
]
|
||||
|
||||
const chatCompletion = await openai.chat.completions.create({
|
||||
...baseChatCompletionCreateParams,
|
||||
messages,
|
||||
model: baseModel,
|
||||
// stream: true
|
||||
})
|
||||
|
||||
consola.debug(chatCompletion)
|
||||
return chatCompletion.choices[0].message
|
||||
}
|
||||
26
packages/ai/src/config.ts
Normal file
26
packages/ai/src/config.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'dotenv/config'
|
||||
import process from 'node:process'
|
||||
import OpenAI from 'openai'
|
||||
|
||||
const deepseekApiUrl = 'https://api.deepseek.com/v1'
|
||||
|
||||
const aiServiceUrl = process.env.AI_SERVICE_URL || deepseekApiUrl
|
||||
|
||||
export const config = {
|
||||
inputMaxLength: 300,
|
||||
}
|
||||
|
||||
export const openai = new OpenAI({
|
||||
baseURL: aiServiceUrl,
|
||||
apiKey: process.env.OPENAI_API_KEY, // This is the default and can be omitted
|
||||
})
|
||||
|
||||
export const baseModel = process.env.MODEL_NAME || 'deepseek-chat'
|
||||
|
||||
export const baseChatCompletionCreateParams: Partial<OpenAI.ChatCompletionCreateParamsNonStreaming> = {
|
||||
max_tokens: 100,
|
||||
// TODO: for use control
|
||||
// presence_penalty: 0,
|
||||
// frequency_penalty: 0,
|
||||
// stream: true
|
||||
}
|
||||
3
packages/ai/src/index.ts
Normal file
3
packages/ai/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './api'
|
||||
export * from './config'
|
||||
export * from './types'
|
||||
7
packages/ai/src/types.ts
Normal file
7
packages/ai/src/types.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* AI 食谱
|
||||
*/
|
||||
export interface AIRecipeInfo {
|
||||
名称: string
|
||||
介绍: string
|
||||
}
|
||||
25
pages/ai.vue
Normal file
25
pages/ai.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script lang="ts" setup>
|
||||
definePageMeta({
|
||||
layout: 'preview',
|
||||
})
|
||||
|
||||
const rStore = useRecipeStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div text-4xl>
|
||||
<button
|
||||
class="cursor-pointer transition active:text-green-800 hover:(text-green-600)"
|
||||
title="重置"
|
||||
@click="rStore.reset"
|
||||
>
|
||||
<div v-if="rStore.selectedStuff.length" i-mdi-pot-steam-outline />
|
||||
<div v-else i-mdi-pot-mix-outline />
|
||||
</button>
|
||||
</div>
|
||||
<p text="sm" m="b-4">
|
||||
好的,今天我们来做菜!
|
||||
</p>
|
||||
|
||||
<AiChooseFood />
|
||||
</template>
|
||||
6091
pnpm-lock.yaml
generated
6091
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
- packages/*
|
||||
3
server/api/README.md
Normal file
3
server/api/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# SD API
|
||||
|
||||
- [SD WebUI API](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/API)
|
||||
74
server/api/recipes/image/generate.ts
Normal file
74
server/api/recipes/image/generate.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
// http://localhost:3001/api/recipes/image/generate
|
||||
|
||||
import process from 'node:process'
|
||||
import { meat, staple, vegetable } from '~/data/food'
|
||||
|
||||
// internal temp
|
||||
// const sdBaseUrl = 'http://30.30.168.63:7860/'
|
||||
const sdBaseUrl = process.env.SD_API_BASE_URL
|
||||
|
||||
const payload = {
|
||||
// denoising_strength: 0,
|
||||
prompt: 'food', // 提示词
|
||||
negative_prompt: '', // 反向提示词
|
||||
seed: -1, // 种子,随机数
|
||||
batch_size: 2, // 每次张数
|
||||
n_iter: 1, // 生成批次
|
||||
steps: 50, // 生成步数
|
||||
cfg_scale: 7, // 关键词相关性
|
||||
width: 512, // 宽度
|
||||
height: 512, // 高度
|
||||
restore_faces: false, // 脸部修复
|
||||
tiling: false, // 可平埔
|
||||
// override_settings: {
|
||||
// sd_model_checkpoint: 'wlop-any.ckpt [7331f3bc87]',
|
||||
// }, // 一般用于修改本次的生成图片的stable diffusion 模型,用法需保持一致
|
||||
// script_args: [
|
||||
// 0,
|
||||
// true,
|
||||
// true,
|
||||
// 'LoRA',
|
||||
// 'dingzhenlora_v1(fa7c1732cc95)',
|
||||
// 1,
|
||||
// 1,
|
||||
// ], // 一般用于lora模型或其他插件参数,如示例,我放入了一个lora模型, 1,1为两个权重值,一般只用到前面的权重值1
|
||||
sampler_index: 'Euler', // 采样方法
|
||||
}
|
||||
|
||||
// with api /sdapi/v1/txt2img
|
||||
|
||||
export interface Txt2ImgResponse {
|
||||
images: string[]
|
||||
}
|
||||
|
||||
const stuffItems = [
|
||||
...vegetable,
|
||||
...meat,
|
||||
...staple,
|
||||
]
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const body = await readBody(e)
|
||||
|
||||
const zhFoods = body.foods as string[]
|
||||
const enFoods = zhFoods.map((food) => {
|
||||
const item = stuffItems.find(item => item.name === food)
|
||||
if (item)
|
||||
return item.en
|
||||
return ''
|
||||
})
|
||||
|
||||
// TODO: 过滤 prompt 只能是食材
|
||||
// <lora:TODO:1>,
|
||||
payload.prompt = `food focus,transparent background,${enFoods.join(',')}`
|
||||
|
||||
console.log(payload.prompt)
|
||||
|
||||
const data = await $fetch<Txt2ImgResponse>('/sdapi/v1/txt2img', {
|
||||
baseURL: sdBaseUrl,
|
||||
body: payload,
|
||||
method: 'POST',
|
||||
})
|
||||
console.log(data)
|
||||
return data
|
||||
})
|
||||
40
server/api/recipes/text/generate.ts
Normal file
40
server/api/recipes/text/generate.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import consola from 'consola'
|
||||
import type { AIRecipeInfo } from '~/packages/ai/src'
|
||||
import { getAIRecipeInfo } from '~/packages/ai/src'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event)
|
||||
const zhFoods = body.foods as string[]
|
||||
|
||||
consola.debug(zhFoods)
|
||||
|
||||
const data = await getAIRecipeInfo(zhFoods)
|
||||
const { content } = data
|
||||
|
||||
let unWrapperContent = content || ''
|
||||
const startPos = unWrapperContent.indexOf('{')
|
||||
const endPos = unWrapperContent.lastIndexOf('}')
|
||||
|
||||
if (startPos === -1 || endPos === -1) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(content)
|
||||
return
|
||||
}
|
||||
|
||||
unWrapperContent = unWrapperContent.slice(startPos, endPos + 1)
|
||||
unWrapperContent = (unWrapperContent || '{}')?.replace('```json\n', '').replace('```', '')
|
||||
|
||||
unWrapperContent = unWrapperContent.endsWith('}') ? unWrapperContent : `${unWrapperContent}}`
|
||||
|
||||
let coupletData: AIRecipeInfo | undefined
|
||||
try {
|
||||
coupletData = JSON.parse(unWrapperContent) as AIRecipeInfo
|
||||
}
|
||||
catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(content)
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
return coupletData
|
||||
})
|
||||
7
server/api/recipes/text/test.ts
Normal file
7
server/api/recipes/text/test.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
const startAt = Date.now()
|
||||
let count = 0
|
||||
|
||||
export default defineEventHandler(() => ({
|
||||
pageview: count++,
|
||||
startAt,
|
||||
}))
|
||||
@@ -66,4 +66,9 @@ export interface StuffItem {
|
||||
* 显示标签
|
||||
*/
|
||||
label?: string
|
||||
/**
|
||||
* 英文名称, for ai keyword
|
||||
* @example 'potato'
|
||||
*/
|
||||
en?: string
|
||||
}
|
||||
|
||||
0
utils/api/ai.ts
Normal file
0
utils/api/ai.ts
Normal file
24
utils/api/index.ts
Normal file
24
utils/api/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// filter prompt
|
||||
export async function generateRecipeImage(foods: string[]) {
|
||||
return $fetch('/api/recipes/image/generate', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
foods,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function generateRecipeInfo(foods: string[]) {
|
||||
console.log(foods)
|
||||
return $fetch('/api/recipes/text/generate', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
foods,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function getRecipeImage(foods: string[]) {
|
||||
const data = await generateRecipeImage(foods)
|
||||
return `data:image/png;base64,${data.images[0]}`
|
||||
}
|
||||
Reference in New Issue
Block a user