dev
This commit is contained in:
@@ -3,7 +3,7 @@ import bcrypt from 'bcrypt';
|
||||
import sql from '$lib/db/db.server';
|
||||
import { type Cookies, error } from '@sveltejs/kit';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import type { inquiryTokenPayload } from '$lib/types/inquiries.server';
|
||||
import type { inquiryTokenPayload } from '$lib/types/inquiries';
|
||||
|
||||
export function setJWTCookie(cookies: Cookies, user: User) {
|
||||
const payload = {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import * as RadioGroup from '$lib/components/ui/radio-group';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import * as Field from '$lib/components/ui/field';
|
||||
import type { Item } from '$lib/types/item.server';
|
||||
import type { Item } from '$lib/types/item';
|
||||
import NoImagePlaceholder from './no-image-placeholder.svelte';
|
||||
import LocationIcon from '@lucide/svelte/icons/map-pinned';
|
||||
import { EMAIL_REGEX_STRING } from '$lib/consts';
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import ImageUpload from '$lib/components/custom/image-upload/image-upload.svelte';
|
||||
import { approveDenyItem, genDescription } from '$lib/db/items.remote';
|
||||
import { EMAIL_REGEX_STRING } from '$lib/consts';
|
||||
import type { Item } from '$lib/types/item.server';
|
||||
import type { Item } from '$lib/types/item';
|
||||
import { Textarea } from '$lib/components/ui/textarea';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { dateFormatOptions } from '$lib/shared';
|
||||
@@ -28,7 +28,13 @@
|
||||
|
||||
async function onSelect(file: File) {
|
||||
isGenerating = true;
|
||||
description = await genDescription(file);
|
||||
|
||||
const buffer = await file.arrayBuffer();
|
||||
const base64 = btoa(
|
||||
String.fromCharCode(...new Uint8Array(buffer))
|
||||
);
|
||||
|
||||
description = await genDescription(base64);
|
||||
isGenerating = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,17 +3,25 @@
|
||||
import X from '@lucide/svelte/icons/x';
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
export let name = 'image';
|
||||
export let required = false;
|
||||
export let disabled = false;
|
||||
export let onSelect: ((file: File) => void) | null = null;
|
||||
export let canRemove = !required;
|
||||
let {
|
||||
name = 'image',
|
||||
required = false,
|
||||
disabled = false,
|
||||
onSelect = null,
|
||||
previewUrl = null
|
||||
}: {
|
||||
name?: string;
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
onSelect?: ((file: File) => void) | null;
|
||||
previewUrl?: string | null;
|
||||
} = $props();
|
||||
|
||||
let inputEl = $state<HTMLInputElement | null>(null);
|
||||
let dragging = $state(false);
|
||||
|
||||
let inputEl: HTMLInputElement | null = null;
|
||||
export let previewUrl: string | null = null;
|
||||
let dragging = false;
|
||||
|
||||
// derived value instead of initializing from required
|
||||
let canRemove = $derived(!required);
|
||||
|
||||
function openFileDialog() {
|
||||
if (!disabled) {
|
||||
@@ -22,6 +30,7 @@
|
||||
}
|
||||
|
||||
function handleFiles(files: FileList | null) {
|
||||
console.log('handleFiles');
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const selected = files[0];
|
||||
@@ -30,7 +39,6 @@
|
||||
cleanupPreview();
|
||||
previewUrl = URL.createObjectURL(selected);
|
||||
|
||||
// Trigger callback
|
||||
if (onSelect) onSelect(selected);
|
||||
}
|
||||
|
||||
@@ -56,9 +64,7 @@
|
||||
onDestroy(cleanupPreview);
|
||||
</script>
|
||||
|
||||
|
||||
<div>
|
||||
|
||||
<input
|
||||
bind:this={inputEl}
|
||||
type="file"
|
||||
@@ -67,7 +73,7 @@
|
||||
required={required}
|
||||
disabled={disabled}
|
||||
capture="environment"
|
||||
on:change={onInputChange}
|
||||
onchange={onInputChange}
|
||||
formaction="/item?/describe"
|
||||
hidden
|
||||
/>
|
||||
@@ -76,10 +82,10 @@
|
||||
class="dropzone"
|
||||
class:has-image={!!previewUrl}
|
||||
class:dragging={dragging}
|
||||
on:click={openFileDialog}
|
||||
on:dragover|preventDefault={() => !disabled && (dragging = true)}
|
||||
on:dragleave={() => (dragging = false)}
|
||||
on:drop={onDrop}
|
||||
onclick={openFileDialog}
|
||||
ondragover={(e) => {e.preventDefault(); dragging = !disabled;}}
|
||||
ondragleave={() => (dragging = false)}
|
||||
ondrop={onDrop}
|
||||
type="button"
|
||||
>
|
||||
{#if previewUrl}
|
||||
@@ -92,12 +98,20 @@
|
||||
{:else}
|
||||
<div class="placeholder py-4">
|
||||
<ImagePlus size={32} />
|
||||
<p>Click or drag an image here <span class="text-error">{required ? '*' : ''}</span></p>
|
||||
<p>
|
||||
Click or drag an image here
|
||||
<span class="text-error">{required ? '*' : ''}</span>
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if previewUrl && canRemove}
|
||||
<button class="hover:text-destructive p-2" on:click={cleanupPreview} type="button">
|
||||
<button
|
||||
class="hover:text-destructive p-2"
|
||||
onclick={cleanupPreview}
|
||||
type="button"
|
||||
>
|
||||
<X size={24} class="inline" />
|
||||
<span class="inline align-middle">Remove image</span>
|
||||
</button>
|
||||
@@ -110,10 +124,8 @@
|
||||
width: 100%;
|
||||
max-height: 200px;
|
||||
min-height: 80px;
|
||||
/*height: 200px;*/
|
||||
border-radius: 12px;
|
||||
border: 2px dashed var(--border);
|
||||
/*background: var(--bg, #fafafa);*/
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import * as RadioGroup from '$lib/components/ui/radio-group';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import * as Field from '$lib/components/ui/field';
|
||||
import type { Item } from '$lib/types/item.server';
|
||||
import type { Item } from '$lib/types/item';
|
||||
import NoImagePlaceholder from './no-image-placeholder.svelte';
|
||||
import LocationIcon from '@lucide/svelte/icons/map-pinned';
|
||||
import { EMAIL_REGEX_STRING } from '$lib/consts';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
|
||||
import type { Item } from '$lib/types/item.server';
|
||||
import type { Item } from '$lib/types/item';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import LocationIcon from '@lucide/svelte/icons/map-pinned';
|
||||
import CheckIcon from '@lucide/svelte/icons/check';
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import * as RadioGroup from '$lib/components/ui/radio-group';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import * as Field from '$lib/components/ui/field';
|
||||
import { fileToBase64 } from '$lib/shared';
|
||||
|
||||
import ImageUpload from '$lib/components/custom/image-upload/image-upload.svelte';
|
||||
import { genDescription } from '$lib/db/items.remote';
|
||||
@@ -19,10 +20,7 @@
|
||||
async function onSelect(file: File) {
|
||||
isGenerating = true;
|
||||
|
||||
const buffer = await file.arrayBuffer();
|
||||
const base64 = btoa(
|
||||
String.fromCharCode(...new Uint8Array(buffer))
|
||||
);
|
||||
const base64 = await fileToBase64(file);
|
||||
|
||||
description = await genDescription(base64);
|
||||
isGenerating = false;
|
||||
|
||||
@@ -16,7 +16,7 @@ export const genDescription = command(v.string(), async (data) => {
|
||||
|
||||
const outputBuffer = await image
|
||||
.rotate()
|
||||
.resize({ fit: 'outside', height: 400, width: 400 })
|
||||
.resize({ fit: 'outside', height: 300, width: 300 })
|
||||
.jpeg({ quality: 80 }) // adjust if needed
|
||||
.toBuffer();
|
||||
|
||||
|
||||
+126
-10
@@ -1,4 +1,9 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
import sql from '$lib/db/db.server';
|
||||
import type { Item } from '$lib/types/item';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { type inquiryTokenPayload, Sender } from '$lib/types/inquiries';
|
||||
import { threadId } from 'node:worker_threads';
|
||||
|
||||
// Create a transporter object using SMTP transport
|
||||
export const transporter = nodemailer.createTransport({
|
||||
@@ -11,13 +16,124 @@ export const transporter = nodemailer.createTransport({
|
||||
}
|
||||
});
|
||||
|
||||
// export async function sendEmail() {
|
||||
// // Send mail with defined transport object
|
||||
// await transporter.sendMail({
|
||||
// from: `Westuffind Notifier <${process.env.EMAIL_USER}>`,
|
||||
// // to: info.emails.join(', '), // EMAILING OF REAL COMPANIES DISABLED, UNCOMMENT TO ENABLE
|
||||
// to: 'drake@marinodev.com', // TEMPORARY EMAIL FOR TESTING
|
||||
// subject: 'New Application Received!',
|
||||
// text: `A new application has been received for the posting ''!\n\nCheck it out at ${process.env.BASE_URL}/`
|
||||
// });
|
||||
// }
|
||||
export async function sendNewInquiryEmail(inquiryId: number) {
|
||||
// const item: Item = await sql`
|
||||
// SELECT json_agg(item_data) AS result
|
||||
// FROM (
|
||||
// SELECT
|
||||
// i.*,
|
||||
// (
|
||||
// SELECT json_agg(thread_data)
|
||||
// FROM (
|
||||
// SELECT
|
||||
// it.id,
|
||||
// it.item_id,
|
||||
// (
|
||||
// SELECT json_agg(im)
|
||||
// FROM inquiry_messages im
|
||||
// WHERE im.thread_id = it.id
|
||||
// ) AS messages
|
||||
// FROM inquiry_threads it
|
||||
// WHERE it.id = ${inquiryId}
|
||||
// ) AS thread_data
|
||||
// ) AS threads
|
||||
// FROM items i
|
||||
// WHERE i.id = (
|
||||
// SELECT item_id
|
||||
// FROM inquiry_threads
|
||||
// WHERE id = ${inquiryId}
|
||||
// )
|
||||
// ) AS item_data;
|
||||
//
|
||||
// `;
|
||||
|
||||
const item: Item = await sql`
|
||||
SELECT
|
||||
i.*,
|
||||
json_agg(
|
||||
jsonb_build_object(
|
||||
'id', t.id,
|
||||
'item_id', t.item_id,
|
||||
'created_at', t.created_at,
|
||||
'messages', m.messages
|
||||
)
|
||||
) FILTER (WHERE t.id = ${threadId}) AS threads
|
||||
FROM items i
|
||||
LEFT JOIN inquiry_threads t
|
||||
ON t.item_id = i.id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
json_agg(im.* ORDER BY im.created_at) AS messages
|
||||
FROM inquiry_messages im
|
||||
WHERE im.thread_id = t.id
|
||||
) m ON TRUE
|
||||
WHERE i.id = (SELECT item_id FROM inquiry_threads WHERE id = ${inquiryId})
|
||||
GROUP BY i.id;`;
|
||||
|
||||
const tokenPayload: inquiryTokenPayload = {
|
||||
threadId: inquiryId,
|
||||
sender: Sender.FINDER
|
||||
};
|
||||
const replyToken = jwt.sign(tokenPayload, process.env.JWT_SECRET!);
|
||||
|
||||
console.log(item);
|
||||
console.log(replyToken);
|
||||
// Send mail with defined transport object
|
||||
// await transporter.sendMail({
|
||||
// from: `Westuffind Notifier <${process.env.EMAIL_USER}>`,
|
||||
// to: item.emails,
|
||||
// // to: 'drake@marinodev.com', // TEMPORARY EMAIL FOR TESTING
|
||||
// replyTo: `${process.env.EMAIL_USER!.split('@')[0]}+${replyToken}${process.env.EMAIL_USER!.split('@')[1]}`,
|
||||
// subject: 'New Item Inquiry!',
|
||||
// text: `Someone has made an inquiry on the item with description: ${item.description}\nThey ask: ${item.threads![0].messages[0].body}\n\n\nRespond to this email directly, or click the below link to reply on Westuffinder\n${process.env.BASE_URL}/items/${item.id}/inquiries/${inquiryId}?token=${replyToken}`
|
||||
// });
|
||||
}
|
||||
|
||||
export async function sendInquiryMessageEmail(inquiryId: number, sender: Sender) {
|
||||
const item: Item = await sql`
|
||||
SELECT json_agg(item_data) AS result
|
||||
FROM (
|
||||
SELECT
|
||||
i.*,
|
||||
(
|
||||
SELECT json_agg(thread_data)
|
||||
FROM (
|
||||
SELECT
|
||||
it.id,
|
||||
it.item_id,
|
||||
(
|
||||
SELECT json_agg(im)
|
||||
FROM inquiry_messages im
|
||||
WHERE im.thread_id = it.id
|
||||
ORDER BY im.created_at
|
||||
) AS messages
|
||||
FROM inquiry_threads it
|
||||
WHERE it.id = ${inquiryId}
|
||||
) AS thread_data
|
||||
) AS threads
|
||||
FROM items i
|
||||
WHERE i.id = (
|
||||
SELECT item_id
|
||||
FROM inquiry_threads
|
||||
WHERE id = ${inquiryId}
|
||||
)
|
||||
) AS item_data;
|
||||
|
||||
`;
|
||||
|
||||
const tokenPayload: inquiryTokenPayload = {
|
||||
threadId: inquiryId,
|
||||
sender
|
||||
};
|
||||
const replyToken = jwt.sign(tokenPayload, process.env.JWT_SECRET!);
|
||||
|
||||
// Send mail with defined transport object
|
||||
await transporter.sendMail({
|
||||
from: `Westuffind Notifier <${process.env.EMAIL_USER}>`,
|
||||
to: item.emails,
|
||||
// to: 'drake@marinodev.com', // TEMPORARY EMAIL FOR TESTING
|
||||
replyTo: `${process.env.EMAIL_USER!.split('@')[0]}+${replyToken}${process.env.EMAIL_USER!.split('@')[1]}`,
|
||||
subject: 'New Item Inquiry!',
|
||||
text: `Someone has replied to the inquiry on the item with description: ${item.description}\nThey say: ${item.threads![0].messages[item.threads![0].messages.length - 1].body}\n\n\nRespond to this email directly, or click the below link to reply on Westuffinder\n${process.env.BASE_URL}/items/${item.id}/inquiries/${inquiryId}?token=${replyToken}`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export async function LLMDescribe(imageData: string) {
|
||||
};
|
||||
|
||||
console.log('AIing it');
|
||||
console.log(payload);
|
||||
|
||||
const res = await fetch(
|
||||
`http://${process.env.LLAMA_HOST!}:${process.env.LLAMA_PORT!}/v1/chat/completions`,
|
||||
@@ -35,7 +36,7 @@ export async function LLMDescribe(imageData: string) {
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
console.error(await res.text());
|
||||
console.log(await res.text());
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,3 +55,19 @@ export const dateFormatOptions: Intl.DateTimeFormatOptions = {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
};
|
||||
|
||||
export function fileToBase64(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = () => {
|
||||
const result = reader.result as string;
|
||||
// result is like: "data:image/jpeg;base64,AAAA..."
|
||||
const base64 = result.split(',')[1];
|
||||
resolve(base64);
|
||||
};
|
||||
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ export enum Sender {
|
||||
|
||||
export interface Message {
|
||||
id: number;
|
||||
// threadId: number;
|
||||
sender: Sender;
|
||||
body: string;
|
||||
createdAt: Date;
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Thread } from '$lib/types/inquiries.server';
|
||||
import type { Thread } from '$lib/types/inquiries';
|
||||
|
||||
export interface Item {
|
||||
id: number;
|
||||
Reference in New Issue
Block a user