ai impl
This commit is contained in:
parent
156e74450a
commit
90bbe4bfa4
2
.gitignore
vendored
2
.gitignore
vendored
@ -4,6 +4,8 @@ node_modules
|
|||||||
|
|
||||||
uploads
|
uploads
|
||||||
|
|
||||||
|
llm-models
|
||||||
|
|
||||||
# Output
|
# Output
|
||||||
.output
|
.output
|
||||||
.vercel
|
.vercel
|
||||||
|
|||||||
@ -13,7 +13,7 @@ services:
|
|||||||
- fbla26
|
- fbla26
|
||||||
fbla26:
|
fbla26:
|
||||||
image: git.marinodev.com/marinodev/fbla26_ci:latest
|
image: git.marinodev.com/marinodev/fbla26_ci:latest
|
||||||
restart: on-failure
|
restart: unless-stopped
|
||||||
env_file: ".env"
|
env_file: ".env"
|
||||||
ports:
|
ports:
|
||||||
- "${FBLA26_PORT}:3000"
|
- "${FBLA26_PORT}:3000"
|
||||||
@ -22,6 +22,29 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./.env:/srv/fbla26/.env
|
- ./.env:/srv/fbla26/.env
|
||||||
- /var/fbla26/uploads/:/srv/fbla26/uploads/
|
- /var/fbla26/uploads/:/srv/fbla26/uploads/
|
||||||
|
llama:
|
||||||
|
image: ghcr.io/ggml-org/llama.cpp:server
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file: ".env"
|
||||||
|
ports:
|
||||||
|
- "${LLAMA_PORT}:8080"
|
||||||
|
networks:
|
||||||
|
- fbla26
|
||||||
|
volumes:
|
||||||
|
- /home/drake/llama/models:/models:ro
|
||||||
|
command:
|
||||||
|
- -m
|
||||||
|
- /models/Qwen3VL-2B-Instruct-Q4_K_M.gguf
|
||||||
|
- --mmproj
|
||||||
|
- /models/mmproj-Qwen3VL-2B-Instruct-Q8_0.gguf
|
||||||
|
- --host
|
||||||
|
- 0.0.0.0
|
||||||
|
- --port
|
||||||
|
- "8080"
|
||||||
|
- -n
|
||||||
|
- "512"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
fbla26:
|
fbla26:
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
import { Separator } from '$lib/components/ui/separator';
|
import { Separator } from '$lib/components/ui/separator';
|
||||||
import TrashIcon from '@lucide/svelte/icons/trash';
|
import TrashIcon from '@lucide/svelte/icons/trash';
|
||||||
import ImageUpload from '$lib/components/custom/image-upload/image-upload.svelte';
|
import ImageUpload from '$lib/components/custom/image-upload/image-upload.svelte';
|
||||||
import { genDescription } from '$lib/db/items.remote';
|
import { approveDenyItem, genDescription } from '$lib/db/items.remote';
|
||||||
import { EMAIL_REGEX_STRING } from '$lib/consts';
|
import { EMAIL_REGEX_STRING } from '$lib/consts';
|
||||||
import type { Item } from '$lib/types/item.server';
|
import type { Item } from '$lib/types/item.server';
|
||||||
import { Textarea } from '$lib/components/ui/textarea';
|
import { Textarea } from '$lib/components/ui/textarea';
|
||||||
@ -26,9 +26,9 @@
|
|||||||
let description: string = $derived(item ? item.description : '');
|
let description: string = $derived(item ? item.description : '');
|
||||||
let isGenerating = $state(false);
|
let isGenerating = $state(false);
|
||||||
|
|
||||||
async function onSelect() {
|
async function onSelect(file: File) {
|
||||||
isGenerating = true;
|
isGenerating = true;
|
||||||
description = await genDescription();
|
description = await genDescription(file);
|
||||||
isGenerating = false;
|
isGenerating = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,13 +110,23 @@
|
|||||||
</Field.Group>
|
</Field.Group>
|
||||||
|
|
||||||
<Dialog.Footer class="mt-4">
|
<Dialog.Footer class="mt-4">
|
||||||
<Dialog.Close
|
<div class="flex justify-between w-full">
|
||||||
type="button"
|
<Button variant="destructive"
|
||||||
class={buttonVariants({ variant: "outline" })}
|
onclick={() => {approveDenyItem({id: item?.id || 0, approved: false}); invalidateAll();}}>
|
||||||
>
|
Delete
|
||||||
Cancel
|
</Button>
|
||||||
</Dialog.Close>
|
|
||||||
<Button type="submit">Submit</Button>
|
<div>
|
||||||
|
|
||||||
|
<Dialog.Close
|
||||||
|
type="button"
|
||||||
|
class={buttonVariants({ variant: "outline" })}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Dialog.Close>
|
||||||
|
<Button type="submit" class="ml-1">Submit</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Dialog.Footer>
|
</Dialog.Footer>
|
||||||
</form>
|
</form>
|
||||||
{#if item?.threads}
|
{#if item?.threads}
|
||||||
|
|||||||
@ -14,7 +14,6 @@
|
|||||||
export let previewUrl: string | null = null;
|
export let previewUrl: string | null = null;
|
||||||
let dragging = false;
|
let dragging = false;
|
||||||
|
|
||||||
console.log(previewUrl);
|
|
||||||
|
|
||||||
function openFileDialog() {
|
function openFileDialog() {
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
@ -69,6 +68,7 @@
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
capture="environment"
|
capture="environment"
|
||||||
on:change={onInputChange}
|
on:change={onInputChange}
|
||||||
|
formaction="/item?/describe"
|
||||||
hidden
|
hidden
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -31,7 +31,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="h-full bg-card text-card-foreground flex flex-col gap-2 rounded-xl border shadow-sm max-w-sm overflow-hidden min-2-3xs">
|
class="h-full bg-card text-card-foreground flex flex-col gap-2 rounded-xl border shadow-sm max-w-sm overflow-hidden min-w-2xs">
|
||||||
{#if item.image}
|
{#if item.image}
|
||||||
<img src="https://fbla26.marinodev.com/uploads/{item.id}.jpg" class="object-cover min-h-48 max-h-48"
|
<img src="https://fbla26.marinodev.com/uploads/{item.id}.jpg" class="object-cover min-h-48 max-h-48"
|
||||||
alt="Lost item">
|
alt="Lost item">
|
||||||
|
|||||||
@ -16,9 +16,15 @@
|
|||||||
let description: string | undefined = $state();
|
let description: string | undefined = $state();
|
||||||
let isGenerating = $state(false);
|
let isGenerating = $state(false);
|
||||||
|
|
||||||
async function onSelect() {
|
async function onSelect(file: File) {
|
||||||
isGenerating = true;
|
isGenerating = true;
|
||||||
description = await genDescription();
|
|
||||||
|
const buffer = await file.arrayBuffer();
|
||||||
|
const base64 = btoa(
|
||||||
|
String.fromCharCode(...new Uint8Array(buffer))
|
||||||
|
);
|
||||||
|
|
||||||
|
description = await genDescription(base64);
|
||||||
isGenerating = false;
|
isGenerating = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,31 @@
|
|||||||
import { command, getRequestEvent, query } from '$app/server';
|
import { command, getRequestEvent } from '$app/server';
|
||||||
import * as v from 'valibot';
|
import * as v from 'valibot';
|
||||||
import sql from '$lib/db/db.server';
|
import sql from '$lib/db/db.server';
|
||||||
import { verifyJWT } from '$lib/auth/index.server';
|
import { verifyJWT } from '$lib/auth/index.server';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
import { LLMDescribe } from '$lib/llm/llm.server';
|
||||||
|
|
||||||
export const genDescription = query(async () => {
|
export const genDescription = command(v.string(), async (data) => {
|
||||||
await new Promise((f) => setTimeout(f, 1000));
|
const inputBuffer = Buffer.from(data, 'base64');
|
||||||
|
|
||||||
return 'A matte black water bottle with a black lid and a "BKLYN BENTO" logo on the side, resting on a tree trunk in a forest.';
|
// Convert File → Buffer
|
||||||
|
// const inputBuffer = Buffer.from(await file.arrayBuffer());
|
||||||
|
|
||||||
|
// Detect format (Sharp does this internally)
|
||||||
|
const image = sharp(inputBuffer).rotate();
|
||||||
|
|
||||||
|
const outputBuffer = await image
|
||||||
|
.rotate()
|
||||||
|
.resize({ fit: 'outside', height: 400, width: 400 })
|
||||||
|
.jpeg({ quality: 80 }) // adjust if needed
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
const description = await LLMDescribe(
|
||||||
|
`data:image/jpeg;base64,${outputBuffer.toString('base64')}`
|
||||||
|
);
|
||||||
|
console.log(description);
|
||||||
|
|
||||||
|
return description;
|
||||||
});
|
});
|
||||||
|
|
||||||
export const approveDenyItem = command(
|
export const approveDenyItem = command(
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import nodemailer from 'nodemailer';
|
import nodemailer from 'nodemailer';
|
||||||
|
|
||||||
// Create a transporter object using SMTP transport
|
// Create a transporter object using SMTP transport
|
||||||
const transporter = nodemailer.createTransport({
|
export const transporter = nodemailer.createTransport({
|
||||||
host: process.env.EMAIL_HOST,
|
host: process.env.EMAIL_HOST,
|
||||||
port: Number(process.env.EMAIL_PORT),
|
port: Number(process.env.EMAIL_PORT),
|
||||||
secure: true, // true for 465, false for other ports
|
secure: true, // true for 465, false for other ports
|
||||||
@ -11,13 +11,13 @@ const transporter = nodemailer.createTransport({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function sendEmployerNotificationEmail() {
|
// export async function sendEmail() {
|
||||||
// Send mail with defined transport object
|
// // Send mail with defined transport object
|
||||||
await transporter.sendMail({
|
// await transporter.sendMail({
|
||||||
from: `CareerConnect Notifications <${process.env.EMAIL_USER}>`,
|
// from: `Westuffind Notifier <${process.env.EMAIL_USER}>`,
|
||||||
// to: info.emails.join(', '), // EMAILING OF REAL COMPANIES DISABLED, UNCOMMENT TO ENABLE
|
// // to: info.emails.join(', '), // EMAILING OF REAL COMPANIES DISABLED, UNCOMMENT TO ENABLE
|
||||||
to: 'drake@marinodev.com', // TEMPORARY EMAIL FOR TESTING
|
// to: 'drake@marinodev.com', // TEMPORARY EMAIL FOR TESTING
|
||||||
subject: 'New Application Received!',
|
// subject: 'New Application Received!',
|
||||||
text: `A new application has been received for the posting ''!\n\nCheck it out at ${process.env.BASE_URL}/`
|
// text: `A new application has been received for the posting ''!\n\nCheck it out at ${process.env.BASE_URL}/`
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|||||||
44
src/lib/llm/llm.server.ts
Normal file
44
src/lib/llm/llm.server.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
export async function LLMDescribe(imageData: string) {
|
||||||
|
const payload = {
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'Describe only the main object in this image in one sentence. Include its color, shape, brand, or any text on it. Focus solely on the object itself. Example: A blue Hydroflask water bottle with a dented lid and a sticker of a mountain on the side.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'image_url',
|
||||||
|
image_url: {
|
||||||
|
url: imageData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
max_tokens: 128,
|
||||||
|
temperature: 0.2
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('AIing it');
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`http://${process.env.LLAMA_HOST!}:${process.env.LLAMA_PORT!}/v1/chat/completions`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error(await res.text());
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
return data.choices[0].message.content;
|
||||||
|
}
|
||||||
@ -8,7 +8,7 @@ import type { Item } from '$lib/types/item.server';
|
|||||||
import { getFormString, getRequiredFormString } from '$lib/shared';
|
import { getFormString, getRequiredFormString } from '$lib/shared';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ url, locals }) => {
|
export const load: PageServerLoad = async ({ url, locals }) => {
|
||||||
const searchQuery = url.searchParams.get('search') as string;
|
const searchQuery = url.searchParams.get('search');
|
||||||
|
|
||||||
// If the user is logged in, fetch items together with their threads (each thread contains its first message)
|
// If the user is logged in, fetch items together with their threads (each thread contains its first message)
|
||||||
if (locals && locals.user) {
|
if (locals && locals.user) {
|
||||||
@ -33,6 +33,7 @@ export const load: PageServerLoad = async ({ url, locals }) => {
|
|||||||
FROM inquiry_messages im
|
FROM inquiry_messages im
|
||||||
WHERE im.thread_id = t.id
|
WHERE im.thread_id = t.id
|
||||||
) m ON TRUE
|
) m ON TRUE
|
||||||
|
${searchQuery ? sql`WHERE word_similarity(${searchQuery}, i.description) > 0.3` : sql``}
|
||||||
GROUP BY i.id;
|
GROUP BY i.id;
|
||||||
`;
|
`;
|
||||||
items.forEach((item) =>
|
items.forEach((item) =>
|
||||||
@ -265,7 +266,6 @@ export const actions: Actions = {
|
|||||||
|
|
||||||
let outputBuffer: Buffer | undefined;
|
let outputBuffer: Buffer | undefined;
|
||||||
if (file) {
|
if (file) {
|
||||||
console.log(file);
|
|
||||||
if (!(file instanceof File) || file.size === 0) {
|
if (!(file instanceof File) || file.size === 0) {
|
||||||
return fail(400, { message: 'No file uploaded or file is invalid', success: false });
|
return fail(400, { message: 'No file uploaded or file is invalid', success: false });
|
||||||
}
|
}
|
||||||
@ -315,7 +315,6 @@ export const actions: Actions = {
|
|||||||
inquire: async ({ request, url }) => {
|
inquire: async ({ request, url }) => {
|
||||||
const id = url.searchParams.get('id');
|
const id = url.searchParams.get('id');
|
||||||
if (!id) {
|
if (!id) {
|
||||||
console.log('No id provided!');
|
|
||||||
return fail(400, { message: 'Missing id!', success: false });
|
return fail(400, { message: 'Missing id!', success: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -370,4 +369,29 @@ export const actions: Actions = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// describe: async ({ request }) => {
|
||||||
|
// const data = await request.formData();
|
||||||
|
//
|
||||||
|
// let image = data.get('image');
|
||||||
|
// if (!(image instanceof File) || image.size === 0) {
|
||||||
|
// return fail(400, { message: 'No file uploaded or file is invalid', success: false });
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Convert File → Buffer
|
||||||
|
// const inputBuffer = Buffer.from(await file.arrayBuffer());
|
||||||
|
//
|
||||||
|
// // Detect format (Sharp does this internally)
|
||||||
|
// const image = sharp(inputBuffer).rotate();
|
||||||
|
//
|
||||||
|
// const outputBuffer = await image
|
||||||
|
// .rotate()
|
||||||
|
// .resize({ fit: 'outside', height: 400, width: 400 })
|
||||||
|
// .jpeg({ quality: 90 }) // adjust if needed
|
||||||
|
// .toBuffer();
|
||||||
|
//
|
||||||
|
// const description = LLMDescribe(`data:image/jpeg;base64,${outputBuffer.toString('base64')}`);
|
||||||
|
// console.log(description);
|
||||||
|
//
|
||||||
|
// return description;
|
||||||
|
// }
|
||||||
} satisfies Actions;
|
} satisfies Actions;
|
||||||
|
|||||||
@ -70,39 +70,59 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-4 grid-cols-[repeat(auto-fill,minmax(16rem,max-content))] justify-center">
|
{#if data.items.length === 0}
|
||||||
|
|
||||||
{#if data.user && data.items.some(
|
<h1 class="text-xl text-center font-semibold mt-16">No items found!</h1>
|
||||||
(item) => item.approvedDate === null
|
<p class="text-center">Try broadening your search terms to get more results.</p>
|
||||||
)}
|
{:else}
|
||||||
<FieldSeparator class="col-span-full text-lg h-8 my-2">
|
<div class="grid gap-4 grid-cols-[repeat(auto-fill,minmax(18rem,max-content))] justify-center">
|
||||||
Pending items
|
|
||||||
</FieldSeparator>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#each data.items as item (item.id)}
|
{#if data.user && data.items.some(
|
||||||
{#if item.approvedDate === null}
|
(item) => item.approvedDate === null
|
||||||
<ItemListing item={item} admin={data.user !== null} editCallback={openEditDialog}
|
)}
|
||||||
inquireCallback={openInquireDialog} claimCallback={openClaimDialog} />
|
<FieldSeparator class="col-span-full text-lg h-8 my-2">
|
||||||
|
Pending items
|
||||||
|
</FieldSeparator>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
|
||||||
|
|
||||||
{#if data.user && data.items.some(
|
{#each data.items as item (item.id)}
|
||||||
(item) => item.approvedDate !== null
|
{#if item.approvedDate === null}
|
||||||
)}
|
<ItemListing item={item} admin={data.user !== null} editCallback={openEditDialog}
|
||||||
<FieldSeparator class="col-span-full text-lg my-2 h-8 w-full">
|
inquireCallback={openInquireDialog} claimCallback={openClaimDialog} />
|
||||||
Public items
|
{/if}
|
||||||
</FieldSeparator>
|
{/each}
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#each data.items as item (item.id)}
|
{#if data.user && data.items.some(
|
||||||
{#if item.approvedDate !== null}
|
(item) => item.approvedDate !== null && item.claimedDate === null
|
||||||
<ItemListing item={item} admin={data.user !== null} editCallback={openEditDialog}
|
)}
|
||||||
inquireCallback={openInquireDialog} claimCallback={openClaimDialog} />
|
<FieldSeparator class="col-span-full text-lg my-2 h-8 w-full">
|
||||||
|
Public items
|
||||||
|
</FieldSeparator>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
|
||||||
|
|
||||||
</div>
|
{#each data.items as item (item.id)}
|
||||||
|
{#if item.approvedDate !== null}
|
||||||
|
<ItemListing item={item} admin={data.user !== null} editCallback={openEditDialog}
|
||||||
|
inquireCallback={openInquireDialog} claimCallback={openClaimDialog} />
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if data.user && data.items.some(
|
||||||
|
(item) => item.claimedDate !== null
|
||||||
|
)}
|
||||||
|
<FieldSeparator class="col-span-full text-lg my-2 h-8 w-full">
|
||||||
|
Claimed items
|
||||||
|
</FieldSeparator>
|
||||||
|
{#each data.items as item (item.id)}
|
||||||
|
{#if item.claimedDate !== null}
|
||||||
|
<ItemListing item={item} admin={data.user !== null} editCallback={openEditDialog}
|
||||||
|
inquireCallback={openInquireDialog} claimCallback={openClaimDialog} />
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button class="fixed shadow-lg bottom-6 right-6 rounded-xl md:hidden p-6 text-xl" size="default"
|
<Button class="fixed shadow-lg bottom-6 right-6 rounded-xl md:hidden p-6 text-xl" size="default"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user