backing_up
This commit is contained in:
24
Seasoned.Frontend/.gitignore
vendored
Normal file
24
Seasoned.Frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
1
Seasoned.Frontend/.nuxtrc
Normal file
1
Seasoned.Frontend/.nuxtrc
Normal file
@@ -0,0 +1 @@
|
||||
telemetry.enabled=false
|
||||
18
Seasoned.Frontend/LICENSE
Normal file
18
Seasoned.Frontend/LICENSE
Normal file
@@ -0,0 +1,18 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 chloe
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
59
Seasoned.Frontend/README.md
Normal file
59
Seasoned.Frontend/README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Seasoned
|
||||
|
||||
The Pitch:
|
||||
|
||||
Seasoned is a high-performance, private digital cookbook that bridges the gap between web discovery and kitchen execution. By combining the multimodal power of Gemini 1.5 Flash with a secure, self-hosted PostgreSQL backbone, Seasoned allows users to instantly "distill" messy recipe blogs and food photos into a standardized, searchable, and shareable library they truly own.
|
||||
|
||||
Target Audience:
|
||||
|
||||
The Modern Minimalist: Cooks who want an ad-free, recipe experience.
|
||||
|
||||
The Legacy Keeper: Families digitizing handwritten recipes into a clean digital format.
|
||||
|
||||
The Privacy Enthusiast: Users who want the power of AI without storing their personal data in a massive cloud.
|
||||
|
||||
The Hybrid Tech Stack:
|
||||
|
||||
| Components | Technology |
|
||||
| :--- | :--- |
|
||||
| **Hosting** | Private Server (Dockerized on home hardware) |
|
||||
| **CI/CD** | Jenkins server |
|
||||
| **Frontend** | Nuxt 4 + Vuetify + CSS |
|
||||
| **Backend** | Nuxt Nitro |
|
||||
| **Database** | Postgres + pgvector |
|
||||
| **Intelligence** | Gemini 2.5 Flash |
|
||||
| **Storage** | Local File System |
|
||||
|
||||
Technical Requirements:
|
||||
|
||||
1. AI & Multimodal Intelligence
|
||||
|
||||
Multimodal Extraction: Use Gemini 1.5 Flash to accept image/jpeg inputs and return a strictly validated JSON Schema containing title, ingredients, and steps.
|
||||
|
||||
Semantic Search: Implement pgvector in the local database. Recipes will be converted into "embeddings" (via Gemini) to allow users to search for "Comfort food for a rainy day" instead of just keyword matches.
|
||||
|
||||
2. Full-Stack Architecture (Nuxt 4)
|
||||
|
||||
Directory Structure: Adherence to the new app/ directory standard for better IDE performance and separation of concerns.
|
||||
|
||||
Serverless-Style Routes: Use Nitro server routes to keep the Gemini API Key hidden from the client-side.
|
||||
|
||||
Responsive Design: A UI that adapts perfectly to a tablet propped up on a kitchen counter.
|
||||
|
||||
3. Data & Storage
|
||||
|
||||
Relational Schema: A PostgreSQL database to manage Users, Recipes, Tags, and Shares.
|
||||
|
||||
Private Media Pipeline: A custom upload handler that saves images to a local Docker volume, served via a secured static asset route.
|
||||
|
||||
Sharing Permissions: A relational join-table logic that allows one user to "push" a recipe to another user's library.
|
||||
|
||||
Use Cases:
|
||||
|
||||
Photo-to-Recipe: User snaps a picture of a magazine page; Gemini extracts the text; the user saves it to their Postgres DB.
|
||||
|
||||
Semantic Discovery: User searches for "High protein dinner with lime" and the app uses vector similarity to find the best match.
|
||||
|
||||
Ad-Free Web Scraping: User pastes a blog URL; the server fetches the content, and Gemini strips out the ads and life stories.
|
||||
|
||||
Collaborative Boxes: One user "seasons" a recipe (rates/tags it) and shares it with someone who also uses the instance.
|
||||
14
Seasoned.Frontend/Seasoned.code-workspace
Normal file
14
Seasoned.Frontend/Seasoned.code-workspace
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "../Seasoned.Backend"
|
||||
},
|
||||
{
|
||||
"path": "../Seasoned.Tests"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
110
Seasoned.Frontend/app/app.vue
Normal file
110
Seasoned.Frontend/app/app.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-main>
|
||||
<v-container>
|
||||
<v-card class="pa-5 mx-auto mt-10" max-width="500" elevation="10">
|
||||
<v-card-title class="text-center">Seasoned AI</v-card-title>
|
||||
|
||||
<v-divider class="my-3"></v-divider>
|
||||
|
||||
<v-file-input
|
||||
v-model="files"
|
||||
label="Pick a recipe photo"
|
||||
prepend-icon="mdi-camera"
|
||||
variant="outlined"
|
||||
accept="image/*"
|
||||
></v-file-input>
|
||||
|
||||
<v-btn
|
||||
color="primary"
|
||||
block
|
||||
size="x-large"
|
||||
:loading="loading"
|
||||
@click="uploadImage"
|
||||
>
|
||||
Analyze Recipe
|
||||
</v-btn>
|
||||
|
||||
<div v-if="recipe" class="mt-5">
|
||||
<h2 class="text-h4 mb-4">{{ recipe.title }}</h2>
|
||||
<p class="text-subtitle-1 mb-6 text-grey-darken-1">{{ recipe.description }}</p>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="5">
|
||||
<h3 class="text-h6 mb-2">Ingredients</h3>
|
||||
<v-list lines="one" variant="flat" class="bg-grey-lighten-4 rounded-lg">
|
||||
<v-list-item v-for="(item, i) in recipe.ingredients" :key="i">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-circle-small"></v-icon>
|
||||
</template>
|
||||
{{ item }}
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="7">
|
||||
<h3 class="text-h6 mb-2">Instructions</h3>
|
||||
<v-timeline side="end" align="start" density="compact">
|
||||
<v-timeline-item
|
||||
v-for="(step, i) in recipe.instructions"
|
||||
:key="i"
|
||||
dot-color="primary"
|
||||
size="x-small"
|
||||
>
|
||||
<div class="text-body-1">{{ step }}</div>
|
||||
</v-timeline-item>
|
||||
</v-timeline>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import axios from 'axios'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const files = ref([])
|
||||
const loading = ref(false)
|
||||
const recipe = ref(null)
|
||||
|
||||
const uploadImage = async () => {
|
||||
// 1. Debug: Check what Vuetify is actually giving us
|
||||
console.log("Files variable:", files.value);
|
||||
|
||||
// Vuetify 3 v-file-input can return a single File or an Array of Files
|
||||
// We need to ensure we have the actual File object
|
||||
const fileToUpload = Array.isArray(files.value) ? files.value[0] : files.value;
|
||||
|
||||
if (!fileToUpload) {
|
||||
alert("Please select a file first!");
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const formData = new FormData();
|
||||
|
||||
// 2. Append the file. The string 'image' MUST match your C# parameter name
|
||||
formData.append('image', fileToUpload);
|
||||
|
||||
try {
|
||||
// 3. Post with explicit multipart/form-data header (Axios usually does this, but let's be sure)
|
||||
const response = await axios.post('http://localhost:5000/api/recipe/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
|
||||
recipe.value = response.data;
|
||||
console.log("Success:", response.data);
|
||||
} catch (error) {
|
||||
console.error("Detailed Error:", error.response?.data || error.message);
|
||||
alert("Backend error: Check the browser console for details.");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
38
Seasoned.Frontend/nuxt.config.ts
Normal file
38
Seasoned.Frontend/nuxt.config.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2025-07-15',
|
||||
|
||||
devtools: { enabled: true },
|
||||
|
||||
future: {
|
||||
compatibilityVersion: 4,
|
||||
},
|
||||
|
||||
srcDir: 'app/',
|
||||
|
||||
css: [
|
||||
'vuetify/lib/styles/main.sass',
|
||||
'@mdi/font/css/materialdesignicons.min.css',
|
||||
],
|
||||
|
||||
build: {
|
||||
transpile: ['vuetify'],
|
||||
},
|
||||
|
||||
modules: [
|
||||
'vuetify-nuxt-module'
|
||||
],
|
||||
|
||||
runtimeConfig: {
|
||||
geminiApiKey: '',
|
||||
},
|
||||
|
||||
vite: {
|
||||
server: {
|
||||
hmr: {
|
||||
protocol: 'ws',
|
||||
host: 'localhost',
|
||||
port: 3000
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
12278
Seasoned.Frontend/package-lock.json
generated
Normal file
12278
Seasoned.Frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
Seasoned.Frontend/package.json
Normal file
35
Seasoned.Frontend/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "Seasoned",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/generative-ai": "^0.24.1",
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@prisma/client": "^7.4.2",
|
||||
"axios": "^1.13.6",
|
||||
"dotenv": "^17.3.1",
|
||||
"nuxt": "^4.1.3",
|
||||
"prisma": "^6.19.2",
|
||||
"sass": "^1.97.3",
|
||||
"vue": "^3.5.29",
|
||||
"vue-router": "^4.6.4",
|
||||
"vuetify": "^4.0.1",
|
||||
"vuetify-nuxt-module": "^0.19.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.3.3",
|
||||
"@vitejs/plugin-vue": "^6.0.4",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"happy-dom": "^20.8.3",
|
||||
"jsdom": "^28.1.0",
|
||||
"vitest": "^4.0.18"
|
||||
}
|
||||
}
|
||||
14
Seasoned.Frontend/plugins/vuetify.ts
Normal file
14
Seasoned.Frontend/plugins/vuetify.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// @ts-nocheck
|
||||
import { createVuetify } from 'vuetify'
|
||||
import * as components from 'vuetify/components'
|
||||
import * as directives from 'vuetify/directives'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
const vuetify = createVuetify({
|
||||
ssr: true,
|
||||
components,
|
||||
directives,
|
||||
})
|
||||
|
||||
nuxtApp.vueApp.use(vuetify)
|
||||
})
|
||||
BIN
Seasoned.Frontend/public/favicon.ico
Normal file
BIN
Seasoned.Frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
2
Seasoned.Frontend/public/robots.txt
Normal file
2
Seasoned.Frontend/public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-Agent: *
|
||||
Disallow:
|
||||
8
Seasoned.Frontend/test/App.spec.ts
Normal file
8
Seasoned.Frontend/test/App.spec.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
describe('Frontend Setup', () => {
|
||||
it('checks that 1 + 1 is 2', () => {
|
||||
expect(1 + 1).toBe(2)
|
||||
})
|
||||
})
|
||||
18
Seasoned.Frontend/tsconfig.json
Normal file
18
Seasoned.Frontend/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.server.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.shared.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
16
Seasoned.Frontend/vitest.config.ts
Normal file
16
Seasoned.Frontend/vitest.config.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// vitest.config.ts
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
server: {
|
||||
deps: {
|
||||
inline: [/@exodus\/bytes/, /html-encoding-sniffer/],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user