Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type { Suggestion } from "../api/suggestions-api";
import { useLanguage } from "@/components/LanguageProvider";
import { useAuth } from "@/contexts/AuthContext";
import { useChatStore } from "@/hooks/store/chat";
import { useIsMobile } from "@/hooks/use-mobile";
import { environment } from "@/lib/config/environment";

const MAX_CLIENT_IMAGE_SIZE_BYTES = 10 * 1024 * 1024;
Expand Down Expand Up @@ -71,6 +72,7 @@ export function ChatInput({
const { t } = useLanguage();
const { user } = useAuth();
const setToast = useChatStore((s) => s.setToast);
const isMobile = useIsMobile();
const isUnauthenticated = !user;
const [isPestDialogOpen, setIsPestDialogOpen] = useState(false);
const [pestImage, setPestImage] = useState<File | null>(null);
Expand Down Expand Up @@ -331,70 +333,84 @@ export function ChatInput({
return (
<div className="bg-[#FFFFFF] dark:bg-[var(--inputBg-dark)] backdrop-blur supports-[backdrop-filter]:bg-[#FFFFFF] dark:supports-[backdrop-filter]:bg-[var(--inputBg-dark)]">
<Dialog open={isPestDialogOpen} onOpenChange={setIsPestDialogOpen}>
<DialogContent className="max-w-md overflow-hidden p-0 gap-0 bg-white dark:bg-[var(--background)]" showCloseButton={false}>
<DialogContent className="max-w-md overflow-hidden rounded-2xl p-0 gap-0 bg-white dark:bg-[var(--background)] border shadow-xl" showCloseButton={false}>
{/* Warm amber header — softer than before */}
<DialogHeader className="gap-0">
<div className="bg-amber-500 px-6 py-4 text-white">
<DialogTitle>{t("pestApi.title") as string}</DialogTitle>
<DialogDescription className="mt-1 text-amber-50">
<div className="bg-gradient-to-br from-amber-400 to-amber-500 px-6 py-5 text-white">
<div className="flex items-center gap-2 mb-1">
<Camera className="h-5 w-5 opacity-90" />
<DialogTitle className="text-lg font-semibold">
{t("pestApi.title") as string}
</DialogTitle>
</div>
<DialogDescription className="text-amber-50/90 text-sm leading-relaxed">
{t("pestApi.description") as string}
</DialogDescription>
</div>
</DialogHeader>
<div className="space-y-4 px-6 pb-6 pt-2">
<div className="rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900 dark:border-amber-900/50 dark:bg-amber-950/30 dark:text-amber-100">

<div className="px-6 py-5 space-y-5">
{/* Refined amber note with icon */}
<div className="flex items-start gap-3 rounded-xl border border-amber-100 bg-amber-50/60 px-4 py-3 text-sm text-amber-900 dark:border-amber-900/30 dark:bg-amber-950/20 dark:text-amber-100">
<svg className="mt-0.5 h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
</svg>
{t("pestApi.note") as string}
</div>

<div className="space-y-2">
<p className="text-center text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
{/* Photo section */}
<div className="space-y-3">
<p className="text-sm font-semibold text-foreground flex items-center gap-2">
<span className="inline-block h-1.5 w-1.5 rounded-full bg-amber-500" />
{t("pestApi.photoLabel") as string}
</p>

{pestImagePreview ? (
<div className="relative overflow-hidden rounded-2xl border border-border bg-muted/20 p-2">
<div className="relative overflow-hidden rounded-xl border border-border bg-muted/30 shadow-sm">
<img
src={pestImagePreview}
alt={pestImage?.name || "Pest API upload preview"}
className="max-h-64 w-full rounded-xl object-contain"
alt={pestImage?.name || "Crop image preview"}
className="max-h-56 w-full object-contain p-3"
/>
<Button
<button
type="button"
variant="destructive"
size="icon"
className="absolute right-4 top-4 h-8 w-8 rounded-full"
onClick={removePestFile}
className="absolute right-3 top-3 flex h-8 w-8 items-center justify-center rounded-full bg-black/50 text-white backdrop-blur-md transition-all hover:bg-black/70 hover:scale-105"
>
<X className="h-4 w-4" />
</Button>
</button>
</div>
) : (
<div className="rounded-2xl border-2 border-dashed border-border px-5 py-6">
<div className="flex items-start justify-center gap-6">
<button
type="button"
onClick={openPestApiPicker}
className="group flex flex-col items-center gap-2"
>
<div className="flex h-16 w-16 items-center justify-center rounded-full border-2 border-amber-300 text-amber-700 transition-colors group-hover:border-amber-500 group-hover:bg-amber-50 dark:border-amber-900/60 dark:text-amber-200 dark:group-hover:bg-amber-950/40">
<Camera className="h-7 w-7" />
</div>
<span className="text-center text-xs text-muted-foreground">
{t("imageUpload.captureImage") as string}
</span>
</button>
<div className="rounded-xl border-2 border-dashed border-amber-200/70 bg-amber-50/30 px-4 py-8 transition-all hover:border-amber-300 hover:bg-amber-50/50 dark:border-amber-900/30 dark:bg-amber-950/10">
<div className={cn("flex items-center justify-center", isMobile ? "gap-8" : "gap-4 flex-col")}>
{isMobile && (
<button
type="button"
onClick={openPestApiPicker}
className="group flex flex-col items-center gap-2.5"
>
<div className="flex h-14 w-14 items-center justify-center rounded-full border-2 border-amber-300/80 bg-white text-amber-600 shadow-sm transition-all group-hover:border-amber-400 group-hover:bg-amber-50 group-hover:shadow-md group-hover:scale-105 dark:border-amber-800 dark:bg-amber-950/30 dark:text-amber-300">
<Camera className="h-6 w-6" />
</div>
<span className="text-center text-sm font-medium text-foreground">
{t("imageUpload.captureImage") as string}
</span>
</button>
)}
<button
type="button"
onClick={openPestGalleryPicker}
className="group flex flex-col items-center gap-2"
className="group flex flex-col items-center gap-2.5"
>
<div className="flex h-16 w-16 items-center justify-center rounded-full border-2 border-amber-300 text-amber-700 transition-colors group-hover:border-amber-500 group-hover:bg-amber-50 dark:border-amber-900/60 dark:text-amber-200 dark:group-hover:bg-amber-950/40">
<ImageIcon className="h-7 w-7" />
<div className="flex h-14 w-14 items-center justify-center rounded-full border-2 border-amber-300/80 bg-white text-amber-600 shadow-sm transition-all group-hover:border-amber-400 group-hover:bg-amber-50 group-hover:shadow-md group-hover:scale-105 dark:border-amber-800 dark:bg-amber-950/30 dark:text-amber-300">
<ImageIcon className="h-6 w-6" />
</div>
<span className="text-center text-xs text-muted-foreground">
<span className="text-center text-sm font-medium text-foreground">
{t("imageUpload.selectFromGallery") as string}
</span>
</button>
</div>
<p className="mt-4 text-center text-xs text-muted-foreground">
<p className="mt-5 text-center text-xs text-muted-foreground/80">
{t("imageUpload.imageFormatHint") as string}
</p>
</div>
Expand All @@ -417,11 +433,23 @@ export function ChatInput({
onChange={onPestFilePicked}
/>
</div>
<DialogFooter className="border-t px-6 pb-6 pt-4 sm:justify-end">
<Button type="button" variant="outline" onClick={() => setIsPestDialogOpen(false)}>

{/* Footer buttons */}
<DialogFooter className="border-t border-border/50 px-6 py-4 gap-3 sm:justify-end bg-muted/20">
<Button
type="button"
variant="outline"
onClick={() => setIsPestDialogOpen(false)}
className="flex-1 sm:flex-none rounded-xl border-border/80 hover:bg-muted"
>
{t("pestApi.cancel") as string}
</Button>
<Button type="button" onClick={submitPestImage} disabled={isPestSubmitDisabled}>
<Button
type="button"
onClick={submitPestImage}
disabled={isPestSubmitDisabled}
className="flex-1 sm:flex-none rounded-xl bg-gradient-to-r from-amber-500 to-amber-600 text-white shadow-md hover:from-amber-600 hover:to-amber-700 hover:shadow-lg disabled:opacity-50 transition-all"
>
{t("pestApi.submit") as string}
</Button>
</DialogFooter>
Expand Down Expand Up @@ -546,20 +574,19 @@ export function ChatInput({
{charCount}/{maxLength}
</span>
)}
<div className="flex shrink-0 items-center pr-1">
<div className="flex shrink-0 items-center pr-2">
<Button
type="button"
size="icon"
disabled={disabled || isLoading || isUnauthenticated}
onClick={() => setIsPestDialogOpen(true)}
className={cn(
"h-9 w-9 rounded-full border border-transparent bg-transparent text-amber-700 hover:bg-amber-50 hover:text-amber-900 dark:text-amber-200 dark:hover:bg-amber-950/40 dark:hover:text-amber-100 shadow-none",
"h-12 w-12 rounded-full border border-transparent bg-transparent text-amber-700 hover:bg-amber-50 hover:text-amber-900 dark:text-amber-200 dark:hover:bg-amber-950/40 dark:hover:text-amber-100 shadow-none p-0",
disabled || isLoading || isUnauthenticated ? "opacity-50 cursor-not-allowed" : "cursor-pointer"
)}
aria-label={t("pestApi.trigger") as string}
title={t("pestApi.trigger") as string}
>
<Camera className="h-5 w-5" />
<Camera className="size-5" />
</Button>
</div>

Expand Down
8 changes: 4 additions & 4 deletions src/hooks/store/chat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,8 +391,8 @@ export const useChatStore = create<ChatStore>((set, get) => ({
};
}
});
},
() => set({ isInputLocked: false })
}
// Note: input stays locked until sendUserQuery fully resolves (after all stream chunks)
);

set({ isAssistantTyping: false, isInputLocked: false });
Expand Down Expand Up @@ -581,8 +581,8 @@ export const useChatStore = create<ChatStore>((set, get) => ({
};
}
});
},
() => set({ isInputLocked: false })
}
// Note: input stays locked until sendImageQuery fully resolves (after all stream chunks)
);

set({ isAssistantTyping: false, isInputLocked: false });
Expand Down
37 changes: 25 additions & 12 deletions src/layouts/chat-layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,36 @@ function ChatLayout() {
const [settingsOpen, setSettingsOpen] = useState(false);

useEffect(() => {
if (typeof window === "undefined" || !navigator.geolocation || !navigator.permissions?.query) {
if (typeof window === "undefined" || !navigator.geolocation) {
return;
}

let cancelled = false;

// Only auto-fetch when the browser already granted location permission.
void navigator.permissions
.query({ name: "geolocation" as PermissionName })
.then((status) => {
if (!cancelled && status.state === "granted") {
fetchLocation(t);
}
})
.catch(() => {
// Skip auto-request when the permission state cannot be checked.
});
const requestLocation = () => {
if (!cancelled) fetchLocation(t);
};

// If the Permissions API is available, check the current state first.
// - "granted" → fetch silently (no prompt shown to user)
// - "prompt" → actively request so the browser shows the permission dialog
// - "denied" → skip (user has blocked it; don't bother)
if (navigator.permissions?.query) {
void navigator.permissions
.query({ name: "geolocation" as PermissionName })
.then((status) => {
if (status.state === "granted" || status.state === "prompt") {
requestLocation();
}
})
.catch(() => {
// Fallback: just try — the browser will show its own prompt.
requestLocation();
});
} else {
// Permissions API not available — request directly.
requestLocation();
}

return () => {
cancelled = true;
Expand Down
8 changes: 4 additions & 4 deletions translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -286,10 +286,10 @@
"processingFailed": "Could not process the image. Please try another photo."
},
"pestApi": {
"trigger": "Pest API",
"title": "Use Pest API",
"description": "Upload a crop image only for pest or disease identification.",
"note": "This is only for the Pest API flow. Use a clear photo of the affected crop, leaf, stem, fruit, or visible pest.",
"trigger": "Pest & Disease Detection",
"title": "Pest & Disease Detection",
"description": "Upload a crop image to identify pests or diseases affecting your crop.",
"note": "Use a clear photo of the affected crop, leaf, stem, fruit, or visible pest for best results.",
"photoLabel": "Plant/Pest Image",
"continue": "Continue",
"submit": "Analyze & Submit",
Expand Down
8 changes: 4 additions & 4 deletions translations/gu.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@
"processingFailed": "ફોટો પ્રોસેસ થઈ શક્યો નથી. કૃપા કરીને બીજો ફોટો અજમાવો."
},
"pestApi": {
"trigger": "પેસ્ટ API",
"title": "પેસ્ટ API વાપરો",
"description": "પાકની છબી ફક્ત જીવાત અથવા રોગ ઓળખ માટે અપલોડ કરો.",
"note": "આ ફક્ત Pest API ફ્લો માટે છે. અસરગ્રસ્ત પાન, ડાંટી, ફળ, પાક અથવા દેખાતી જીવાતનો સ્પષ્ટ ફોટો લો.",
"trigger": "જીવાત અને રોગ ઓળખ",
"title": "જીવાત અને રોગ ઓળખ",
"description": "પાક પર જીવાત અથવા રોગ ઓળખવા માટે છબી અપલોડ કરો.",
"note": "સારા પરિણામ માટે અસરગ્રસ્ત પાન, ડાંટી, ફળ, પાક અથવા દેખાતી જીવાતનો સ્પષ્ટ ફોટો લો.",
"photoLabel": "પાક/જીવાત ફોટો",
"continue": "આગળ વધો",
"submit": "વિશ્લેષણ કરો અને મોકલો",
Expand Down
8 changes: 4 additions & 4 deletions translations/hi.json
Original file line number Diff line number Diff line change
Expand Up @@ -287,10 +287,10 @@
"processingFailed": "तस्वीर प्रोसेस नहीं हो सकी। कृपया दूसरी फोटो आज़माएँ।"
},
"pestApi": {
"trigger": "पेस्ट API",
"title": "पेस्ट API इस्तेमाल करें",
"description": "फसल की तस्वीर केवल कीट या रोग पहचान के लिए अपलोड करें।",
"note": "यह केवल Pest API फ्लो के लिए है। प्रभावित पत्ती, तना, फल, फसल या दिखाई देने वाले कीट की साफ तस्वीर लें।",
"trigger": "कीट और रोग पहचान",
"title": "कीट और रोग पहचान",
"description": "फसल पर कीट या रोग की पहचान के लिए तस्वीर अपलोड करें।",
"note": "बेहतर परिणाम के लिए प्रभावित पत्ती, तना, फल, फसल या दिखाई देने वाले कीट की साफ तस्वीर लें।",
"photoLabel": "पौधा/कीट तस्वीर",
"continue": "जारी रखें",
"submit": "विश्लेषण करें और भेजें",
Expand Down
8 changes: 4 additions & 4 deletions translations/mr.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,10 @@
"processingFailed": "फोटो प्रोसेस करता आला नाही. कृपया दुसरा फोटो वापरून पाहा."
},
"pestApi": {
"trigger": "पेस्ट API",
"title": "पेस्ट API वापरा",
"description": "पीक प्रतिमा फक्त कीड किंवा रोग ओळखीसाठी अपलोड करा.",
"note": "हे फक्त Pest API फ्लोसाठी आहे. प्रभावित पान, खोड, फळ, पीक किंवा दिसणाऱ्या किडीचा स्पष्ट फोटो वापरा.",
"trigger": "कीड आणि रोग ओळख",
"title": "कीड आणि रोग ओळख",
"description": "पिकावरील कीड किंवा रोग ओळखण्यासाठी प्रतिमा अपलोड करा.",
"note": "चांगल्या परिणामासाठी प्रभावित पान, खोड, फळ, पीक किंवा दिसणाऱ्या किडीचा स्पष्ट फोटो वापरा.",
"photoLabel": "पीक/कीड फोटो",
"continue": "पुढे जा",
"submit": "विश्लेषण करून पाठवा",
Expand Down
Loading