backing_up

This commit is contained in:
2026-03-04 22:26:44 +00:00
parent 0d517b198d
commit 910b9bf4bc
1708 changed files with 11 additions and 685500 deletions

24
Seasoned.Frontend/.gitignore vendored Normal file
View 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

View File

@@ -0,0 +1 @@
telemetry.enabled=false

18
Seasoned.Frontend/LICENSE Normal file
View 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.

View 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.

View File

@@ -0,0 +1,14 @@
{
"folders": [
{
"path": "."
},
{
"path": "../Seasoned.Backend"
},
{
"path": "../Seasoned.Tests"
}
],
"settings": {}
}

View 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>

View 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

File diff suppressed because it is too large Load Diff

View 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"
}
}

View 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)
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,2 @@
User-Agent: *
Disallow:

View 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)
})
})

View 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"
}
]
}

View 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/],
},
},
},
})